diff --git a/LibgenDesktop.Setup/AppFile.cs b/LibgenDesktop.Setup/AppFile.cs new file mode 100644 index 0000000..7226e76 --- /dev/null +++ b/LibgenDesktop.Setup/AppFile.cs @@ -0,0 +1,14 @@ +namespace LibgenDesktop.Setup +{ + internal class AppFile + { + public AppFile(string sourceFilePath, string targetFilePath) + { + SourceFilePath = sourceFilePath; + TargetFilePath = targetFilePath; + } + + public string SourceFilePath { get; } + public string TargetFilePath { get; } + } +} diff --git a/LibgenDesktop.Setup/AppFiles.cs b/LibgenDesktop.Setup/AppFiles.cs index 8ce6e0a..dc310ce 100644 --- a/LibgenDesktop.Setup/AppFiles.cs +++ b/LibgenDesktop.Setup/AppFiles.cs @@ -1,31 +1,58 @@ using System; +using System.Collections.Generic; namespace LibgenDesktop.Setup { internal static class AppFiles { + static AppFiles() + { + X86 = new List(); + X64 = new List(); + AddFile(Constants.MAIN_EXECUTABLE_NAME); + AddFile("System.ValueTuple.dll"); + AddFile("Newtonsoft.Json.dll"); + AddFile("SharpCompress.dll"); + AddFile("MaterialDesignThemes.Wpf.dll"); + AddFile("MaterialDesignColors.dll"); + AddFile("Dragablz.dll"); + AddFile("System.Data.SQLite.dll"); + AddFile("NLog.dll"); + AddFile("EPPlus.dll"); + AddFile("HtmlAgilityPack.dll"); + AddFile("Microsoft.WindowsAPICodePack.dll"); + AddFile("Microsoft.WindowsAPICodePack.Shell.dll"); + X86.Add(new AppFile(@"x86\SQLite.Interop.dll", "SQLite.Interop.dll")); + X64.Add(new AppFile(@"x64\SQLite.Interop.dll", "SQLite.Interop.dll")); + AddFile(@"Languages\English.lng"); + AddFile(@"Languages\Russian.lng"); + AddFile(@"Mirrors\mirrors.config"); + AddFile(@"Mirrors\libgen_io_nonfiction.xslt"); + AddFile(@"Mirrors\libgen_io_fiction.xslt"); + AddFile(@"Mirrors\libgen_io_scimag.xslt"); + AddFile(@"Mirrors\libgen_pw_nonfiction_step1.xslt"); + AddFile(@"Mirrors\libgen_pw_nonfiction_step2.xslt"); + AddFile(@"Mirrors\libgen_pw_fiction_step1.xslt"); + AddFile(@"Mirrors\libgen_pw_fiction_step2.xslt"); + AddFile(@"Mirrors\libgen_pw_scimag_step1.xslt"); + AddFile(@"Mirrors\libgen_pw_scimag_step2.xslt"); + AddFile(@"Mirrors\bookfi_net.xslt"); + AddFile(@"Mirrors\b_ok_org.xslt"); + AddFile(@"Mirrors\booksc_org.xslt"); + } + public static string GetBinariesDirectoryPath(bool is64Bit) { return String.Format(Constants.BINARIES_DIRECTORY_PATH_FORMAT, (is64Bit ? "64" : "86")); } - public static string[] GetFileList(bool is64Bit) + public static List X86 { get; } + public static List X64 { get; } + + private static void AddFile(string filePath) { - return new[] - { - Constants.MAIN_EXECUTABLE_NAME, - "System.ValueTuple.dll", - "Newtonsoft.Json.dll", - "SharpCompress.dll", - "MaterialDesignThemes.Wpf.dll", - "MaterialDesignColors.dll", - "Dragablz.dll", - "System.Data.SQLite.dll", - "NLog.dll", - "EPPlus.dll", - (is64Bit ? "x64" : "x86") + @"\SQLite.Interop.dll", - "mirrors.config" - }; + X86.Add(new AppFile(filePath, filePath)); + X64.Add(new AppFile(filePath, filePath)); } } } diff --git a/LibgenDesktop.Setup/AppPortable.cs b/LibgenDesktop.Setup/AppPortable.cs index dd7fb46..fe96666 100644 --- a/LibgenDesktop.Setup/AppPortable.cs +++ b/LibgenDesktop.Setup/AppPortable.cs @@ -1,5 +1,4 @@ using System; -using System.IO; using SharpCompress.Archives; using SharpCompress.Archives.Zip; using SharpCompress.Common; @@ -22,9 +21,9 @@ private static void BuildPortablePackage(bool is64Bit) { using (ZipArchive zipArchive = ZipArchive.Create()) { - foreach (string fileName in AppFiles.GetFileList(is64Bit)) + foreach (AppFile appFile in (is64Bit ? AppFiles.X64 : AppFiles.X86)) { - zipArchive.AddEntry(Path.GetFileName(fileName), Utils.GetFullFilePath(AppFiles.GetBinariesDirectoryPath(is64Bit), fileName)); + zipArchive.AddEntry(appFile.TargetFilePath, Utils.GetFullFilePath(AppFiles.GetBinariesDirectoryPath(is64Bit), appFile.SourceFilePath)); } string outputFilePath = Utils.GetFullFilePath(@"..\Release", String.Format(Constants.PORTABLE_PACKAGE_FILE_NAME_FORMAT, is64Bit ? 64 : 32)); zipArchive.SaveTo(outputFilePath, CompressionType.Deflate); diff --git a/LibgenDesktop.Setup/AppSetup.cs b/LibgenDesktop.Setup/AppSetup.cs index 103352e..0c8c096 100644 --- a/LibgenDesktop.Setup/AppSetup.cs +++ b/LibgenDesktop.Setup/AppSetup.cs @@ -1,4 +1,5 @@ using System; +using System.Collections.Generic; using System.Linq; using WixSharp; using WixSharp.CommonTasks; @@ -25,19 +26,33 @@ private static void BuildSetupPackage(bool is64Bit, string projectGuid) string normalizedCurrentVersion = Constants.CURRENT_VERSION.Count(c => c == '.') > 1 ? Constants.CURRENT_VERSION : Constants.CURRENT_VERSION + ".0"; string installerFileName = String.Format(Constants.INSTALLER_FILE_NAME_FORMAT, is64Bit ? 64 : 32); Project project = new Project(productTitle, new Dir(@"%ProgramFiles%\Libgen Desktop")); - Dir targetDirectory = project.Dirs.First().Dirs.First(); - foreach (string fileName in AppFiles.GetFileList(is64Bit)) + Dir rootDirectory = project.Dirs.First().Dirs.First(); + Dictionary> appFiles = GetAppFiles(is64Bit); + foreach (string subDirectoryName in appFiles.Keys) { - string filePath = Utils.GetFullFilePath(AppFiles.GetBinariesDirectoryPath(is64Bit), fileName); - File file = new File(filePath); - if (fileName == Constants.MAIN_EXECUTABLE_NAME) + Dir directory; + if (String.IsNullOrEmpty(subDirectoryName)) { - file.Shortcuts = new[] + directory = rootDirectory; + } + else + { + directory = new Dir(subDirectoryName); + rootDirectory.AddDir(directory); + } + foreach (AppFile appFile in appFiles[subDirectoryName]) + { + string filePath = Utils.GetFullFilePath(AppFiles.GetBinariesDirectoryPath(is64Bit), appFile.SourceFilePath); + File file = new File(filePath); + if (appFile.SourceFilePath == Constants.MAIN_EXECUTABLE_NAME) { - new FileShortcut(shortcutTitle, "%ProgramMenu%") - }; + file.Shortcuts = new[] + { + new FileShortcut(shortcutTitle, "%ProgramMenu%") + }; + } + directory.AddFile(file); } - targetDirectory.AddFile(file); } project.GUID = new Guid(projectGuid); project.ControlPanelInfo.Manufacturer = Constants.PRODUCT_COMPANY; @@ -69,5 +84,21 @@ private static void BuildSetupPackage(bool is64Bit, string projectGuid) project.BuildMsi(installerFileName); Utils.MoveFile($"{installerFileName}.msi", @"..\Release"); } + + private static Dictionary> GetAppFiles(bool is64Bit) + { + Dictionary> result = new Dictionary>(); + foreach (AppFile appFile in (is64Bit ? AppFiles.X64 : AppFiles.X86)) + { + int directorySeparatorIndex = appFile.TargetFilePath.IndexOf('\\'); + string directory = directorySeparatorIndex != -1 ? appFile.TargetFilePath.Substring(0, directorySeparatorIndex) : String.Empty; + if (!result.ContainsKey(directory)) + { + result.Add(directory, new List()); + } + result[directory].Add(appFile); + } + return result; + } } } diff --git a/LibgenDesktop.Setup/Constants.cs b/LibgenDesktop.Setup/Constants.cs index c94f144..65309f3 100644 --- a/LibgenDesktop.Setup/Constants.cs +++ b/LibgenDesktop.Setup/Constants.cs @@ -2,7 +2,7 @@ { internal static class Constants { - public const string CURRENT_VERSION = "0.12"; + public const string CURRENT_VERSION = "0.13"; public const string PRODUCT_TITLE_FORMAT = "Libgen Desktop " + CURRENT_VERSION + " ({0}-bit)"; public const string SHORTCUT_TITLE_FORMAT = "Libgen Desktop ({0}-bit)"; public const string PRODUCT_COMPANY = "Libgen Apps"; diff --git a/LibgenDesktop.Setup/LibgenDesktop.Setup.csproj b/LibgenDesktop.Setup/LibgenDesktop.Setup.csproj index a1f6315..c984d5b 100644 --- a/LibgenDesktop.Setup/LibgenDesktop.Setup.csproj +++ b/LibgenDesktop.Setup/LibgenDesktop.Setup.csproj @@ -71,6 +71,7 @@ + diff --git a/LibgenDesktop/App.xaml b/LibgenDesktop/App.xaml index fe7f884..12ba901 100644 --- a/LibgenDesktop/App.xaml +++ b/LibgenDesktop/App.xaml @@ -9,7 +9,7 @@ - + diff --git a/LibgenDesktop/App.xaml.cs b/LibgenDesktop/App.xaml.cs index ec1d3e5..c3ce12f 100644 --- a/LibgenDesktop/App.xaml.cs +++ b/LibgenDesktop/App.xaml.cs @@ -4,19 +4,21 @@ using LibgenDesktop.Common; using LibgenDesktop.Infrastructure; using LibgenDesktop.Models; -using LibgenDesktop.ViewModels; +using LibgenDesktop.ViewModels.Windows; namespace LibgenDesktop { public partial class App : Application { + private MainModel mainModel; + protected override void OnStartup(StartupEventArgs e) { base.OnStartup(e); SetupExceptionHandlers(); try { - MainModel mainModel = new MainModel(); + mainModel = new MainModel(); if (mainModel.LocalDatabaseStatus == MainModel.DatabaseStatus.OPENED) { ShowMainWindow(mainModel); @@ -30,7 +32,7 @@ protected override void OnStartup(StartupEventArgs e) { Logger.EnableLogging(); ShowErrorWindow(exception); - Shutdown(); + Close(); } } @@ -38,7 +40,7 @@ private void ShowMainWindow(MainModel mainModel) { MainWindowViewModel mainWindowViewModel = new MainWindowViewModel(mainModel); IWindowContext windowContext = WindowManager.CreateWindow(RegisteredWindows.WindowKey.MAIN_WINDOW, mainWindowViewModel); - windowContext.Closed += (sender, args) => Shutdown(); + windowContext.Closed += (sender, args) => Close(); windowContext.Show(); } @@ -53,7 +55,7 @@ private void ShowCreateDatabaseWindow(MainModel mainModel) } else { - Shutdown(); + Close(); } } @@ -74,10 +76,34 @@ private void SetupExceptionHandlers() private void ShowErrorWindow(Exception exception) { - Logger.Exception(exception); - ErrorWindowViewModel errorWindowViewModel = new ErrorWindowViewModel(exception?.ToString() ?? "(null)"); - IWindowContext errorWindowContext = WindowManager.CreateWindow(RegisteredWindows.WindowKey.ERROR_WINDOW, errorWindowViewModel); - errorWindowContext.ShowDialog(); + if (!Dispatcher.CheckAccess()) + { + Dispatcher.Invoke(() => ShowErrorWindow(exception)); + } + else + { + Logger.Exception(exception); + try + { + ErrorWindowViewModel errorWindowViewModel = new ErrorWindowViewModel(exception?.ToString() ?? "(null)", + mainModel?.Localization?.CurrentLanguage); + IWindowContext errorWindowContext = WindowManager.CreateWindow(RegisteredWindows.WindowKey.ERROR_WINDOW, errorWindowViewModel); + errorWindowContext.ShowDialog(); + } + catch (Exception errorWindowException) + { + Logger.Exception(errorWindowException); + } + } + } + + private void Close() + { + if (mainModel != null) + { + mainModel.Downloader.Shutdown(); + } + Shutdown(); } } } diff --git a/LibgenDesktop/Common/Constants.cs b/LibgenDesktop/Common/Constants.cs index 818fe62..ee6e2c3 100644 --- a/LibgenDesktop/Common/Constants.cs +++ b/LibgenDesktop/Common/Constants.cs @@ -2,14 +2,19 @@ { internal static class Constants { - public const string CURRENT_VERSION = "0.12"; - public const string CURRENT_GITHUB_RELEASE_NAME = "v0.12 alpha"; + public const string CURRENT_VERSION = "0.13"; + public const string CURRENT_GITHUB_RELEASE_NAME = "v0.13 alpha"; public const string CURRENT_DATABASE_VERSION = "0.7"; public const string APP_SETTINGS_FILE_NAME = "libgen.config"; + public const string MIRRORS_DIRECTORY_NAME = "Mirrors"; public const string MIRRORS_FILE_NAME = "mirrors.config"; + public const string LANGUAGES_DIRECTORY_NAME = "Languages"; + public const string DEFAULT_LANGUAGE_NAME = "English"; + public const string DEFAULT_DOWNLOAD_DIRECTORY_NAME = "Downloads"; + public const string DOWNLOAD_LIST_FILE_NAME = "downloads.json"; - public const int MAIN_WINDOW_MIN_WIDTH = 1000; + public const int MAIN_WINDOW_MIN_WIDTH = 1050; public const int MAIN_WINDOW_MIN_HEIGHT = 500; public const int NON_FICTION_DETAILS_WINDOW_MIN_WIDTH = 1000; public const int NON_FICTION_DETAILS_WINDOW_MIN_HEIGHT = 500; @@ -38,6 +43,9 @@ internal static class Constants public const int SCI_MAG_GRID_YEAR_COLUMN_MIN_WIDTH = 60; public const int SCI_MAG_GRID_FILESIZE_COLUMN_MIN_WIDTH = 130; public const int SCI_MAG_GRID_DOI_COLUMN_MIN_WIDTH = 150; + public const int DOWNLOAD_MANAGER_TAB_DOWNLOADS_PANEL_MIN_HEIGHT = 100; + public const int DOWNLOAD_MANAGER_TAB_LOG_PANEL_MIN_HEIGHT = 50; + public const int DOWNLOAD_MANAGER_TAB_LOG_PANEL_DEFAULT_HEIGHT = 150; public const int ERROR_WINDOW_DEFAULT_WIDTH = 620; public const int ERROR_WINDOW_DEFAULT_HEIGHT = 450; public const int ERROR_WINDOW_MIN_WIDTH = 400; @@ -55,7 +63,7 @@ internal static class Constants public const int MESSAGE_BOX_WINDOW_WIDTH = 500; public const string DEFAULT_DATABASE_FILE_NAME = "libgen.db"; - public const int DEFAULT_MAIN_WINDOW_WIDTH = 1000; + public const int DEFAULT_MAIN_WINDOW_WIDTH = 1050; public const int DEFAULT_MAIN_WINDOW_HEIGHT = 650; public const int DEFAULT_NON_FICTION_DETAILS_WINDOW_WIDTH = 1200; public const int DEFAULT_NON_FICTION_DETAILS_WINDOW_HEIGHT = 618; @@ -91,7 +99,15 @@ internal static class Constants public const double IMPORT_PROGRESS_UPDATE_INTERVAL = 0.5; public const double SYNCHRONIZATION_PROGRESS_UPDATE_INTERVAL = 0.1; public const int DATABASE_TRANSACTION_BATCH = 500; - public const int MAX_EXPORT_ROWS_PER_FILE = 1000000; + public const int MAX_EXPORT_ROWS_PER_FILE = 1048575; + public const int MIN_DOWNLOAD_TIMEOUT = 15; + public const int MAX_DOWNLOAD_TIMEOUT = 9999; + public const int DEFAULT_DOWNLOAD_TIMEOUT = 120; + public const int MAX_DOWNLOAD_ATTEMPT_COUNT = 99; + public const int DEFAULT_DOWNLOAD_ATTEMPT_COUNT = 3; + public const int MAX_DOWNLOAD_RETRY_DELAY = 9999; + public const int DEFAULT_DOWNLOAD_RETRY_DELAY = 60; + public const int MAX_DOWNLOAD_REDIRECT_COUNT = 10; public const string GITHUB_RELEASE_API_URL = "https://api.github.com/repos/libgenapps/LibgenDesktop/releases"; public const string USER_AGENT = "LibgenDesktop/" + CURRENT_VERSION; diff --git a/LibgenDesktop/Common/Environment.cs b/LibgenDesktop/Common/Environment.cs index 73c69f9..22f5190 100644 --- a/LibgenDesktop/Common/Environment.cs +++ b/LibgenDesktop/Common/Environment.cs @@ -47,7 +47,8 @@ static Environment() string logFileName = $"{DateTime.Now.ToString("yyyy-MM-dd_HH-mm-ss")}.log"; LogFilePath = Path.Combine(AppDataDirectory, logFileName); AppSettingsFilePath = Path.Combine(AppDataDirectory, APP_SETTINGS_FILE_NAME); - MirrorsFilePath = Path.Combine(AppBinariesDirectory, MIRRORS_FILE_NAME); + MirrorsDirectoryPath = Path.Combine(AppBinariesDirectory, MIRRORS_DIRECTORY_NAME); + LanguagesDirectoryPath = Path.Combine(AppBinariesDirectory, LANGUAGES_DIRECTORY_NAME); OsVersion = GetOsVersion(); NetFrameworkVersion = GetNetFrameworkVersion(); IsIn64BitProcess = System.Environment.Is64BitProcess; @@ -121,7 +122,8 @@ private static string GetNetFrameworkVersion() public static string AppDataDirectory { get; } public static string LogFilePath { get; } public static string AppSettingsFilePath { get; } - public static string MirrorsFilePath { get; } + public static string MirrorsDirectoryPath { get; } + public static string LanguagesDirectoryPath { get; } public static string OsVersion { get; } public static string NetFrameworkVersion { get; } public static bool IsInPortableMode { get; } diff --git a/LibgenDesktop/Common/Logger.cs b/LibgenDesktop/Common/Logger.cs index f186e2a..1bb7c43 100644 --- a/LibgenDesktop/Common/Logger.cs +++ b/LibgenDesktop/Common/Logger.cs @@ -1,4 +1,5 @@ using System; +using System.Linq; using System.Runtime.CompilerServices; using NLog; using NLog.Config; @@ -36,7 +37,7 @@ public static void Debug(params string[] lines) { if (loggingEnabled) { - logger.Log(typeof(Logger), new LogEventInfo(LogLevel.Debug, String.Empty, String.Join("\r\n", lines))); + logger.Log(typeof(Logger), new LogEventInfo(LogLevel.Debug, String.Empty, String.Join("\r\n", lines.Where(line => !String.IsNullOrEmpty(line))))); } } diff --git a/LibgenDesktop/Infrastructure/IMessageBox.cs b/LibgenDesktop/Infrastructure/IMessageBox.cs new file mode 100644 index 0000000..8045783 --- /dev/null +++ b/LibgenDesktop/Infrastructure/IMessageBox.cs @@ -0,0 +1,8 @@ +namespace LibgenDesktop.Infrastructure +{ + internal interface IMessageBox + { + void ShowMessage(string title, string text, string ok, IWindowContext parentWindowContext); + bool ShowPrompt(string title, string text, string yes, string no, IWindowContext parentWindowContext); + } +} \ No newline at end of file diff --git a/LibgenDesktop/Infrastructure/MessageBox.cs b/LibgenDesktop/Infrastructure/MessageBox.cs new file mode 100644 index 0000000..39fd25f --- /dev/null +++ b/LibgenDesktop/Infrastructure/MessageBox.cs @@ -0,0 +1,17 @@ +using LibgenDesktop.Views.Windows; + +namespace LibgenDesktop.Infrastructure +{ + internal class MessageBox : IMessageBox + { + public void ShowMessage(string title, string text, string ok, IWindowContext parentWindowContext) + { + MessageBoxWindow.ShowMessage(title, text, ok, parentWindowContext); + } + + public bool ShowPrompt(string title, string text, string yes, string no, IWindowContext parentWindowContext) + { + return MessageBoxWindow.ShowPrompt(title, text, yes, no, parentWindowContext); + } + } +} diff --git a/LibgenDesktop/Infrastructure/RegisteredWindows.cs b/LibgenDesktop/Infrastructure/RegisteredWindows.cs index c09a763..4b0332b 100644 --- a/LibgenDesktop/Infrastructure/RegisteredWindows.cs +++ b/LibgenDesktop/Infrastructure/RegisteredWindows.cs @@ -1,7 +1,7 @@ using System; using System.Collections.Generic; -using LibgenDesktop.ViewModels; -using LibgenDesktop.Views; +using LibgenDesktop.ViewModels.Windows; +using LibgenDesktop.Views.Windows; namespace LibgenDesktop.Infrastructure { @@ -48,9 +48,11 @@ static RegisteredWindows() RegisterWindow(WindowKey.SETTINGS_WINDOW, typeof(SettingsWindow), typeof(SettingsWindowViewModel)); RegisterWindow(WindowKey.SYNCHRONIZATION_WINDOW, typeof(SynchronizationWindow), typeof(SynchronizationWindowViewModel)); RegisterWindow(WindowKey.APPLICATION_UPDATE_WINDOW, typeof(ApplicationUpdateWindow), typeof(ApplicationUpdateWindowViewModel)); + MessageBox = new MessageBox(); } public static Dictionary AllWindows { get; } + public static IMessageBox MessageBox { get; } private static void RegisterWindow(WindowKey windowKey, Type windowType, Type viewModelType) { diff --git a/LibgenDesktop/Infrastructure/SelectFolderDialogParameters.cs b/LibgenDesktop/Infrastructure/SelectFolderDialogParameters.cs new file mode 100644 index 0000000..d77f533 --- /dev/null +++ b/LibgenDesktop/Infrastructure/SelectFolderDialogParameters.cs @@ -0,0 +1,8 @@ +namespace LibgenDesktop.Infrastructure +{ + internal class SelectFolderDialogParameters + { + public string DialogTitle { get; set; } + public string InitialDirectory { get; set; } + } +} diff --git a/LibgenDesktop/Infrastructure/SelectFolderDialogResult.cs b/LibgenDesktop/Infrastructure/SelectFolderDialogResult.cs new file mode 100644 index 0000000..a5cbf82 --- /dev/null +++ b/LibgenDesktop/Infrastructure/SelectFolderDialogResult.cs @@ -0,0 +1,8 @@ +namespace LibgenDesktop.Infrastructure +{ + internal class SelectFolderDialogResult + { + public bool DialogResult { get; set; } + public string SelectedFolderPath { get; set; } + } +} diff --git a/LibgenDesktop/Infrastructure/ViewModelEvent.cs b/LibgenDesktop/Infrastructure/ViewModelEvent.cs index 3d62a1d..cce15e7 100644 --- a/LibgenDesktop/Infrastructure/ViewModelEvent.cs +++ b/LibgenDesktop/Infrastructure/ViewModelEvent.cs @@ -4,7 +4,9 @@ public class ViewModelEvent { public enum RegisteredEventId { - FOCUS_SEARCH_TEXT_BOX = 1 + FOCUS_SEARCH_TEXT_BOX = 1, + SCROLL_TO_SELECTION, + BRING_TO_FRONT } public ViewModelEvent(RegisteredEventId eventId) diff --git a/LibgenDesktop/Infrastructure/WindowManager.cs b/LibgenDesktop/Infrastructure/WindowManager.cs index 8abc5cb..f8ed334 100644 --- a/LibgenDesktop/Infrastructure/WindowManager.cs +++ b/LibgenDesktop/Infrastructure/WindowManager.cs @@ -6,6 +6,7 @@ using System.Windows.Interop; using System.Windows.Threading; using Microsoft.Win32; +using Microsoft.WindowsAPICodePack.Dialogs; namespace LibgenDesktop.Infrastructure { @@ -91,28 +92,14 @@ public static void ExecuteActionInBackground(Action action) Dispatcher.CurrentDispatcher.BeginInvoke(DispatcherPriority.ApplicationIdle, action); } - public static void ShowMessageBox(string title, string text, IWindowContext parentWindowContext = null) + public static void ShowMessage(string title, string text, string ok, IWindowContext parentWindowContext) { - if (parentWindowContext != null) - { - MessageBox.Show((parentWindowContext as WindowContext)?.Window, text, title); - } - else - { - MessageBox.Show(text, title); - } + RegisteredWindows.MessageBox.ShowMessage(title, text, ok, parentWindowContext); } - public static bool ShowPrompt(string title, string text, IWindowContext parentWindowContext = null) + public static bool ShowPrompt(string title, string text, string yes, string no, IWindowContext parentWindowContext) { - if (parentWindowContext != null) - { - return MessageBox.Show((parentWindowContext as WindowContext)?.Window, text, title, MessageBoxButton.YesNo) == MessageBoxResult.Yes; - } - else - { - return MessageBox.Show(text, title, MessageBoxButton.YesNo) == MessageBoxResult.Yes; - } + return RegisteredWindows.MessageBox.ShowPrompt(title, text, yes, no, parentWindowContext); } public static OpenFileDialogResult ShowOpenFileDialog(OpenFileDialogParameters openFileDialogParameters) @@ -171,6 +158,20 @@ public static SaveFileDialogResult ShowSaveFileDialog(SaveFileDialogParameters s return result; } + public static SelectFolderDialogResult ShowSelectFolderDialog(SelectFolderDialogParameters selectFolderDialogParameters) + { + using (CommonOpenFileDialog commonOpenFileDialog = new CommonOpenFileDialog()) + { + commonOpenFileDialog.IsFolderPicker = true; + commonOpenFileDialog.Title = selectFolderDialogParameters.DialogTitle; + commonOpenFileDialog.InitialDirectory = selectFolderDialogParameters.InitialDirectory; + SelectFolderDialogResult result = new SelectFolderDialogResult(); + result.DialogResult = commonOpenFileDialog.ShowDialog() == CommonFileDialogResult.Ok; + result.SelectedFolderPath = result.DialogResult ? commonOpenFileDialog.FileName : null; + return result; + } + } + public static void RemoveWindowMaximizeButton(Window window) { RemoveWindowStyle(window, WS_MAXIMIZEBOX); diff --git a/LibgenDesktop/LibgenDesktop.csproj b/LibgenDesktop/LibgenDesktop.csproj index e04a69b..f5c8de7 100644 --- a/LibgenDesktop/LibgenDesktop.csproj +++ b/LibgenDesktop/LibgenDesktop.csproj @@ -58,6 +58,9 @@ ..\packages\EPPlus.4.1.1\lib\net40\EPPlus.dll + + ..\packages\HtmlAgilityPack.1.6.17\lib\Net45\HtmlAgilityPack.dll + ..\packages\MaterialDesignColors.1.1.3\lib\net45\MaterialDesignColors.dll True @@ -66,6 +69,12 @@ ..\packages\MaterialDesignThemes.2.3.1.953\lib\net45\MaterialDesignThemes.Wpf.dll True + + ..\packages\WindowsAPICodePack-Core.1.1.2\lib\Microsoft.WindowsAPICodePack.dll + + + ..\packages\WindowsAPICodePack-Shell.1.1.1\lib\Microsoft.WindowsAPICodePack.Shell.dll + ..\packages\Newtonsoft.Json.10.0.3\lib\net45\Newtonsoft.Json.dll True @@ -79,16 +88,14 @@ - - ..\packages\System.Data.SQLite.Core.1.0.106.0\lib\net45\System.Data.SQLite.dll - True + + ..\packages\System.Data.SQLite.Core.1.0.107.0\lib\net45\System.Data.SQLite.dll - + ..\packages\System.ValueTuple.4.4.0\lib\netstandard1.0\System.ValueTuple.dll - @@ -112,17 +119,31 @@ + + + + + + + + + + + + + + @@ -143,7 +164,38 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + @@ -159,66 +211,89 @@ - + + + - - - - - - - - - - - - - + + + + + + + + + + + + + + + + + + + + + + + + + + + + + - - - - - - - - - - + + + + + + + + + + ApplicationUpdateWindow.xaml AddTabButton.xaml + CloseTabButton.xaml + ExportPanel.xaml ImportLogPanel.xaml + + PressedToggleButton.xaml + - + MessageBoxWindow.xaml - + SynchronizationWindow.xaml - + SciMagDetailsWindow.xaml - + FictionDetailsWindow.xaml - + NonFictionDetailsWindow.xaml @@ -235,13 +310,13 @@ Toolbar.xaml - + CreateDatabaseWindow.xaml - + ErrorWindow.xaml - + ImportWindow.xaml @@ -268,7 +343,7 @@ SearchTab.xaml - + SettingsWindow.xaml @@ -278,8 +353,9 @@ + - + Designer MSBuild:Compile @@ -299,7 +375,11 @@ Designer MSBuild:Compile - + + Designer + MSBuild:Compile + + MSBuild:Compile Designer @@ -323,19 +403,19 @@ MSBuild:Compile Designer - + MSBuild:Compile Designer - + MSBuild:Compile Designer - + MSBuild:Compile Designer - + Designer MSBuild:Compile @@ -355,19 +435,19 @@ Designer MSBuild:Compile - + Designer MSBuild:Compile - + Designer MSBuild:Compile - + Designer MSBuild:Compile - + MSBuild:Compile Designer @@ -381,10 +461,9 @@ - - + - + MainWindow.xaml Code @@ -456,7 +535,7 @@ Designer MSBuild:Compile - + Designer MSBuild:Compile @@ -521,9 +600,65 @@ SettingsSingleFileGenerator Settings.Designer.cs - + + PreserveNewest + Mirrors\mirrors.config + + + PreserveNewest + Mirrors\libgen_io_nonfiction.xslt + + + PreserveNewest + Mirrors\libgen_io_fiction.xslt + + + PreserveNewest + Mirrors\libgen_io_scimag.xslt + + + PreserveNewest + Mirrors\libgen_pw_nonfiction_step1.xslt + + + PreserveNewest + Mirrors\libgen_pw_nonfiction_step2.xslt + + + PreserveNewest + Mirrors\libgen_pw_fiction_step1.xslt + + + PreserveNewest + Mirrors\libgen_pw_fiction_step2.xslt + + + PreserveNewest + Mirrors\libgen_pw_scimag_step1.xslt + + + PreserveNewest + Mirrors\libgen_pw_scimag_step2.xslt + + + PreserveNewest + Mirrors\bookfi_net.xslt + + + PreserveNewest + Mirrors\b_ok_org.xslt + + + PreserveNewest + Mirrors\booksc_org.xslt + + + PreserveNewest + Languages\Russian.lng + + PreserveNewest - mirrors.config + Languages\English.lng @@ -535,6 +670,13 @@ + - + + + + This project references NuGet package(s) that are missing on this computer. Use NuGet Package Restore to download them. For more information, see http://go.microsoft.com/fwlink/?LinkID=322105. The missing file is {0}. + + + \ No newline at end of file diff --git a/LibgenDesktop/Models/Database/SearchQueryParser.cs b/LibgenDesktop/Models/Database/SearchQueryParser.cs index a78687f..42091d3 100644 --- a/LibgenDesktop/Models/Database/SearchQueryParser.cs +++ b/LibgenDesktop/Models/Database/SearchQueryParser.cs @@ -1,8 +1,5 @@ using System; using System.Collections.Generic; -using System.Linq; -using System.Text; -using System.Threading.Tasks; namespace LibgenDesktop.Models.Database { diff --git a/LibgenDesktop/Models/Download/DownloadItem.cs b/LibgenDesktop/Models/Download/DownloadItem.cs new file mode 100644 index 0000000..b58552c --- /dev/null +++ b/LibgenDesktop/Models/Download/DownloadItem.cs @@ -0,0 +1,84 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Threading; + +namespace LibgenDesktop.Models.Download +{ + internal class DownloadItem + { + private CancellationTokenSource cancellationTokenSource; + + public DownloadItem(Guid id, string downloadPageUrl, string downloadDirectory, string fileName, string downloadTransformations) + { + cancellationTokenSource = new CancellationTokenSource(); + Id = id; + DownloadPageUrl = downloadPageUrl; + DownloadDirectory = downloadDirectory; + Logs = new List(); + Cookies = new Dictionary(StringComparer.OrdinalIgnoreCase); + DownloadTransformations = downloadTransformations; + CancellationToken = cancellationTokenSource.Token; + FileName = fileName; + DirectFileUrl = null; + Referer = null; + Status = DownloadItemStatus.QUEUED; + FileCreated = false; + FileHandleOpened = false; + DownloadedFileSize = null; + TotalFileSize = null; + CurrentAttempt = 1; + } + + private DownloadItem(DownloadItem source) + { + Id = source.Id; + DownloadPageUrl = source.DownloadPageUrl; + DownloadDirectory = source.DownloadDirectory; + Logs = source.Logs.ToList(); + Cookies = new Dictionary(source.Cookies, StringComparer.OrdinalIgnoreCase); + DownloadTransformations = source.DownloadTransformations; + FileName = source.FileName; + DirectFileUrl = source.DirectFileUrl; + Referer = source.Referer; + Status = source.Status; + FileCreated = source.FileCreated; + DownloadedFileSize = source.DownloadedFileSize; + TotalFileSize = source.TotalFileSize; + CurrentAttempt = source.CurrentAttempt; + } + + public Guid Id { get; } + public string DownloadPageUrl { get; } + public string DownloadDirectory { get; } + public List Logs { get; } + public Dictionary Cookies { get; } + public string DownloadTransformations { get; } + public CancellationToken CancellationToken { get; set; } + public string FileName { get; set; } + public string DirectFileUrl { get; set; } + public string Referer { get; set; } + public DownloadItemStatus Status { get; set; } + public bool FileCreated { get; set; } + public bool FileHandleOpened { get; set; } + public long? DownloadedFileSize { get; set; } + public long? TotalFileSize { get; set; } + public int CurrentAttempt { get; set; } + + public void CancelDownload() + { + cancellationTokenSource.Cancel(); + } + + public void CreateNewCancellationToken() + { + cancellationTokenSource = new CancellationTokenSource(); + CancellationToken = cancellationTokenSource.Token; + } + + public DownloadItem Clone() + { + return new DownloadItem(this); + } + } +} diff --git a/LibgenDesktop/Models/Download/DownloadItemAddedEventArgs.cs b/LibgenDesktop/Models/Download/DownloadItemAddedEventArgs.cs new file mode 100644 index 0000000..43147e1 --- /dev/null +++ b/LibgenDesktop/Models/Download/DownloadItemAddedEventArgs.cs @@ -0,0 +1,14 @@ +using System; + +namespace LibgenDesktop.Models.Download +{ + internal class DownloadItemAddedEventArgs : EventArgs + { + public DownloadItemAddedEventArgs(DownloadItem addedDownloadItem) + { + AddedDownloadItem = addedDownloadItem.Clone(); + } + + public DownloadItem AddedDownloadItem { get; } + } +} diff --git a/LibgenDesktop/Models/Download/DownloadItemChangedEventArgs.cs b/LibgenDesktop/Models/Download/DownloadItemChangedEventArgs.cs new file mode 100644 index 0000000..f8933a4 --- /dev/null +++ b/LibgenDesktop/Models/Download/DownloadItemChangedEventArgs.cs @@ -0,0 +1,14 @@ +using System; + +namespace LibgenDesktop.Models.Download +{ + internal class DownloadItemChangedEventArgs : EventArgs + { + public DownloadItemChangedEventArgs(DownloadItem changedDownloadItem) + { + ChangedDownloadItem = changedDownloadItem.Clone(); + } + + public DownloadItem ChangedDownloadItem { get; } + } +} diff --git a/LibgenDesktop/Models/Download/DownloadItemLogLine.cs b/LibgenDesktop/Models/Download/DownloadItemLogLine.cs new file mode 100644 index 0000000..6c9ee11 --- /dev/null +++ b/LibgenDesktop/Models/Download/DownloadItemLogLine.cs @@ -0,0 +1,18 @@ +using System; + +namespace LibgenDesktop.Models.Download +{ + internal class DownloadItemLogLine + { + public DownloadItemLogLine(DownloadItemLogLineType type, DateTime timeStamp, string text) + { + Type = type; + TimeStamp = timeStamp; + Text = text; + } + + public DownloadItemLogLineType Type { get; } + public DateTime TimeStamp { get; } + public string Text { get; } + } +} diff --git a/LibgenDesktop/Models/Download/DownloadItemLogLineEventArgs.cs b/LibgenDesktop/Models/Download/DownloadItemLogLineEventArgs.cs new file mode 100644 index 0000000..4b473af --- /dev/null +++ b/LibgenDesktop/Models/Download/DownloadItemLogLineEventArgs.cs @@ -0,0 +1,18 @@ +using System; + +namespace LibgenDesktop.Models.Download +{ + internal class DownloadItemLogLineEventArgs : EventArgs + { + public DownloadItemLogLineEventArgs(Guid downloadItemId, int lineIndex, DownloadItemLogLine logLine) + { + DownloadItemId = downloadItemId; + LineIndex = lineIndex; + LogLine = logLine; + } + + public Guid DownloadItemId { get; } + public int LineIndex { get; } + public DownloadItemLogLine LogLine { get; } + } +} diff --git a/LibgenDesktop/Models/Download/DownloadItemLogLineType.cs b/LibgenDesktop/Models/Download/DownloadItemLogLineType.cs new file mode 100644 index 0000000..a678ffb --- /dev/null +++ b/LibgenDesktop/Models/Download/DownloadItemLogLineType.cs @@ -0,0 +1,10 @@ +namespace LibgenDesktop.Models.Download +{ + internal enum DownloadItemLogLineType + { + INFORMATIONAL = 1, + DEBUG, + COMPLETED, + ERROR + } +} diff --git a/LibgenDesktop/Models/Download/DownloadItemRemovedEventArgs.cs b/LibgenDesktop/Models/Download/DownloadItemRemovedEventArgs.cs new file mode 100644 index 0000000..3732edc --- /dev/null +++ b/LibgenDesktop/Models/Download/DownloadItemRemovedEventArgs.cs @@ -0,0 +1,14 @@ +using System; + +namespace LibgenDesktop.Models.Download +{ + internal class DownloadItemRemovedEventArgs : EventArgs + { + public DownloadItemRemovedEventArgs(DownloadItem removedDownloadItem) + { + RemovedDownloadItem = removedDownloadItem.Clone(); + } + + public DownloadItem RemovedDownloadItem { get; } + } +} diff --git a/LibgenDesktop/Models/Download/DownloadItemStatus.cs b/LibgenDesktop/Models/Download/DownloadItemStatus.cs new file mode 100644 index 0000000..cf92aff --- /dev/null +++ b/LibgenDesktop/Models/Download/DownloadItemStatus.cs @@ -0,0 +1,13 @@ +namespace LibgenDesktop.Models.Download +{ + internal enum DownloadItemStatus + { + QUEUED = 1, + DOWNLOADING, + STOPPED, + RETRY_DELAY, + ERROR, + COMPLETED, + REMOVED + } +} diff --git a/LibgenDesktop/Models/Download/DownloadQueueStorage.cs b/LibgenDesktop/Models/Download/DownloadQueueStorage.cs new file mode 100644 index 0000000..81ff892 --- /dev/null +++ b/LibgenDesktop/Models/Download/DownloadQueueStorage.cs @@ -0,0 +1,116 @@ +using System; +using System.Collections.Generic; +using System.IO; +using System.Linq; +using Newtonsoft.Json; + +namespace LibgenDesktop.Models.Download +{ + internal static class DownloadQueueStorage + { + internal class StorageDownloadItem + { + public Guid Id { get; set; } + public string FileName { get; set; } + public DownloadItemStatus Status { get; set; } + public string DownloadPageUrl { get; set; } + public string DirectFileUrl { get; set; } + public string DownloadDirectory { get; set; } + public Dictionary Cookies { get; set; } + public string Referer { get; set; } + public string DownloadTransformations { get; set; } + public bool FileCreated { get; set; } + public long? DownloadedFileSize { get; set; } + public long? TotalFileSize { get; set; } + public int CurrentAttempt { get; set; } + } + + public static List LoadDownloadQueue(string downloadQueueFilePath) + { + List storageDownloads; + try + { + if (File.Exists(downloadQueueFilePath)) + { + JsonSerializer jsonSerializer = new JsonSerializer(); + using (StreamReader streamReader = new StreamReader(downloadQueueFilePath)) + using (JsonTextReader jsonTextReader = new JsonTextReader(streamReader)) + { + storageDownloads = jsonSerializer.Deserialize>(jsonTextReader); + } + } + else + { + storageDownloads = null; + } + } + catch + { + storageDownloads = null; + } + List result; + if (storageDownloads != null) + { + result = storageDownloads.Select(FromStorageDownloadItem).ToList(); + } + else + { + result = new List(); + } + return result; + } + + public static void SaveDownloadQueue(string downloadQueueFilePath, List downloadQueue) + { + List storageDownloads = downloadQueue.Select(ToStorageDownloadItem).ToList(); + JsonSerializer jsonSerializer = new JsonSerializer(); + using (StreamWriter streamWriter = new StreamWriter(downloadQueueFilePath)) + using (JsonTextWriter jsonTextWriter = new JsonTextWriter(streamWriter)) + { + jsonTextWriter.Formatting = Formatting.Indented; + jsonTextWriter.Indentation = 4; + jsonSerializer.Serialize(jsonTextWriter, storageDownloads); + } + } + + private static StorageDownloadItem ToStorageDownloadItem(DownloadItem downloadItem) + { + return new StorageDownloadItem + { + Id = downloadItem.Id, + FileName = downloadItem.FileName, + Status = downloadItem.Status, + DownloadPageUrl = downloadItem.DownloadPageUrl, + DirectFileUrl = downloadItem.DirectFileUrl, + DownloadDirectory = downloadItem.DownloadDirectory, + Cookies = downloadItem.Cookies, + Referer = downloadItem.Referer, + DownloadTransformations = downloadItem.DownloadTransformations, + FileCreated = downloadItem.FileCreated, + DownloadedFileSize = downloadItem.DownloadedFileSize, + TotalFileSize = downloadItem.TotalFileSize, + CurrentAttempt = downloadItem.CurrentAttempt + }; + } + + private static DownloadItem FromStorageDownloadItem(StorageDownloadItem storageDownloadItem) + { + DownloadItem result = new DownloadItem(storageDownloadItem.Id, storageDownloadItem.DownloadPageUrl, storageDownloadItem.DownloadDirectory, + storageDownloadItem.FileName, storageDownloadItem.DownloadTransformations) + { + Status = storageDownloadItem.Status, + DirectFileUrl = storageDownloadItem.DirectFileUrl, + Referer = storageDownloadItem.Referer, + FileCreated = storageDownloadItem.FileCreated, + DownloadedFileSize = storageDownloadItem.DownloadedFileSize, + TotalFileSize = storageDownloadItem.TotalFileSize, + CurrentAttempt = storageDownloadItem.CurrentAttempt + }; + foreach (KeyValuePair cookieValuePair in storageDownloadItem.Cookies) + { + result.Cookies.Add(cookieValuePair.Key, cookieValuePair.Value); + } + return result; + } + } +} diff --git a/LibgenDesktop/Models/Download/Downloader.cs b/LibgenDesktop/Models/Download/Downloader.cs new file mode 100644 index 0000000..6710186 --- /dev/null +++ b/LibgenDesktop/Models/Download/Downloader.cs @@ -0,0 +1,1024 @@ +using System; +using System.Collections.Concurrent; +using System.Collections.Generic; +using System.IO; +using System.Linq; +using System.Net; +using System.Net.Http; +using System.Net.Http.Headers; +using System.Text; +using System.Threading; +using System.Threading.Tasks; +using System.Xml; +using System.Xml.Xsl; +using HtmlAgilityPack; +using LibgenDesktop.Common; +using LibgenDesktop.Models.Localization; +using LibgenDesktop.Models.Localization.Localizators; +using LibgenDesktop.Models.Utils; +using static LibgenDesktop.Common.Constants; +using static LibgenDesktop.Models.Settings.AppSettings; +using Environment = LibgenDesktop.Common.Environment; + +namespace LibgenDesktop.Models.Download +{ + internal partial class Downloader + { + private readonly object downloadQueueLock; + private readonly string downloadQueueFilePath; + private readonly List downloadQueue; + private readonly BlockingCollection eventQueue; + private readonly Task downloadTask; + private readonly AutoResetEvent downloadTaskResetEvent; + private DownloadManagerLocalizator localization; + private HttpClient httpClient; + private DownloadSettings downloadSettings; + private bool isInOfflineMode; + private bool isShuttingDown; + + public Downloader() + { + downloadQueueLock = new object(); + downloadQueueFilePath = Path.Combine(Environment.AppDataDirectory, DOWNLOAD_LIST_FILE_NAME); + downloadQueue = DownloadQueueStorage.LoadDownloadQueue(downloadQueueFilePath); + eventQueue = new BlockingCollection(); + downloadTaskResetEvent = new AutoResetEvent(false); + localization = null; + httpClient = null; + downloadSettings = null; + isInOfflineMode = false; + isShuttingDown = false; + StartEventPublisherTask(); + downloadTask = StartDownloadTask(); + } + + public event EventHandler DownloaderEvent; + + public void Configure(Language currentLanguage, NetworkSettings networkSettings, DownloadSettings downloadSettings) + { + localization = currentLanguage.DownloadManager; + this.downloadSettings = downloadSettings; + if (networkSettings.OfflineMode) + { + if (!isInOfflineMode) + { + isInOfflineMode = true; + SwitchToOfflineMode(); + } + } + else + { + httpClient = CreateNewHttpClient(networkSettings, downloadSettings); + isInOfflineMode = false; + ResumeDownloadTask(); + } + } + + public List GetDownloadQueueSnapshot() + { + lock (downloadQueueLock) + { + return downloadQueue.ToList(); + } + } + + public DownloadItem GetDownloadItemByDownloadPageUrl(string downloadPageUrl) + { + lock (downloadQueueLock) + { + return downloadQueue.FirstOrDefault(downloadItem => downloadItem.DownloadPageUrl == downloadPageUrl)?.Clone(); + } + } + + public void EnqueueDownloadItem(string downloadPageUrl, string fileNameWithoutExtension, string fileExtension, string md5Hash, + string downloadTransformations) + { + string fileName = String.Concat(FileUtils.RemoveInvalidFileNameCharacters(fileNameWithoutExtension, md5Hash), ".", fileExtension.ToLower()); + lock (downloadQueueLock) + { + DownloadItem newDownloadItem = new DownloadItem(Guid.NewGuid(), downloadPageUrl, downloadSettings.DownloadDirectory, fileName, downloadTransformations); + downloadQueue.Add(newDownloadItem); + eventQueue.Add(new DownloadItemAddedEventArgs(newDownloadItem)); + AddLogLine(newDownloadItem, DownloadItemLogLineType.INFORMATIONAL, localization.LogLineQueued); + SaveDownloadQueue(); + ResumeDownloadTask(); + } + } + + public void StartDownloads(IEnumerable downloadItemIds) + { + lock (downloadQueueLock) + { + foreach (Guid downloadItemId in downloadItemIds) + { + DownloadItem downloadItem = downloadQueue.FirstOrDefault(item => item.Id == downloadItemId); + if (downloadItem == null) + { + Logger.Debug($"Download item with ID = {downloadItemId} not found."); + return; + } + Logger.Debug($"Start download requested for download ID = {downloadItemId}."); + if (downloadItem.Status == DownloadItemStatus.STOPPED || downloadItem.Status == DownloadItemStatus.ERROR) + { + downloadItem.CreateNewCancellationToken(); + downloadItem.CurrentAttempt = 1; + ReportStatusChange(downloadItem, DownloadItemStatus.QUEUED, DownloadItemLogLineType.INFORMATIONAL, localization.LogLineQueued); + ResumeDownloadTask(); + } + else + { + Logger.Debug($"Incorrect download item status = {downloadItem.Status} to start it."); + } + } + SaveDownloadQueue(); + } + } + + public void StopDownloads(IEnumerable downloadItemIds) + { + lock (downloadQueueLock) + { + foreach (Guid downloadItemId in downloadItemIds) + { + DownloadItem downloadItem = downloadQueue.FirstOrDefault(item => item.Id == downloadItemId); + if (downloadItem == null) + { + Logger.Debug($"Download item with ID = {downloadItemId} not found."); + return; + } + Logger.Debug($"Stop download requested for download ID = {downloadItemId}."); + if (downloadItem.Status == DownloadItemStatus.QUEUED || downloadItem.Status == DownloadItemStatus.DOWNLOADING || + downloadItem.Status == DownloadItemStatus.RETRY_DELAY) + { + downloadItem.CancelDownload(); + ReportStatusChange(downloadItem, DownloadItemStatus.STOPPED, DownloadItemLogLineType.INFORMATIONAL, localization.LogLineStopped); + } + else + { + Logger.Debug($"Incorrect download item status = {downloadItem.Status} to stop it."); + } + } + SaveDownloadQueue(); + } + } + + public void RemoveDownloads(IEnumerable downloadItemIds) + { + lock (downloadQueueLock) + { + foreach (Guid downloadItemId in downloadItemIds) + { + DownloadItem downloadItem = downloadQueue.FirstOrDefault(item => item.Id == downloadItemId); + if (downloadItem == null) + { + Logger.Debug($"Download item with ID = {downloadItemId} not found."); + return; + } + Logger.Debug($"Remove download requested for download ID = {downloadItemId}."); + downloadItem.CancelDownload(); + downloadQueue.Remove(downloadItem); + downloadItem.Status = DownloadItemStatus.REMOVED; + eventQueue.Add(new DownloadItemRemovedEventArgs(downloadItem)); + if (downloadItem.FileCreated && !downloadItem.FileHandleOpened) + { + string filePath = Path.Combine(downloadItem.DownloadDirectory, downloadItem.FileName); + if (downloadItem.Status != DownloadItemStatus.COMPLETED) + { + filePath += ".part"; + } + try + { + File.Delete(filePath); + } + catch (Exception exception) + { + Logger.Exception(exception); + } + } + } + SaveDownloadQueue(); + } + } + + public void Shutdown() + { + isShuttingDown = true; + lock (downloadQueueLock) + { + SaveDownloadQueue(); + foreach (DownloadItem downloadItem in downloadQueue) + { + downloadItem.Status = DownloadItemStatus.STOPPED; + downloadItem.CancelDownload(); + } + } + ResumeDownloadTask(); + try + { + downloadTask.Wait(); + } + catch (Exception exception) + { + Logger.Exception(exception); + } + Logger.Debug("Downloader was shut down successfully."); + } + + private void ResumeDownloadTask() + { + downloadTaskResetEvent.Set(); + } + + private Task StartDownloadTask() + { + return Task.Run(async () => + { + while (true) + { + DownloadItem downloadItem = null; + if (isShuttingDown) + { + return; + } + if (httpClient == null || isInOfflineMode) + { + downloadTaskResetEvent.WaitOne(); + } + else + { + lock (downloadQueueLock) + { + if (downloadItem == null || downloadItem.Status != DownloadItemStatus.DOWNLOADING) + { + downloadItem = downloadQueue.FirstOrDefault(item => item.Status == DownloadItemStatus.QUEUED || + item.Status == DownloadItemStatus.DOWNLOADING || item.Status == DownloadItemStatus.RETRY_DELAY); + if (downloadItem != null) + { + ReportStatusChange(downloadItem, DownloadItemStatus.DOWNLOADING, DownloadItemLogLineType.INFORMATIONAL, + localization.LogLineStarted); + SaveDownloadQueue(); + } + } + } + if (downloadItem == null) + { + downloadTaskResetEvent.WaitOne(); + } + else + { + if (!downloadItem.FileCreated) + { + string url = downloadItem.DownloadPageUrl; + string referer = null; + if (!String.IsNullOrWhiteSpace(downloadItem.DownloadTransformations)) + { + foreach (string transformationName in downloadItem.DownloadTransformations.Split(','). + Select(transformation => transformation.Trim())) + { + if (String.IsNullOrEmpty(transformationName)) + { + continue; + } + string pageContent; + try + { + pageContent = await DownloadPageAsync(downloadItem, url, referer); + } + catch (TaskCanceledException) + { + Logger.Debug("Page download has been cancelled."); + break; + } + if (pageContent == null) + { + break; + } + try + { + referer = url; + url = ExecuteTransformation(pageContent, transformationName); + } + catch (Exception exception) + { + Logger.Debug($"Transformation {transformationName} threw an exception."); + Logger.Exception(exception); + ReportError(downloadItem, localization.GetLogLineTransformationError(transformationName)); + break; + } + bool isUrlValid; + if (String.IsNullOrWhiteSpace(url)) + { + isUrlValid = false; + } + else if (!url.ToLower().StartsWith("http://") && !url.ToLower().StartsWith("https://")) + { + isUrlValid = false; + } + else + { + url = url.Replace("<", "%3C").Replace(">", "%3E"); + isUrlValid = Uri.IsWellFormedUriString(url, UriKind.Absolute); + } + if (!isUrlValid) + { + Logger.Debug($"Transformation {transformationName} failed, returned string:", url); + ReportError(downloadItem, localization.GetLogLineTransformationReturnedIncorrectUrl(transformationName)); + break; + } + } + } + if (downloadItem.CancellationToken.IsCancellationRequested) + { + continue; + } + bool skipFileDownload = false; + lock (downloadQueueLock) + { + if (downloadItem.Status == DownloadItemStatus.DOWNLOADING) + { + downloadItem.DirectFileUrl = url; + downloadItem.Referer = referer; + SaveDownloadQueue(); + } + else + { + SaveDownloadQueue(); + if (downloadItem.Status == DownloadItemStatus.RETRY_DELAY) + { + skipFileDownload = true; + } + else + { + continue; + } + } + } + if (!skipFileDownload) + { + await DownloadFileAsync(downloadItem); + } + } + else + { + await DownloadFileAsync(downloadItem); + } + bool retryDelay; + CancellationToken cancellationToken; + int currentAttempt; + lock (downloadQueueLock) + { + retryDelay = downloadItem.Status == DownloadItemStatus.RETRY_DELAY; + cancellationToken = downloadItem.CancellationToken; + currentAttempt = downloadItem.CurrentAttempt; + } + if (retryDelay) + { + if (currentAttempt == downloadSettings.Attempts) + { + lock (downloadQueueLock) + { + ReportError(downloadItem, localization.LogLineMaximumDownloadAttempts); + SaveDownloadQueue(); + } + } + else + { + AddLogLine(downloadItem, DownloadItemLogLineType.INFORMATIONAL, + localization.GetLogLineRetryDelay(downloadSettings.RetryDelay)); + bool isCancelled = false; + try + { + await Task.Delay(TimeSpan.FromSeconds(downloadSettings.RetryDelay), cancellationToken); + } + catch (TaskCanceledException) + { + isCancelled = true; + } + if (!isCancelled) + { + lock (downloadQueueLock) + { + downloadItem.Status = DownloadItemStatus.DOWNLOADING; + currentAttempt++; + downloadItem.CurrentAttempt = currentAttempt; + AddLogLine(downloadItem, DownloadItemLogLineType.INFORMATIONAL, + localization.GetLogLineAttempt(currentAttempt, downloadSettings.Attempts)); + } + } + } + } + } + } + } + }); + } + + private async Task DownloadPageAsync(DownloadItem downloadItem, string url, string referer) + { + AddLogLine(downloadItem, DownloadItemLogLineType.INFORMATIONAL, localization.GetLogLineDownloadingPage(Uri.UnescapeDataString(url))); + bool isRedirect; + int redirectCount = 0; + HttpResponseMessage response; + do + { + response = await SendDownloadRequestAsync(downloadItem, url, referer); + if (response == null) + { + return null; + } + isRedirect = response.StatusCode == HttpStatusCode.MovedPermanently || response.StatusCode == HttpStatusCode.Redirect || + response.StatusCode == HttpStatusCode.TemporaryRedirect; + if (isRedirect) + { + referer = url; + url = response.Headers.Location.AbsoluteUri; + AddLogLine(downloadItem, DownloadItemLogLineType.INFORMATIONAL, localization.GetLogLineRedirect(Uri.UnescapeDataString(url))); + redirectCount++; + if (redirectCount == MAX_DOWNLOAD_REDIRECT_COUNT) + { + ReportError(downloadItem, localization.LogLineTooManyRedirects); + return null; + } + } + } + while (isRedirect); + if (response.StatusCode != HttpStatusCode.OK) + { + string statusCode = $"{(int)response.StatusCode} {response.StatusCode}"; + Logger.Debug($"Server returned non-successful status code: {statusCode}."); + string messageText = localization.GetLogLineNonSuccessfulStatusCode(statusCode); + if (response.StatusCode == HttpStatusCode.InternalServerError || response.StatusCode == HttpStatusCode.BadGateway || + response.StatusCode == HttpStatusCode.ServiceUnavailable || response.StatusCode == HttpStatusCode.GatewayTimeout) + { + ReportStatusChange(downloadItem, DownloadItemStatus.RETRY_DELAY, DownloadItemLogLineType.INFORMATIONAL, messageText); + } + else + { + ReportError(downloadItem, messageText); + } + return null; + } + return await response.Content.ReadAsStringAsync(); + } + + private async Task DownloadFileAsync(DownloadItem downloadItem) + { + try + { + AddLogLine(downloadItem, DownloadItemLogLineType.INFORMATIONAL, + localization.GetLogLineDownloadingFile(Uri.UnescapeDataString(downloadItem.DirectFileUrl))); + if (!Directory.Exists(downloadItem.DownloadDirectory)) + { + try + { + Directory.CreateDirectory(downloadItem.DownloadDirectory); + } + catch (Exception exception) + { + Logger.Exception(exception); + ReportError(downloadItem, localization.GetLogLineCannotCreateDownloadDirectory(downloadItem.DownloadDirectory)); + } + } + string fileName = downloadItem.FileCreated ? downloadItem.FileName : GenerateFileName(downloadItem.DownloadDirectory, downloadItem.FileName); + string targetFilePath = Path.Combine(downloadItem.DownloadDirectory, fileName); + string partFilePath = targetFilePath + ".part"; + bool isCompleted; + bool isDeleteRequested; + FileStream destinationFileStream; + try + { + destinationFileStream = new FileStream(partFilePath, FileMode.Append, FileAccess.Write, FileShare.None); + } + catch (Exception exception) + { + Logger.Exception(exception); + ReportError(downloadItem, localization.GetLogLineCannotCreateOrOpenFile(partFilePath)); + return; + } + using (destinationFileStream) + { + lock (downloadQueueLock) + { + downloadItem.FileName = fileName; + downloadItem.FileCreated = true; + downloadItem.FileHandleOpened = true; + SaveDownloadQueue(); + } + await DownloadFileAsync(downloadItem, destinationFileStream); + lock (downloadQueueLock) + { + isCompleted = downloadItem.Status == DownloadItemStatus.COMPLETED; + isDeleteRequested = downloadItem.Status == DownloadItemStatus.REMOVED; + SaveDownloadQueue(); + } + } + if (isCompleted) + { + bool moveFileError = false; + try + { + File.Move(partFilePath, targetFilePath); + } + catch (Exception exception) + { + Logger.Debug($"File rename error: partFilePath = {partFilePath}, targetFilePath = {targetFilePath}"); + Logger.Exception(exception); + ReportError(downloadItem, localization.GetLogLineCannotRenamePartFile(partFilePath, targetFilePath)); + moveFileError = true; + } + if (!moveFileError) + { + ReportStatusChange(downloadItem, DownloadItemStatus.COMPLETED, DownloadItemLogLineType.COMPLETED, localization.LogLineCompleted); + } + } + else if (isDeleteRequested) + { + try + { + File.Delete(partFilePath); + } + catch (Exception exception) + { + Logger.Debug($"Part file delete error: partFilePath = {partFilePath}"); + Logger.Exception(exception); + } + } + lock (downloadQueueLock) + { + downloadItem.FileHandleOpened = false; + } + } + catch (Exception exception) + { + Logger.Exception(exception); + ReportError(downloadItem, localization.LogLineUnexpectedError); + } + } + + private async Task DownloadFileAsync(DownloadItem downloadItem, FileStream destinationFileStream) + { + string url = downloadItem.DirectFileUrl; + string referer = downloadItem.Referer; + long startPosition = destinationFileStream.Position; + bool partialDownload = startPosition > 0; + bool isRedirect; + int redirectCount = 0; + HttpResponseMessage response; + do + { + response = await SendDownloadRequestAsync(downloadItem, url, referer, startPosition); + if (response == null) + { + return; + } + isRedirect = response.StatusCode == HttpStatusCode.MovedPermanently || response.StatusCode == HttpStatusCode.Redirect || + response.StatusCode == HttpStatusCode.TemporaryRedirect; + if (isRedirect) + { + referer = url; + url = response.Headers.Location.AbsoluteUri; + AddLogLine(downloadItem, DownloadItemLogLineType.INFORMATIONAL, localization.GetLogLineRedirect(Uri.UnescapeDataString(url))); + redirectCount++; + if (redirectCount == MAX_DOWNLOAD_REDIRECT_COUNT) + { + ReportError(downloadItem, localization.LogLineTooManyRedirects); + return; + } + } + } + while (isRedirect); + if (response.StatusCode != HttpStatusCode.OK && response.StatusCode != HttpStatusCode.PartialContent) + { + string statusCode = $"{(int)response.StatusCode} {response.StatusCode}"; + Logger.Debug($"Server returned non-successful status code: {statusCode}."); + string messageText = localization.GetLogLineNonSuccessfulStatusCode(statusCode); + if (response.StatusCode == HttpStatusCode.InternalServerError || response.StatusCode == HttpStatusCode.BadGateway || + response.StatusCode == HttpStatusCode.ServiceUnavailable || response.StatusCode == HttpStatusCode.GatewayTimeout) + { + ReportStatusChange(downloadItem, DownloadItemStatus.RETRY_DELAY, DownloadItemLogLineType.INFORMATIONAL, messageText); + } + else + { + ReportError(downloadItem, messageText); + } + return; + } + if (response.Content.Headers.ContentType != null && response.Content.Headers.ContentType.MediaType.CompareOrdinalIgnoreCase("text/html")) + { + Logger.Debug($"Server returned HTML page instead of the file."); + ReportError(downloadItem, localization.LogLineHtmlPageReturned); + return; + } + if (partialDownload && response.StatusCode == HttpStatusCode.OK) + { + Logger.Debug("Server doesn't support partial downloads."); + ReportError(downloadItem, localization.LogLineNoPartialDownloadSupport); + return; + } + long? contentLength = response.Content.Headers.ContentLength; + if (!contentLength.HasValue) + { + Logger.Debug("Server did not return Content-Length value."); + AddLogLine(downloadItem, DownloadItemLogLineType.INFORMATIONAL, localization.LogLineNoContentLengthWarning); + } + long? remainingSize = contentLength; + lock (downloadQueueLock) + { + downloadItem.DownloadedFileSize = startPosition; + if (remainingSize.HasValue) + { + downloadItem.TotalFileSize = startPosition + remainingSize; + } + else + { + downloadItem.TotalFileSize = null; + } + ReportChange(downloadItem); + } + if (remainingSize == 0) + { + Logger.Debug($"File download is complete."); + lock (downloadQueueLock) + { + downloadItem.Status = DownloadItemStatus.COMPLETED; + } + return; + } + if (partialDownload) + { + if (remainingSize.HasValue) + { + Logger.Debug($"Remaining download size is {remainingSize.Value} bytes."); + AddLogLine(downloadItem, DownloadItemLogLineType.INFORMATIONAL, + localization.GetLogLineResumingFileDownloadKnownFileSize(remainingSize.Value)); + } + else + { + Logger.Debug($"Remaining download size is unknown."); + AddLogLine(downloadItem, DownloadItemLogLineType.INFORMATIONAL, localization.LogLineResumingFileDownloadUnknownFileSize); + } + } + else + { + if (remainingSize.HasValue) + { + Logger.Debug($"Download file size is {remainingSize.Value} bytes."); + AddLogLine(downloadItem, DownloadItemLogLineType.INFORMATIONAL, + localization.GetLogLineStartingFileDownloadKnownFileSize(remainingSize.Value)); + } + else + { + Logger.Debug($"Download file size is unknown."); + AddLogLine(downloadItem, DownloadItemLogLineType.INFORMATIONAL, localization.LogLineStartingFileDownloadUnknownFileSize); + } + } + Stream downloadStream = await response.Content.ReadAsStreamAsync(); + byte[] buffer = new byte[4096]; + long downloadedBytes = 0; + while (!downloadItem.CancellationToken.IsCancellationRequested) + { + int bytesRead; + try + { + bytesRead = downloadStream.Read(buffer, 0, buffer.Length); + } + catch (IOException ioException) + { + bool expectedError = false; + if (ioException.InnerException != null) + { + if (ioException.InnerException is WebException webException) + { + switch (webException.Status) + { + case WebExceptionStatus.Timeout: + Logger.Debug("Download timeout."); + ReportStatusChange(downloadItem, DownloadItemStatus.RETRY_DELAY, DownloadItemLogLineType.INFORMATIONAL, + localization.LogLineServerResponseTimeout); + expectedError = true; + break; + case WebExceptionStatus.RequestCanceled: + Logger.Debug("File download has been cancelled."); + expectedError = true; + break; + } + } + } + if (!expectedError) + { + Logger.Exception(ioException); + ReportError(downloadItem, localization.LogLineUnexpectedError); + } + break; + } + catch (Exception exception) + { + if (downloadItem.CancellationToken.IsCancellationRequested) + { + Logger.Debug("File download has been cancelled."); + break; + } + Logger.Exception(exception); + ReportError(downloadItem, localization.LogLineUnexpectedError); + break; + } + if (bytesRead == 0) + { + bool isCompleted = !remainingSize.HasValue || downloadedBytes == remainingSize.Value; + Logger.Debug($"File download is {(isCompleted ? "complete" : "incomplete")}."); + if (isCompleted) + { + lock (downloadQueueLock) + { + downloadItem.Status = DownloadItemStatus.COMPLETED; + } + } + else + { + ReportError(downloadItem, localization.LogLineDownloadIncompleteError); + } + break; + } + try + { + destinationFileStream.Write(buffer, 0, bytesRead); + } + catch (Exception exception) + { + Logger.Exception(exception); + ReportError(downloadItem, localization.LogLineFileWriteError); + break; + } + downloadedBytes += bytesRead; + downloadItem.DownloadedFileSize = startPosition + downloadedBytes; + ReportChange(downloadItem); + } + } + + private async Task SendDownloadRequestAsync(DownloadItem downloadItem, string url, string referer, long? startPosition = null) + { + bool partialDownload = startPosition.HasValue && startPosition.Value > 0; + if (!partialDownload) + { + Logger.Debug($"Requesting {url}"); + } + else + { + Logger.Debug($"Requesting {url}, range: {startPosition.Value} - end."); + } + HttpRequestMessage request = new HttpRequestMessage(HttpMethod.Get, url); + request.Headers.UserAgent.ParseAdd(USER_AGENT); + if (downloadItem.Cookies.Any()) + { + request.Headers.Add("Cookie", GenerateCookieHeader(downloadItem.Cookies)); + } + if (partialDownload) + { + request.Headers.Range = new RangeHeaderValue(startPosition.Value, null); + } + if (!String.IsNullOrEmpty(referer)) + { + request.Headers.Referrer = new Uri(referer); + } + string requestHeaders = request.Headers.ToString().TrimEnd(); + Logger.Debug("Request headers:", requestHeaders); + StringBuilder requestLogBuilder = new StringBuilder(); + requestLogBuilder.Append(localization.LogLineRequest); + requestLogBuilder.AppendLine(":"); + requestLogBuilder.Append("GET "); + requestLogBuilder.AppendLine(url); + requestLogBuilder.AppendLine(requestHeaders); + AddLogLine(downloadItem, DownloadItemLogLineType.DEBUG, requestLogBuilder.ToString().TrimEnd()); + HttpResponseMessage response; + try + { + response = await SendRequestAsync(request, downloadItem.CancellationToken); + } + catch (TimeoutException) + { + Logger.Debug("Download timeout."); + ReportStatusChange(downloadItem, DownloadItemStatus.RETRY_DELAY, DownloadItemLogLineType.INFORMATIONAL, + localization.LogLineServerResponseTimeout); + return null; + } + catch (AggregateException aggregateException) + { + if (downloadItem.CancellationToken.IsCancellationRequested) + { + Logger.Debug("File download has been cancelled."); + } + else + { + Logger.Exception(aggregateException); + ReportError(downloadItem, localization.LogLineUnexpectedError); + } + return null; + } + catch (Exception exception) + { + Logger.Exception(exception); + ReportError(downloadItem, localization.LogLineUnexpectedError); + return null; + } + Logger.Debug($"Response status code: {(int)response.StatusCode} {response.StatusCode}."); + string responseHeaders = response.Headers.ToString().TrimEnd(); + string responseContentHeaders = response.Content.Headers.ToString().TrimEnd(); + Logger.Debug("Response headers:", responseHeaders, responseContentHeaders); + StringBuilder responseLogBuilder = new StringBuilder(); + responseLogBuilder.Append(localization.LogLineResponse); + responseLogBuilder.AppendLine(":"); + responseLogBuilder.Append((int)response.StatusCode); + responseLogBuilder.Append(" "); + responseLogBuilder.AppendLine(response.StatusCode.ToString()); + if (!String.IsNullOrEmpty(responseHeaders)) + { + responseLogBuilder.AppendLine(responseHeaders); + } + if (!String.IsNullOrEmpty(responseContentHeaders)) + { + responseLogBuilder.AppendLine(responseContentHeaders); + } + AddLogLine(downloadItem, DownloadItemLogLineType.DEBUG, responseLogBuilder.ToString().TrimEnd()); + if (response.Headers.TryGetValues("Set-Cookie", out IEnumerable cookieHeaders)) + { + Uri uri = new Uri(url); + foreach (string cookieHeader in cookieHeaders) + { + AppendCookies(downloadItem.Cookies, uri, cookieHeader); + } + } + return response; + } + + private Task SendRequestAsync(HttpRequestMessage request, CancellationToken cancellationToken) + { + return Task.Run(() => + { + CancellationTokenSource combinedCancellationTokenSource = CancellationTokenSource.CreateLinkedTokenSource(cancellationToken); + Task innerTask = httpClient.SendAsync(request, HttpCompletionOption.ResponseHeadersRead, + combinedCancellationTokenSource.Token); + bool success = innerTask.Wait(TimeSpan.FromSeconds(downloadSettings.Timeout)); + if (success) + { + return innerTask.Result; + } + else + { + combinedCancellationTokenSource.Cancel(); + throw new TimeoutException(); + } + }); + } + + private string ExecuteTransformation(string pageContent, string transformationName) + { + HtmlDocument htmlDocument = new HtmlDocument(); + htmlDocument.LoadHtml(pageContent); + XslCompiledTransform xslTransform = new XslCompiledTransform(); + xslTransform.Load(Path.Combine(Environment.MirrorsDirectoryPath, transformationName + ".xslt")); + XmlWriterSettings outputSettings = xslTransform.OutputSettings.Clone(); + outputSettings.OmitXmlDeclaration = true; + outputSettings.Encoding = new UTF8Encoding(false); + using (MemoryStream memoryStream = new MemoryStream()) + using (XmlWriter xmlWriter = XmlWriter.Create(memoryStream, outputSettings)) + { + xslTransform.Transform(htmlDocument, xmlWriter); + string xmlEncodedString = Encoding.UTF8.GetString(memoryStream.ToArray()).Trim(); + return WebUtility.HtmlDecode(xmlEncodedString); + } + } + + private void AppendCookies(Dictionary existingCookies, Uri uri, string cookieHeader) + { + CookieContainer cookieContainer = new CookieContainer(); + cookieContainer.SetCookies(uri, cookieHeader); + foreach (Cookie cookie in cookieContainer.GetCookies(uri)) + { + if (!cookie.Expired) + { + existingCookies[cookie.Name] = cookie.Value; + } + } + } + + private string GenerateCookieHeader(Dictionary cookies) + { + return String.Join(";", cookies.Select(cookie => $"{cookie.Key}={cookie.Value}")); + } + + private void StartEventPublisherTask() + { + Task.Run(() => + { + while (!eventQueue.IsCompleted) + { + EventArgs eventArgs; + try + { + eventArgs = eventQueue.Take(); + } + catch (InvalidOperationException) + { + return; + } + DownloaderEvent?.Invoke(this, eventArgs); + } + }); + } + + private HttpClient CreateNewHttpClient(NetworkSettings networkSettings, DownloadSettings downloadSettings) + { + WebRequestHandler webRequestHandler = new WebRequestHandler + { + Proxy = NetworkUtils.CreateProxy(networkSettings), + UseProxy = true, + AllowAutoRedirect = false, + UseCookies = false, + ReadWriteTimeout = downloadSettings.Timeout * 1000 + }; + HttpClient result = new HttpClient(webRequestHandler); + result.Timeout = Timeout.InfiniteTimeSpan; + return result; + } + + private void SwitchToOfflineMode() + { + lock (downloadQueueLock) + { + foreach (DownloadItem downloadingItem in downloadQueue.Where(downloadItem => downloadItem.Status == DownloadItemStatus.DOWNLOADING || + downloadItem.Status == DownloadItemStatus.RETRY_DELAY)) + { + downloadingItem.CancelDownload(); + ReportStatusChange(downloadingItem, DownloadItemStatus.STOPPED, DownloadItemLogLineType.INFORMATIONAL, localization.LogLineOfflineModeIsOn); + } + } + } + + private void SaveDownloadQueue() + { + try + { + DownloadQueueStorage.SaveDownloadQueue(downloadQueueFilePath, downloadQueue); + } + catch (Exception exception) + { + Logger.Exception(exception); + } + } + + private string GenerateFileName(string directory, string fileNameTemplate) + { + string fileName = fileNameTemplate; + string filePath = Path.Combine(directory, fileName); + string fileNameWithoutExtension = null; + string fileExtension = null; + int counter = 0; + while (File.Exists(filePath) || File.Exists(filePath + ".part")) + { + if (fileNameWithoutExtension == null) + { + fileNameWithoutExtension = Path.GetFileNameWithoutExtension(fileNameTemplate); + fileExtension = Path.GetExtension(fileNameTemplate); + } + counter++; + fileName = $"{fileNameWithoutExtension} ({counter}){fileExtension}"; + filePath = Path.Combine(directory, fileName); + } + return fileName; + } + + private void AddLogLine(DownloadItem downloadItem, DownloadItemLogLineType logLineType, string logLine) + { + lock (downloadQueueLock) + { + DownloadItemLogLine downloadItemLogLine = new DownloadItemLogLine(logLineType, DateTime.Now, logLine); + int lineIndex = downloadItem.Logs.Count; + downloadItem.Logs.Add(downloadItemLogLine); + eventQueue.Add(new DownloadItemLogLineEventArgs(downloadItem.Id, lineIndex, downloadItemLogLine)); + } + } + + private void ReportChange(DownloadItem downloadItem) + { + lock (downloadQueueLock) + { + eventQueue.Add(new DownloadItemChangedEventArgs(downloadItem)); + } + } + + private void ReportStatusChange(DownloadItem downloadItem, DownloadItemStatus newStatus, DownloadItemLogLineType logLineType, string logMessage) + { + lock (downloadQueueLock) + { + AddLogLine(downloadItem, logLineType, logMessage); + downloadItem.Status = newStatus; + ReportChange(downloadItem); + } + } + + private void ReportError(DownloadItem downloadItem, string errorMessage) + { + ReportStatusChange(downloadItem, DownloadItemStatus.ERROR, DownloadItemLogLineType.ERROR, errorMessage); + } + } +} diff --git a/LibgenDesktop/Models/Entities/FictionBook.cs b/LibgenDesktop/Models/Entities/FictionBook.cs index 08fed7c..1970752 100644 --- a/LibgenDesktop/Models/Entities/FictionBook.cs +++ b/LibgenDesktop/Models/Entities/FictionBook.cs @@ -1,7 +1,6 @@ using System; using System.Runtime.CompilerServices; using System.Text; -using LibgenDesktop.Models.Utils; namespace LibgenDesktop.Models.Entities { @@ -67,13 +66,6 @@ public FictionBook() public string TitleHash { get; set; } public string Visible { get; set; } - public string YearString => Year != "0" ? Year : String.Empty; - public string PagesString => Pages != "0" ? Pages : "неизвестно"; - public string FileSizeString => Formatters.FileSizeToString(SizeInBytes, false); - public string FileSizeWithBytesString => Formatters.FileSizeToString(SizeInBytes, true); - public string AddedDateTimeString => AddedDateTime != null ? AddedDateTime.Value.ToString("dd.MM.yyyy HH:mm:ss") : "неизвестно"; - public string LastModifiedDateTimeString => LastModifiedDateTime.ToString("dd.MM.yyyy HH:mm:ss"); - public string Authors { get diff --git a/LibgenDesktop/Models/Entities/NonFictionBook.cs b/LibgenDesktop/Models/Entities/NonFictionBook.cs index 6f24a3f..c118fce 100644 --- a/LibgenDesktop/Models/Entities/NonFictionBook.cs +++ b/LibgenDesktop/Models/Entities/NonFictionBook.cs @@ -1,6 +1,4 @@ using System; -using System.Text; -using LibgenDesktop.Models.Utils; namespace LibgenDesktop.Models.Entities { @@ -53,51 +51,6 @@ internal class NonFictionBook : LibgenObject public string Tags { get; set; } public string IdentifierPlain { get; set; } - public string FileSizeString => Formatters.FileSizeToString(SizeInBytes, false); - public string FileSizeWithBytesString => Formatters.FileSizeToString(SizeInBytes, true); public bool Ocr => Searchable == "1"; - public string SearchableString => StringBooleanToLabelString(Searchable, "да", "нет", "неизвестно"); - public string AddedDateTimeString => AddedDateTime.ToString("dd.MM.yyyy HH:mm:ss"); - public string LastModifiedDateTimeString => LastModifiedDateTime.ToString("dd.MM.yyyy HH:mm:ss"); - public string BookmarkedString => StringBooleanToLabelString(Bookmarked, "есть", "нет", "неизвестно"); - public string ScannedString => StringBooleanToLabelString(Scanned, "да", "нет", "неизвестно"); - public string OrientationString => StringBooleanToLabelString(Orientation, "портретная", "альбомная", "неизвестно"); - public string PaginatedString => StringBooleanToLabelString(Paginated, "да", "нет", "неизвестно"); - public string ColorString => StringBooleanToLabelString(Color, "да", "нет", "неизвестно"); - public string CleanedString => StringBooleanToLabelString(Cleaned, "да", "нет", "неизвестно"); - - public string ContentPageCountString - { - get - { - return !String.IsNullOrWhiteSpace(Pages) ? Pages : "неизвестно"; - } - } - - public string PagesString - { - get - { - StringBuilder resultBuilder = new StringBuilder(); - resultBuilder.Append(ContentPageCountString); - resultBuilder.Append(" (содержательная часть) / "); - resultBuilder.Append(PagesInFile.ToString()); - resultBuilder.Append(" (всего в файле)"); - return resultBuilder.ToString(); - } - } - - private static string StringBooleanToLabelString(string value, string value1Label, string value0Label, string valueUnknownLabel) - { - switch (value) - { - case "0": - return value0Label; - case "1": - return value1Label; - default: - return valueUnknownLabel; - } - } } } diff --git a/LibgenDesktop/Models/Entities/SciMagArticle.cs b/LibgenDesktop/Models/Entities/SciMagArticle.cs index 8d236aa..c4bbd3b 100644 --- a/LibgenDesktop/Models/Entities/SciMagArticle.cs +++ b/LibgenDesktop/Models/Entities/SciMagArticle.cs @@ -1,5 +1,4 @@ using System; -using LibgenDesktop.Models.Utils; namespace LibgenDesktop.Models.Entities { @@ -43,10 +42,6 @@ public SciMagArticle() public string Pmc { get; set; } public string Pii { get; set; } - public string FileSizeString => Formatters.FileSizeToString(SizeInBytes, false); - public string FileSizeWithBytesString => Formatters.FileSizeToString(SizeInBytes, true); - public string AddedDateTimeString => AddedDateTime != null ? AddedDateTime.Value.ToString("dd.MM.yyyy HH:mm:ss") : "неизвестно"; - public string DoiString { get @@ -69,22 +64,5 @@ public string DoiString return doiString; } } - - public string PagesString - { - get - { - if ((!String.IsNullOrWhiteSpace(FirstPage) && FirstPage != "0") || (!String.IsNullOrWhiteSpace(LastPage) && LastPage != "0")) - { - string firstPage = FirstPage != "0" ? FirstPage.Trim() + " " : String.Empty; - string lastPage = LastPage != "0" ? " " + LastPage.Trim() : String.Empty; - return firstPage + "–" + lastPage; - } - else - { - return "неизвестно"; - } - } - } } } diff --git a/LibgenDesktop/Models/Export/CsvExporter.cs b/LibgenDesktop/Models/Export/CsvExporter.cs index 0bce538..4c06a83 100644 --- a/LibgenDesktop/Models/Export/CsvExporter.cs +++ b/LibgenDesktop/Models/Export/CsvExporter.cs @@ -1,9 +1,13 @@ -namespace LibgenDesktop.Models.Export +using LibgenDesktop.Models.Localization; + +namespace LibgenDesktop.Models.Export { internal class CsvExporter : Exporter { - public CsvExporter(string filePathTemplate, string fileExtenstion, int? rowsPerFile, bool splitIntoMultipleFiles, char separator) - : base(filePathTemplate, rowsPerFile, splitIntoMultipleFiles, fileExtenstion, filePath => new CsvExportWriter(filePath, separator)) + public CsvExporter(string filePathTemplate, string fileExtenstion, int? rowsPerFile, bool splitIntoMultipleFiles, char separator, + Language currentLanguage) + : base(filePathTemplate, rowsPerFile, splitIntoMultipleFiles, fileExtenstion, currentLanguage, + filePath => new CsvExportWriter(filePath, separator)) { } } diff --git a/LibgenDesktop/Models/Export/Exporter.cs b/LibgenDesktop/Models/Export/Exporter.cs index c285b69..fbea793 100644 --- a/LibgenDesktop/Models/Export/Exporter.cs +++ b/LibgenDesktop/Models/Export/Exporter.cs @@ -4,6 +4,7 @@ using System.Text; using System.Threading; using LibgenDesktop.Models.Entities; +using LibgenDesktop.Models.Localization; using LibgenDesktop.Models.ProgressArgs; using static LibgenDesktop.Common.Constants; @@ -15,31 +16,33 @@ internal abstract class Exporter where TExportWriter : ExportWrit private readonly int? rowsPerFile; private readonly bool splitIntoMultipleFiles; private readonly string fileExtension; + private readonly Language currentLanguage; private readonly Func exportWriterFactory; - public Exporter(string filePathTemplate, int? rowsPerFile, bool splitIntoMultipleFiles, string fileExtension, + public Exporter(string filePathTemplate, int? rowsPerFile, bool splitIntoMultipleFiles, string fileExtension, Language currentLanguage, Func exportWriterFactory) { this.filePathTemplate = filePathTemplate; this.rowsPerFile = rowsPerFile; this.splitIntoMultipleFiles = splitIntoMultipleFiles; this.fileExtension = fileExtension; + this.currentLanguage = currentLanguage; this.exportWriterFactory = exportWriterFactory; } public ExportResult ExportNonFiction(IEnumerable books, IProgress progressHandler, CancellationToken cancellationToken) { - return Export(books, exportWriter => new NonFictionExportObject(exportWriter), progressHandler, cancellationToken); + return Export(books, exportWriter => new NonFictionExportObject(exportWriter, currentLanguage), progressHandler, cancellationToken); } public ExportResult ExportFiction(IEnumerable books, IProgress progressHandler, CancellationToken cancellationToken) { - return Export(books, exportWriter => new FictionExportObject(exportWriter), progressHandler, cancellationToken); + return Export(books, exportWriter => new FictionExportObject(exportWriter, currentLanguage), progressHandler, cancellationToken); } public ExportResult ExportSciMag(IEnumerable articles, IProgress progressHandler, CancellationToken cancellationToken) { - return Export(articles, exportWriter => new SciMagExportObject(exportWriter), progressHandler, cancellationToken); + return Export(articles, exportWriter => new SciMagExportObject(exportWriter, currentLanguage), progressHandler, cancellationToken); } private ExportResult Export(IEnumerable libgenObjects, Func exportObjectFactory, diff --git a/LibgenDesktop/Models/Export/FictionExportObject.cs b/LibgenDesktop/Models/Export/FictionExportObject.cs index b768e64..dd822cf 100644 --- a/LibgenDesktop/Models/Export/FictionExportObject.cs +++ b/LibgenDesktop/Models/Export/FictionExportObject.cs @@ -1,13 +1,18 @@ using System.Collections.Generic; using LibgenDesktop.Models.Entities; +using LibgenDesktop.Models.Localization; +using LibgenDesktop.Models.Localization.Localizators; namespace LibgenDesktop.Models.Export { internal class FictionExportObject : ExportObject { - public FictionExportObject(ExportWriter exportWriter) + private FictionExporterLocalizator localization; + + public FictionExportObject(ExportWriter exportWriter, Language currentLanguage) : base(exportWriter) { + localization = currentLanguage.FictionExporter; } public override IEnumerable FieldList @@ -16,27 +21,27 @@ public override IEnumerable FieldList { return new[] { - "ID", - "Наименование", - "Авторы", - "Автор (рус.)", - "Серия", - "Издатель", - "Издание", - "Год", - "Язык", - "Формат", - "Страниц", - "Версия", - "Размер файла", - "Добавлено", - "Обновлено", - "MD5-хэш", - "Комментарий", - "Libgen ID", - "ISBN", - "Google Books ID", - "ASIN" + localization.Id, + localization.Title, + localization.Authors, + localization.RussianAuthor, + localization.Series, + localization.Publisher, + localization.Edition, + localization.Year, + localization.Language, + localization.FormatHeader, + localization.Pages, + localization.Version, + localization.FileSize, + localization.Added, + localization.LastModified, + localization.Md5Hash, + localization.Comments, + localization.LibgenId, + localization.Isbn, + localization.GoogleBookId, + localization.Asin }; } } @@ -50,10 +55,10 @@ public override void WriteObject(FictionBook book) WriteField(book.Series); WriteField(book.Publisher); WriteField(book.Edition); - WriteField(book.Year); + WriteField(localization.GetYearString(book.Year)); WriteField(book.Language); WriteField(book.Format); - WriteField(book.PagesString); + WriteField(localization.GetPagesString(book.Pages)); WriteField(book.Version); WriteField(book.SizeInBytes); WriteField(book.AddedDateTime); diff --git a/LibgenDesktop/Models/Export/NonFictionExportObject.cs b/LibgenDesktop/Models/Export/NonFictionExportObject.cs index 18d152e..bb37cd1 100644 --- a/LibgenDesktop/Models/Export/NonFictionExportObject.cs +++ b/LibgenDesktop/Models/Export/NonFictionExportObject.cs @@ -1,13 +1,18 @@ using System.Collections.Generic; using LibgenDesktop.Models.Entities; +using LibgenDesktop.Models.Localization; +using LibgenDesktop.Models.Localization.Localizators; namespace LibgenDesktop.Models.Export { internal class NonFictionExportObject : ExportObject { - public NonFictionExportObject(ExportWriter exportWriter) + private NonFictionExporterLocalizator localization; + + public NonFictionExportObject(ExportWriter exportWriter, Language currentLanguage) : base(exportWriter) { + localization = currentLanguage.NonFictionExporter; } public override IEnumerable FieldList @@ -16,47 +21,47 @@ public override IEnumerable FieldList { return new[] { - "ID", - "Наименование", - "Авторы", - "Серия", - "Издатель", - "Год", - "Язык", - "Формат", - "ISBN", - "Добавлено", - "Обновлено", - "Библиотека", - "Размер файла", - "Темы", - "Том", - "Журнал", - "Город", - "Издание", - "Страниц (содержательная часть)", - "Страниц (всего в файле)", - "Теги", - "MD5-хэш", - "Комментарий", - "Libgen ID", - "ISSN", - "UDC", - "LBC", - "LCC", - "DDC", - "DOI", - "OpenLibraryID", - "GoogleID", - "ASIN", - "DPI", - "OCR", - "Оглавление", - "Отсканирована", - "Ориентация", - "Постраничная", - "Цветная", - "Вычищенная" + localization.Id, + localization.Title, + localization.Authors, + localization.Series, + localization.Publisher, + localization.Year, + localization.Language, + localization.FormatHeader, + localization.Isbn, + localization.Added, + localization.LastModified, + localization.Library, + localization.FileSize, + localization.Topics, + localization.Volume, + localization.Magazine, + localization.City, + localization.Edition, + localization.BodyMatterPages, + localization.TotalPages, + localization.Tags, + localization.Md5Hash, + localization.Comments, + localization.LibgenId, + localization.Issn, + localization.Udc, + localization.Lbc, + localization.Lcc, + localization.Ddc, + localization.Doi, + localization.OpenLibraryId, + localization.GoogleBookId, + localization.Asin, + localization.Dpi, + localization.Ocr, + localization.TableOfContents, + localization.Scanned, + localization.Orientation, + localization.Paginated, + localization.Colored, + localization.Cleaned }; } } @@ -81,7 +86,7 @@ public override void WriteObject(NonFictionBook book) WriteField(book.Periodical); WriteField(book.City); WriteField(book.Edition); - WriteField(book.ContentPageCountString); + WriteField(localization.GetBodyMatterPageCountString(book.Pages)); WriteField(book.PagesInFile); WriteField(book.Tags); WriteField(book.Md5Hash); @@ -97,13 +102,13 @@ public override void WriteObject(NonFictionBook book) WriteField(book.GoogleBookId); WriteField(book.Asin); WriteField(book.Dpi); - WriteField(book.SearchableString); - WriteField(book.BookmarkedString); - WriteField(book.ScannedString); - WriteField(book.OrientationString); - WriteField(book.PaginatedString); - WriteField(book.ColorString); - WriteField(book.CleanedString); + WriteField(localization.GetOcrString(book.Searchable)); + WriteField(localization.GetBookmarkedString(book.Bookmarked)); + WriteField(localization.GetScannedString(book.Scanned)); + WriteField(localization.GetOrientationString(book.Orientation)); + WriteField(localization.GetPaginatedString(book.Paginated)); + WriteField(localization.GetColorString(book.Color)); + WriteField(localization.GetCleanedString(book.Cleaned)); } } } diff --git a/LibgenDesktop/Models/Export/SciMagExportObject.cs b/LibgenDesktop/Models/Export/SciMagExportObject.cs index 11dd533..674d6d3 100644 --- a/LibgenDesktop/Models/Export/SciMagExportObject.cs +++ b/LibgenDesktop/Models/Export/SciMagExportObject.cs @@ -1,13 +1,18 @@ using System.Collections.Generic; using LibgenDesktop.Models.Entities; +using LibgenDesktop.Models.Localization; +using LibgenDesktop.Models.Localization.Localizators; namespace LibgenDesktop.Models.Export { internal class SciMagExportObject : ExportObject { - public SciMagExportObject(ExportWriter exportWriter) + private SciMagExporterLocalizator localization; + + public SciMagExportObject(ExportWriter exportWriter, Language currentLanguage) : base(exportWriter) { + localization = currentLanguage.SciMagExporter; } public override IEnumerable FieldList @@ -16,36 +21,36 @@ public override IEnumerable FieldList { return new[] { - "ID", - "Наименование", - "Авторы", - "Журнал", - "Год", - "Месяц", - "День", - "Том", - "Выпуск", - "Страницы", - "Размер файла", - "Добавлено", - "MD5-хэш", - "Abstract URL", - "Libgen ID", - "DOI 1", - "DOI 2", - "ISBN", - "ID журнала", - "ISSN (p)", - "ISSN (e)", - "Pubmed ID", - "PMC", - "PII", - "Атрибут 1", - "Атрибут 2", - "Атрибут 3", - "Атрибут 4", - "Атрибут 5", - "Атрибут 6" + localization.Id, + localization.Title, + localization.Authors, + localization.Magazine, + localization.Year, + localization.Month, + localization.Day, + localization.Volume, + localization.Issue, + localization.Pages, + localization.FileSize, + localization.AddedDateTime, + localization.Md5Hash, + localization.AbstractUrl, + localization.LibgenId, + localization.Doi1, + localization.Doi2, + localization.Isbn, + localization.MagazineId, + localization.Issnp, + localization.Issne, + localization.PubmedId, + localization.Pmc, + localization.Pii, + localization.Attribute1, + localization.Attribute2, + localization.Attribute3, + localization.Attribute4, + localization.Attribute5, + localization.Attribute6 }; } } @@ -61,7 +66,7 @@ public override void WriteObject(SciMagArticle article) WriteField(article.Day); WriteField(article.Volume); WriteField(article.Issue); - WriteField(article.PagesString); + WriteField(localization.GetPagesString(article.FirstPage, article.LastPage)); WriteField(article.SizeInBytes); WriteField(article.AddedDateTime); WriteField(article.Md5Hash); diff --git a/LibgenDesktop/Models/Export/XlsxExporter.cs b/LibgenDesktop/Models/Export/XlsxExporter.cs index c58c853..335f818 100644 --- a/LibgenDesktop/Models/Export/XlsxExporter.cs +++ b/LibgenDesktop/Models/Export/XlsxExporter.cs @@ -1,9 +1,11 @@ -namespace LibgenDesktop.Models.Export +using LibgenDesktop.Models.Localization; + +namespace LibgenDesktop.Models.Export { internal class XlsxExporter : Exporter { - public XlsxExporter(string filePathTemplate, string fileExtenstion, int? rowsPerFile, bool splitIntoMultipleFiles) - : base(filePathTemplate, rowsPerFile, splitIntoMultipleFiles, fileExtenstion, filePath => new XlsxExportWriter(filePath)) + public XlsxExporter(string filePathTemplate, string fileExtenstion, int? rowsPerFile, bool splitIntoMultipleFiles, Language currentLanguage) + : base(filePathTemplate, rowsPerFile, splitIntoMultipleFiles, fileExtenstion, currentLanguage, filePath => new XlsxExportWriter(filePath)) { } } diff --git a/LibgenDesktop/Models/Localization/Language.cs b/LibgenDesktop/Models/Localization/Language.cs new file mode 100644 index 0000000..2c21226 --- /dev/null +++ b/LibgenDesktop/Models/Localization/Language.cs @@ -0,0 +1,129 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using LibgenDesktop.Models.Localization.Localizators; + +namespace LibgenDesktop.Models.Localization +{ + internal class Language + { + private readonly List translations; + private MainWindowLocalizator mainWindow; + private DatabaseWindowLocalizator databaseWindow; + private SearchTabLocalizator searchTab; + private NonFictionSearchResultsTabLocalizator nonFictionSearchResultsTab; + private FictionSearchResultsTabLocalizator fictionSearchResultsTab; + private SciMagSearchResultsTabLocalizator sciMagSearchResultsTab; + private CommonDetailsTabLocalizator commonDetailsTab; + private NonFictionDetailsTabLocalizator nonFictionDetailsTab; + private FictionDetailsTabLocalizator fictionDetailsTab; + private SciMagDetailsTabLocalizator sciMagDetailsTab; + private ImportLocalizator import; + private ExportPanelLocalizator exportPanel; + private NonFictionExporterLocalizator nonFictionExporter; + private FictionExporterLocalizator fictionExporter; + private SciMagExporterLocalizator sciMagExporter; + private SynchronizationLocalizator synchronization; + private DownloadManagerLocalizator downloadManager; + private ApplicationUpdateLocalizator applicationUpdate; + private SettingsWindowLocalizator settings; + private MessageBoxLocalizator messageBox; + private ErrorWindowLocalizator errorWindow; + + public Language(List prioritizedTranslationList) + { + translations = prioritizedTranslationList; + mainWindow = null; + databaseWindow = null; + searchTab = null; + nonFictionSearchResultsTab = null; + fictionSearchResultsTab = null; + sciMagSearchResultsTab = null; + commonDetailsTab = null; + nonFictionDetailsTab = null; + fictionDetailsTab = null; + sciMagDetailsTab = null; + import = null; + exportPanel = null; + nonFictionExporter = null; + fictionExporter = null; + sciMagExporter = null; + synchronization = null; + downloadManager = null; + applicationUpdate = null; + settings = null; + messageBox = null; + errorWindow = null; + Translation mainTranslation = prioritizedTranslationList.First(); + Name = mainTranslation.General?.Name?.Trim() ?? String.Empty; + LocalizedName = mainTranslation.General?.LocalizedName?.Trim() ?? String.Empty; + if (!String.IsNullOrEmpty(Name) && !String.IsNullOrEmpty(LocalizedName)) + { + DisplayName = $"{Name} ({LocalizedName})"; + } + else + { + DisplayName = "Error"; + } + CultureCode = mainTranslation.General?.CultureCode?.Trim() ?? String.Empty; + Formatter = new LanguageFormatter(prioritizedTranslationList); + } + + public string Name { get; } + public string LocalizedName { get; } + public string DisplayName { get; } + public string CultureCode { get; } + public LanguageFormatter Formatter { get; } + + public MainWindowLocalizator MainWindow => mainWindow ?? (mainWindow = new MainWindowLocalizator(translations, Formatter)); + + public DatabaseWindowLocalizator DatabaseWindow => databaseWindow ?? (databaseWindow = new DatabaseWindowLocalizator(translations, Formatter)); + + public SearchTabLocalizator SearchTab => searchTab ?? (searchTab = new SearchTabLocalizator(translations, Formatter)); + + public NonFictionSearchResultsTabLocalizator NonFictionSearchResultsTab => + nonFictionSearchResultsTab ?? (nonFictionSearchResultsTab = new NonFictionSearchResultsTabLocalizator(translations, Formatter)); + + public FictionSearchResultsTabLocalizator FictionSearchResultsTab => + fictionSearchResultsTab ?? (fictionSearchResultsTab = new FictionSearchResultsTabLocalizator(translations, Formatter)); + + public SciMagSearchResultsTabLocalizator SciMagSearchResultsTab => + sciMagSearchResultsTab ?? (sciMagSearchResultsTab = new SciMagSearchResultsTabLocalizator(translations, Formatter)); + + public CommonDetailsTabLocalizator CommonDetailsTab => + commonDetailsTab ?? (commonDetailsTab = new CommonDetailsTabLocalizator(translations, Formatter)); + + public NonFictionDetailsTabLocalizator NonFictionDetailsTab => + nonFictionDetailsTab ?? (nonFictionDetailsTab = new NonFictionDetailsTabLocalizator(translations, Formatter)); + + public FictionDetailsTabLocalizator FictionDetailsTab => + fictionDetailsTab ?? (fictionDetailsTab = new FictionDetailsTabLocalizator(translations, Formatter)); + + public SciMagDetailsTabLocalizator SciMagDetailsTab => + sciMagDetailsTab ?? (sciMagDetailsTab = new SciMagDetailsTabLocalizator(translations, Formatter)); + + public ImportLocalizator Import => import ?? (import = new ImportLocalizator(translations, Formatter)); + + public ExportPanelLocalizator ExportPanel => exportPanel ?? (exportPanel = new ExportPanelLocalizator(translations, Formatter)); + + public NonFictionExporterLocalizator NonFictionExporter => + nonFictionExporter ?? (nonFictionExporter = new NonFictionExporterLocalizator(translations, Formatter)); + + public FictionExporterLocalizator FictionExporter => fictionExporter ?? (fictionExporter = new FictionExporterLocalizator(translations, Formatter)); + + public SciMagExporterLocalizator SciMagExporter => sciMagExporter ?? (sciMagExporter = new SciMagExporterLocalizator(translations, Formatter)); + + public SynchronizationLocalizator Synchronization => synchronization ?? (synchronization = new SynchronizationLocalizator(translations, Formatter)); + + public DownloadManagerLocalizator DownloadManager => downloadManager ?? (downloadManager = new DownloadManagerLocalizator(translations, Formatter)); + + public ApplicationUpdateLocalizator ApplicationUpdate => + applicationUpdate ?? (applicationUpdate = new ApplicationUpdateLocalizator(translations, Formatter)); + + public SettingsWindowLocalizator Settings => settings ?? (settings = new SettingsWindowLocalizator(translations, Formatter)); + + public MessageBoxLocalizator MessageBox => messageBox ?? (messageBox = new MessageBoxLocalizator(translations, Formatter)); + + public ErrorWindowLocalizator ErrorWindow => errorWindow ?? (errorWindow = new ErrorWindowLocalizator(translations, Formatter)); + } +} diff --git a/LibgenDesktop/Models/Localization/LanguageFormatter.cs b/LibgenDesktop/Models/Localization/LanguageFormatter.cs new file mode 100644 index 0000000..b6ff62d --- /dev/null +++ b/LibgenDesktop/Models/Localization/LanguageFormatter.cs @@ -0,0 +1,101 @@ +using System; +using System.Collections.Generic; +using System.Globalization; +using System.Text; + +namespace LibgenDesktop.Models.Localization +{ + internal class LanguageFormatter + { + private readonly List prioritizedTranslationList; + private readonly NumberFormatInfo numberFormatInfo; + private readonly string dateFormat; + private readonly string timeFormat; + private readonly string dateTimeFormat; + private readonly string[] fileSizePostfixes; + + public LanguageFormatter(List prioritizedTranslationList) + { + this.prioritizedTranslationList = prioritizedTranslationList; + numberFormatInfo = (NumberFormatInfo)CultureInfo.InvariantCulture.NumberFormat.Clone(); + numberFormatInfo.NumberDecimalSeparator = GetTranslationFieldValue(translation => translation?.DecimalSeparator); + numberFormatInfo.NumberGroupSeparator = GetTranslationFieldValue(translation => translation?.ThousandsSeparator); + dateFormat = GetTranslationFieldValue(translation => translation?.DateFormat); + timeFormat = GetTranslationFieldValue(translation => translation?.TimeFormat); + dateTimeFormat = dateFormat + " " + timeFormat; + fileSizePostfixes = new[] + { + GetTranslationFieldValue(translation => translation?.FileSizePostfixes?.Byte), + GetTranslationFieldValue(translation => translation?.FileSizePostfixes?.Kilobyte), + GetTranslationFieldValue(translation => translation?.FileSizePostfixes?.Megabyte), + GetTranslationFieldValue(translation => translation?.FileSizePostfixes?.Gigabyte), + GetTranslationFieldValue(translation => translation?.FileSizePostfixes?.Terabyte) + }; + } + + public string ToFormattedString(int value) + { + return value.ToString("N0", numberFormatInfo); + } + + public string ToFormattedString(long value) + { + return value.ToString("N0", numberFormatInfo); + } + + public string ToFormattedString(decimal value) + { + return value.ToString(numberFormatInfo); + } + + public string ToFormattedDateString(DateTime dateTime) + { + return dateTime.ToString(dateFormat, CultureInfo.InvariantCulture); + } + + public string ToFormattedTimeString(DateTime dateTime) + { + return dateTime.ToString(timeFormat, CultureInfo.InvariantCulture); + } + + public string ToFormattedDateTimeString(DateTime dateTime) + { + return dateTime.ToString(dateFormat, CultureInfo.InvariantCulture); + } + + public string FileSizeToString(long fileSize, bool showBytes) + { + int postfixIndex = fileSize != 0 ? (int)Math.Floor(Math.Log(fileSize) / Math.Log(1024)) : 0; + StringBuilder resultBuilder = new StringBuilder(); + resultBuilder.Append((fileSize / Math.Pow(1024, postfixIndex)).ToString("N2", numberFormatInfo)); + resultBuilder.Append(" "); + resultBuilder.Append(fileSizePostfixes[postfixIndex]); + if (showBytes && postfixIndex != 0) + { + resultBuilder.Append(" ("); + resultBuilder.Append(fileSize.ToString("N0", numberFormatInfo)); + resultBuilder.Append(" "); + resultBuilder.Append(fileSizePostfixes[0]); + resultBuilder.Append(")"); + } + return resultBuilder.ToString(); + } + + private string GetTranslationFieldValue(Func translationField) + { + foreach (Translation translation in prioritizedTranslationList) + { + Translation.FormattingInfo formattingInfo = translation?.Formatting; + if (formattingInfo != null) + { + string result = translationField(formattingInfo); + if (result != null) + { + return result; + } + } + } + throw new Exception("Could not find the requested translation field."); + } + } +} diff --git a/LibgenDesktop/Models/Localization/LocalizationStorage.cs b/LibgenDesktop/Models/Localization/LocalizationStorage.cs index 5f27c56..106f3c9 100644 --- a/LibgenDesktop/Models/Localization/LocalizationStorage.cs +++ b/LibgenDesktop/Models/Localization/LocalizationStorage.cs @@ -1,15 +1,143 @@ -using System.Collections.Generic; +using System; +using System.Collections.Generic; +using System.Globalization; +using System.IO; +using System.Linq; +using LibgenDesktop.Common; +using LibgenDesktop.Models.Utils; +using Newtonsoft.Json; +using static LibgenDesktop.Common.Constants; namespace LibgenDesktop.Models.Localization { - internal static class LocalizationStorage + internal class LocalizationStorage { - public static Dictionary LoadLanguages() + public LocalizationStorage(string languageDirectoryPath, string selectedLanguageName) { - return new Dictionary + LoadLanguages(languageDirectoryPath, selectedLanguageName); + } + + public List Languages { get; private set; } + public Language CurrentLanguage { get; private set; } + + public event EventHandler LanguageChanged; + + public void SwitchLanguage(Language newLanguage) + { + if (newLanguage != null && newLanguage != CurrentLanguage) + { + CurrentLanguage = newLanguage; + LanguageChanged?.Invoke(this, EventArgs.Empty); + } + } + + private void LoadLanguages(string languageDirectoryPath, string selectedLanguageName) + { + Languages = new List(); + List translations = new List(); + foreach (string translationFilePath in Directory.EnumerateFiles(languageDirectoryPath, "*.lng")) + { + try + { + translations.Add(LoadTranslation(translationFilePath)); + } + catch (Exception exception) + { + Logger.Debug($"Error while trying to load language file: {translationFilePath}"); + Logger.Exception(exception); + } + } + if (!translations.Any()) + { + throw new Exception($"Cannot find language files in {languageDirectoryPath}"); + } + Translation defaultTranslation = translations.FirstOrDefault(translation => + translation.General.Name.CompareOrdinalIgnoreCase(DEFAULT_LANGUAGE_NAME)); + Language defaultLanguage; + if (defaultTranslation != null) + { + defaultLanguage = CreateLanguage(defaultTranslation, translations, null); + Languages.Add(defaultLanguage); + } + else + { + defaultLanguage = null; + } + foreach (Translation translation in translations) + { + if (translation == defaultTranslation) + { + continue; + } + Languages.Add(CreateLanguage(translation, translations, defaultTranslation)); + } + Languages = Languages.OrderBy(language => language.LocalizedName).ToList(); + Language selectedLanguage = Languages.FirstOrDefault(language => language.Name.CompareOrdinalIgnoreCase(selectedLanguageName)); + if (selectedLanguage != null) + { + CurrentLanguage = selectedLanguage; + } + else + { + string currentCultureCode = CultureInfo.CurrentUICulture.Name; + Language currentCultureLanguage = Languages.FirstOrDefault(language => language.CultureCode.CompareOrdinalIgnoreCase(currentCultureCode)); + if (currentCultureLanguage != null) + { + CurrentLanguage = currentCultureLanguage; + } + else + { + string currentLanguageCode = currentCultureCode.Substring(0, 2); + Language currentBaseCultureLanguage = Languages.FirstOrDefault(language => + language.CultureCode.StartsWith(currentLanguageCode, StringComparison.OrdinalIgnoreCase)); + if (currentBaseCultureLanguage != null) + { + CurrentLanguage = currentBaseCultureLanguage; + } + else + { + if (defaultLanguage != null) + { + CurrentLanguage = defaultLanguage; + } + else + { + CurrentLanguage = Languages.First(); + } + } + } + } + } + + private Translation LoadTranslation(string filePath) + { + JsonSerializer jsonSerializer = new JsonSerializer(); + using (StreamReader streamReader = new StreamReader(filePath)) + using (JsonTextReader jsonTextReader = new JsonTextReader(streamReader)) + { + return jsonSerializer.Deserialize(jsonTextReader); + } + } + + private Language CreateLanguage(Translation languageTranslation, List translationList, Translation defaultTranslation) + { + List prioritizedTranslationList = new List { languageTranslation }; + string translationCultureCode = languageTranslation.General?.CultureCode ?? String.Empty; + if (translationCultureCode.Length > 2) + { + string languageCode = translationCultureCode.Substring(0, 2); + Translation baseLanguageTranslation = translationList.FirstOrDefault(translation => (translation != languageTranslation) && + (translation.General?.CultureCode?.StartsWith(languageCode, StringComparison.OrdinalIgnoreCase) ?? false)); + if (baseLanguageTranslation != null) + { + prioritizedTranslationList.Add(baseLanguageTranslation); + } + } + if (defaultTranslation != null) { - { "Russian", "Russian (русский)" } - }; + prioritizedTranslationList.Add(defaultTranslation); + } + return new Language(prioritizedTranslationList); } } } diff --git a/LibgenDesktop/Models/Localization/Localizators/ApplicationUpdateLocalizator.cs b/LibgenDesktop/Models/Localization/Localizators/ApplicationUpdateLocalizator.cs new file mode 100644 index 0000000..749243b --- /dev/null +++ b/LibgenDesktop/Models/Localization/Localizators/ApplicationUpdateLocalizator.cs @@ -0,0 +1,48 @@ +using System; +using System.Collections.Generic; + +namespace LibgenDesktop.Models.Localization.Localizators +{ + internal class ApplicationUpdateLocalizator : Localizator + { + public ApplicationUpdateLocalizator(List prioritizedTranslationList, LanguageFormatter formatter) + : base(prioritizedTranslationList, formatter) + { + WindowTitle = Format(translation => translation?.WindowTitle); + UpdateAvailable = Format(translation => translation?.UpdateAvailable); + Download = Format(translation => translation?.Download); + DownloadAndInstall = Format(translation => translation?.DownloadAndInstall); + SkipThisVersion = Format(translation => translation?.SkipThisVersion); + Cancel = Format(translation => translation?.Cancel); + Interrupt = Format(translation => translation?.Interrupt); + Interrupting = Format(translation => translation?.Interrupting); + InterruptPromptTitle = Format(translation => translation?.InterruptPromptTitle); + InterruptPromptText = Format(translation => translation?.InterruptPromptText); + Error = Format(translation => translation?.Error); + IncompleteDownload = Format(translation => translation?.IncompleteDownload); + Close = Format(translation => translation?.Close); + } + + public string WindowTitle { get; } + public string UpdateAvailable { get; } + public string Download { get; } + public string DownloadAndInstall { get; } + public string SkipThisVersion { get; } + public string Cancel { get; } + public string Interrupt { get; } + public string Interrupting { get; } + public string InterruptPromptTitle { get; } + public string InterruptPromptText { get; } + public string Error { get; } + public string IncompleteDownload { get; } + public string Close { get; } + + public string GetNewVersionString(string version, DateTime date) => + Format(translation => translation?.NewVersion, new { version, date = Formatter.ToFormattedDateString(date) }); + + private string Format(Func field, object templateArguments = null) + { + return Format(translation => field(translation?.ApplicationUpdate), templateArguments); + } + } +} diff --git a/LibgenDesktop/Models/Localization/Localizators/CommonDetailsTabLocalizator.cs b/LibgenDesktop/Models/Localization/Localizators/CommonDetailsTabLocalizator.cs new file mode 100644 index 0000000..4563fcd --- /dev/null +++ b/LibgenDesktop/Models/Localization/Localizators/CommonDetailsTabLocalizator.cs @@ -0,0 +1,44 @@ +using System.Collections.Generic; + +namespace LibgenDesktop.Models.Localization.Localizators +{ + internal class CommonDetailsTabLocalizator : DetailsTabLocalizator + { + public CommonDetailsTabLocalizator(List prioritizedTranslationList, LanguageFormatter formatter) + : base(prioritizedTranslationList, formatter) + { + CoverIsLoading = Format(translation => translation?.CoverIsLoading); + NoCover = Format(translation => translation?.NoCover); + NoCoverMirror = Format(translation => translation?.NoCoverMirror); + NoCoverDueToOfflineMode = Format(translation => translation?.NoCoverDueToOfflineMode); + CoverLoadingError = Format(translation => translation?.CoverLoadingError); + Download = Format(translation => translation?.Download); + Queued = Format(translation => translation?.Queued); + Downloading = Format(translation => translation?.Downloading); + Stopped = Format(translation => translation?.Stopped); + Error = Format(translation => translation?.Error); + Open = Format(translation => translation?.Open); + ErrorMessageTitle = Format(translation => translation?.ErrorMessageTitle); + NoDownloadMirrorTooltip = Format(translation => translation?.NoDownloadMirrorTooltip); + OfflineModeIsOnTooltip = Format(translation => translation?.OfflineModeIsOnTooltip); + } + + public string CoverIsLoading { get; } + public string NoCover { get; } + public string NoCoverMirror { get; } + public string NoCoverDueToOfflineMode { get; } + public string CoverLoadingError { get; } + public string Download { get; } + public string Queued { get; } + public string Downloading { get; } + public string Stopped { get; } + public string Error { get; } + public string Open { get; } + public string ErrorMessageTitle { get; } + public string NoDownloadMirrorTooltip { get; } + public string OfflineModeIsOnTooltip { get; } + + public string GetDownloadFromMirrorText(string mirror) => Format(translation => translation?.DownloadFromMirror, new { mirror }); + public string GetFileNotFoundErrorText(string file) => Format(translation => translation?.FileNotFoundError, new { file }); + } +} diff --git a/LibgenDesktop/Models/Localization/Localizators/DatabaseWindowLocalizator.cs b/LibgenDesktop/Models/Localization/Localizators/DatabaseWindowLocalizator.cs new file mode 100644 index 0000000..2936115 --- /dev/null +++ b/LibgenDesktop/Models/Localization/Localizators/DatabaseWindowLocalizator.cs @@ -0,0 +1,49 @@ +using System; +using System.Collections.Generic; + +namespace LibgenDesktop.Models.Localization.Localizators +{ + internal class DatabaseWindowLocalizator : Localizator + { + public DatabaseWindowLocalizator(List prioritizedTranslationList, LanguageFormatter formatter) + : base(prioritizedTranslationList, formatter) + { + WindowTitle = Format(translation => translation?.WindowTitle); + FirstRunMessage = Format(translation => translation?.FirstRunMessage); + ChooseOption = Format(translation => translation?.ChooseOption); + CreateNewDatabase = Format(translation => translation?.CreateNewDatabase); + OpenExistingDatabase = Format(translation => translation?.OpenExistingDatabase); + BrowseNewDatabaseDialogTitle = Format(translation => translation?.BrowseNewDatabaseDialogTitle); + BrowseExistingDatabaseDialogTitle = Format(translation => translation?.BrowseExistingDatabaseDialogTitle); + Databases = Format(translation => translation?.Databases); + AllFiles = Format(translation => translation?.AllFiles); + Error = Format(translation => translation?.Error); + CannotCreateDatabase = Format(translation => translation?.CannotCreateDatabase); + Ok = Format(translation => translation?.Ok); + Cancel = Format(translation => translation?.Cancel); + } + + public string WindowTitle { get; } + public string FirstRunMessage { get; } + public string ChooseOption { get; } + public string CreateNewDatabase { get; } + public string OpenExistingDatabase { get; } + public string BrowseNewDatabaseDialogTitle { get; } + public string BrowseExistingDatabaseDialogTitle { get; } + public string Databases { get; } + public string AllFiles { get; } + public string Error { get; } + public string CannotCreateDatabase { get; } + public string Ok { get; } + public string Cancel { get; } + + public string GetDatabaseNotFoundText(string database) => Format(translation => translation.DatabaseNotFound, new { database }); + public string GetDatabaseCorruptedText(string database) => Format(translation => translation.DatabaseCorrupted, new { database }); + + private string Format(Func field, object templateArguments = null) + { + return Format(translation => field(translation?.DatabaseWindow), templateArguments); + } + + } +} diff --git a/LibgenDesktop/Models/Localization/Localizators/DetailsTabLocalizator.cs b/LibgenDesktop/Models/Localization/Localizators/DetailsTabLocalizator.cs new file mode 100644 index 0000000..c069748 --- /dev/null +++ b/LibgenDesktop/Models/Localization/Localizators/DetailsTabLocalizator.cs @@ -0,0 +1,49 @@ +using System; +using System.Collections.Generic; + +namespace LibgenDesktop.Models.Localization.Localizators +{ + internal abstract class DetailsTabLocalizator : Localizator + { + public DetailsTabLocalizator(List prioritizedTranslationList, LanguageFormatter formatter) + : base(prioritizedTranslationList, formatter) + { + CopyContextMenu = Format(translation => translation?.CopyContextMenu).Replace("{text}", "{0}"); + Close = Format(translation => translation?.Close); + Yes = Format(translation => translation?.Yes); + No = Format(translation => translation?.No); + Unknown = Format(translation => translation?.Unknown); + Portrait = Format(translation => translation?.Portrait); + Landscape = Format(translation => translation?.Landscape); + } + + public string CopyContextMenu { get; } + public string Close { get; } + protected string Yes { get; } + protected string No { get; } + protected string Unknown { get; } + protected string Portrait { get; } + protected string Landscape { get; } + + protected string StringBooleanToYesNoUnknownString(string value) => StringBooleanToLabelString(value, Yes, No, Unknown); + protected string StringBooleanToOrientationString(string value) => StringBooleanToLabelString(value, Portrait, Landscape, Unknown); + + protected string Format(Func field, object templateArguments = null) + { + return Format(translation => field(translation?.DetailsTabs), templateArguments); + } + + private string StringBooleanToLabelString(string value, string value1Label, string value0Label, string valueUnknownLabel) + { + switch (value) + { + case "0": + return value0Label; + case "1": + return value1Label; + default: + return valueUnknownLabel; + } + } + } +} diff --git a/LibgenDesktop/Models/Localization/Localizators/DownloadManagerLocalizator.cs b/LibgenDesktop/Models/Localization/Localizators/DownloadManagerLocalizator.cs new file mode 100644 index 0000000..2927151 --- /dev/null +++ b/LibgenDesktop/Models/Localization/Localizators/DownloadManagerLocalizator.cs @@ -0,0 +1,115 @@ +using System; +using System.Collections.Generic; + +namespace LibgenDesktop.Models.Localization.Localizators +{ + internal class DownloadManagerLocalizator : Localizator + { + public DownloadManagerLocalizator(List prioritizedTranslationList, LanguageFormatter formatter) + : base(prioritizedTranslationList, formatter) + { + TabTitle = Format(translation => translation?.TabTitle); + Start = Format(translation => translation?.Start); + Stop = Format(translation => translation?.Stop); + Remove = Format(translation => translation?.Remove); + StartAll = Format(translation => translation?.StartAll); + StopAll = Format(translation => translation?.StopAll); + RemoveCompleted = Format(translation => translation?.RemoveCompleted); + QueuedStatus = Format(translation => translation?.QueuedStatus); + DownloadingStatus = Format(translation => translation?.DownloadingStatus); + StoppedStatus = Format(translation => translation?.StoppedStatus); + RetryDelayStatus = Format(translation => translation?.RetryDelayStatus); + ErrorStatus = Format(translation => translation?.ErrorStatus); + Log = Format(translation => translation?.Log); + TechnicalDetails = Format(translation => translation?.TechnicalDetails); + Copy = Format(translation => translation?.Copy); + FileNotFoundErrorTitle = Format(translation => translation?.FileNotFoundErrorTitle); + LogLineQueued = Format(translation => translation?.LogMessages?.Queued); + LogLineStarted = Format(translation => translation?.LogMessages?.Started); + LogLineStopped = Format(translation => translation?.LogMessages?.Stopped); + LogLineCompleted = Format(translation => translation?.LogMessages?.Completed); + LogLineOfflineModeIsOn = Format(translation => translation?.LogMessages?.OfflineModeIsOn); + LogLineMaximumDownloadAttempts = Format(translation => translation?.LogMessages?.MaximumDownloadAttempts); + LogLineStartingFileDownloadUnknownFileSize = Format(translation => translation?.LogMessages?.StartingFileDownloadUnknownFileSize); + LogLineResumingFileDownloadUnknownFileSize = Format(translation => translation?.LogMessages?.ResumingFileDownloadUnknownFileSize); + LogLineRequest = Format(translation => translation?.LogMessages?.Request); + LogLineResponse = Format(translation => translation?.LogMessages?.Response); + LogLineTooManyRedirects = Format(translation => translation?.LogMessages?.TooManyRedirects); + LogLineHtmlPageReturned = Format(translation => translation?.LogMessages?.HtmlPageReturned); + LogLineNoPartialDownloadSupport = Format(translation => translation?.LogMessages?.NoPartialDownloadSupport); + LogLineNoContentLengthWarning = Format(translation => translation?.LogMessages?.NoContentLengthWarning); + LogLineServerResponseTimeout = Format(translation => translation?.LogMessages?.ServerResponseTimeout); + LogLineDownloadIncompleteError = Format(translation => translation?.LogMessages?.DownloadIncompleteError); + LogLineFileWriteError = Format(translation => translation?.LogMessages?.FileWriteError); + LogLineUnexpectedError = Format(translation => translation?.LogMessages?.UnexpectedError); + } + + public string TabTitle { get; } + public string Start { get; } + public string Stop { get; } + public string Remove { get; } + public string StartAll { get; } + public string StopAll { get; } + public string RemoveCompleted { get; } + public string QueuedStatus { get; } + public string DownloadingStatus { get; } + public string StoppedStatus { get; } + public string RetryDelayStatus { get; } + public string ErrorStatus { get; } + public string Log { get; } + public string TechnicalDetails { get; } + public string Copy { get; } + public string FileNotFoundErrorTitle { get; } + public string LogLineQueued { get; } + public string LogLineStarted { get; } + public string LogLineStopped { get; } + public string LogLineCompleted { get; } + public string LogLineOfflineModeIsOn { get; } + public string LogLineMaximumDownloadAttempts { get; } + public string LogLineStartingFileDownloadUnknownFileSize { get; } + public string LogLineResumingFileDownloadUnknownFileSize { get; } + public string LogLineRequest { get; } + public string LogLineResponse { get; } + public string LogLineTooManyRedirects { get; } + public string LogLineHtmlPageReturned { get; } + public string LogLineNoPartialDownloadSupport { get; } + public string LogLineNoContentLengthWarning { get; } + public string LogLineServerResponseTimeout { get; } + public string LogLineDownloadIncompleteError { get; } + public string LogLineFileWriteError { get; } + public string LogLineUnexpectedError { get; } + + public string GetDownloadProgressKnownFileSize(long downloaded, long total, int percent) => + Format(translation => translation?.DownloadProgressKnownFileSize, + new { downloaded = Formatter.ToFormattedString(downloaded), total = Formatter.ToFormattedString(total), percent }); + public string GetDownloadProgressUnknownFileSize(long downloaded) => + Format(translation => translation?.DownloadProgressUnknownFileSize, new { downloaded = Formatter.ToFormattedString(downloaded) }); + public string GetFileNotFoundErrorText(string file) => Format(translation => translation?.FileNotFoundErrorText, new { file }); + public string GetLogLineRetryDelay(int count) => Format(translation => translation?.LogMessages?.RetryDelay, new { count }); + public string GetLogLineTransformationError(string transformation) => + Format(translation => translation?.LogMessages?.TransformationError, new { transformation }); + public string GetLogLineTransformationReturnedIncorrectUrl(string transformation) => + Format(translation => translation?.LogMessages?.TransformationReturnedIncorrectUrl, new { transformation }); + public string GetLogLineAttempt(int current, int total) => Format(translation => translation?.LogMessages?.Attempt, new { current, total }); + public string GetLogLineDownloadingPage(string url) => Format(translation => translation?.LogMessages?.DownloadingPage, new { url }); + public string GetLogLineDownloadingFile(string url) => Format(translation => translation?.LogMessages?.DownloadingFile, new { url }); + public string GetLogLineStartingFileDownloadKnownFileSize(long size) => + Format(translation => translation?.LogMessages?.StartingFileDownloadKnownFileSize, new { size }); + public string GetLogLineResumingFileDownloadKnownFileSize(long remaining) => + Format(translation => translation?.LogMessages?.ResumingFileDownloadKnownFileSize, new { remaining }); + public string GetLogLineRedirect(string url) => Format(translation => translation?.LogMessages?.Redirect, new { url }); + public string GetLogLineNonSuccessfulStatusCode(string status) => + Format(translation => translation?.LogMessages?.NonSuccessfulStatusCode, new { status }); + public string GetLogLineCannotCreateDownloadDirectory(string directory) => + Format(translation => translation?.LogMessages?.CannotCreateDownloadDirectory, new { directory }); + public string GetLogLineCannotCreateOrOpenFile(string file) => + Format(translation => translation?.LogMessages?.CannotCreateOrOpenFile, new { file }); + public string GetLogLineCannotRenamePartFile(string source, string destination) => + Format(translation => translation?.LogMessages?.CannotRenamePartFile, new { source, destination }); + + private string Format(Func field, object templateArguments = null) + { + return Format(translation => field(translation?.DownloadManager), templateArguments); + } + } +} diff --git a/LibgenDesktop/Models/Localization/Localizators/ErrorWindowLocalizator.cs b/LibgenDesktop/Models/Localization/Localizators/ErrorWindowLocalizator.cs new file mode 100644 index 0000000..d497bc6 --- /dev/null +++ b/LibgenDesktop/Models/Localization/Localizators/ErrorWindowLocalizator.cs @@ -0,0 +1,27 @@ +using System; +using System.Collections.Generic; + +namespace LibgenDesktop.Models.Localization.Localizators +{ + internal class ErrorWindowLocalizator : Localizator + { + public ErrorWindowLocalizator(List prioritizedTranslationList, LanguageFormatter formatter) + : base(prioritizedTranslationList, formatter) + { + WindowTitle = Format(translation => translation?.WindowTitle); + UnexpectedError = Format(translation => translation?.UnexpectedError) + ":"; + Copy = Format(translation => translation?.Copy); + Close = Format(translation => translation?.Close); + } + + public string WindowTitle { get; } + public string UnexpectedError { get; } + public string Copy { get; } + public string Close { get; } + + private string Format(Func field) + { + return Format(translation => field(translation?.ErrorWindow)); + } + } +} diff --git a/LibgenDesktop/Models/Localization/Localizators/ExportPanelLocalizator.cs b/LibgenDesktop/Models/Localization/Localizators/ExportPanelLocalizator.cs new file mode 100644 index 0000000..5b3613e --- /dev/null +++ b/LibgenDesktop/Models/Localization/Localizators/ExportPanelLocalizator.cs @@ -0,0 +1,91 @@ +using System; +using System.Collections.Generic; + +namespace LibgenDesktop.Models.Localization.Localizators +{ + internal class ExportPanelLocalizator : Localizator + { + public ExportPanelLocalizator(List prioritizedTranslationList, LanguageFormatter formatter) + : base(prioritizedTranslationList, formatter) + { + Header = Format(translation => translation?.Header); + FormatLabel = Format(translation => translation?.Format) + ":"; + Excel = Format(translation => translation?.Excel); + Csv = Format(translation => translation?.Csv); + Separator = Format(translation => translation?.Separator) + ":"; + Comma = Format(translation => translation?.Comma); + Semicolon = Format(translation => translation?.Semicolon); + Tab = Format(translation => translation?.Tab); + SaveAs = Format(translation => translation?.SaveAs) + ":"; + Browse = Format(translation => translation?.Browse); + BrowseDialogTitle = Format(translation => translation?.BrowseDialogTitle); + ExcelFiles = Format(translation => translation?.ExcelFiles); + CsvFiles = Format(translation => translation?.CsvFiles); + TsvFiles = Format(translation => translation?.TsvFiles); + AllFiles = Format(translation => translation?.AllFiles); + ExportRange = Format(translation => translation?.ExportRange) + ":"; + NoLimit = Format(translation => translation?.NoLimit); + Export = Format(translation => translation?.Export); + Cancel = Format(translation => translation?.Cancel); + SavingFile = Format(translation => translation?.SavingFile); + ErrorWarningTitle = Format(translation => translation?.ErrorWarningTitle); + InvalidExportPath = Format(translation => translation?.InvalidExportPath); + InvalidExportFileName = Format(translation => translation?.InvalidExportFileName); + OverwritePromptTitle = Format(translation => translation?.OverwritePromptTitle); + RowLimitWarningTitle = Format(translation => translation?.RowLimitWarningTitle); + RowLimitWarningText = Format(translation => translation?.RowLimitWarningText); + ExportError = Format(translation => translation?.ExportError); + Interrupt = Format(translation => translation?.Interrupt); + Interrupting = Format(translation => translation?.Interrupting); + ExportInterrupted = Format(translation => translation?.ExportInterrupted); + Results = Format(translation => translation?.Results); + Close = Format(translation => translation?.Close); + } + + public string Header { get; } + public string FormatLabel { get; } + public string Excel { get; } + public string Csv { get; } + public string Separator { get; } + public string Comma { get; } + public string Semicolon { get; } + public string Tab { get; } + public string SaveAs { get; } + public string Browse { get; } + public string BrowseDialogTitle { get; } + public string ExcelFiles { get; } + public string CsvFiles { get; } + public string TsvFiles { get; } + public string AllFiles { get; } + public string ExportRange { get; } + public string NoLimit { get; } + public string Export { get; } + public string Cancel { get; } + public string SavingFile { get; } + public string ErrorWarningTitle { get; } + public string InvalidExportPath { get; } + public string InvalidExportFileName { get; } + public string OverwritePromptTitle { get; } + public string RowLimitWarningTitle { get; } + public string RowLimitWarningText { get; } + public string ExportError { get; } + public string Interrupt { get; } + public string Interrupting { get; } + public string ExportInterrupted { get; } + public string Results { get; } + public string Close { get; } + + public string GetLimitString(int count) => Format(translation => translation?.Limit, new { count = Formatter.ToFormattedString(count) }); + public string GetRowCountSingleFileString(int rows) => + Format(translation => translation?.RowCountSingleFile, new { rows = Formatter.ToFormattedString(rows) }); + public string GetRowCountMultipleFilesString(int rows, int files) => + Format(translation => translation?.RowCountMultipleFiles, new { rows = Formatter.ToFormattedString(rows), files = Formatter.ToFormattedString(files) }); + public string GetDirectoryNotFoundString(string directory) => Format(translation => translation?.DirectoryNotFound, new { directory }); + public string GetOverwritePromptTextString(string file) => Format(translation => translation?.OverwritePromptText, new { file }); + + private string Format(Func field, object templateArguments = null) + { + return Format(translation => field(translation?.ExportPanel), templateArguments); + } + } +} diff --git a/LibgenDesktop/Models/Localization/Localizators/ExporterLocalizator.cs b/LibgenDesktop/Models/Localization/Localizators/ExporterLocalizator.cs new file mode 100644 index 0000000..9f07a62 --- /dev/null +++ b/LibgenDesktop/Models/Localization/Localizators/ExporterLocalizator.cs @@ -0,0 +1,52 @@ +using System; +using System.Collections.Generic; + +namespace LibgenDesktop.Models.Localization.Localizators +{ + internal abstract class ExporterLocalizator : Localizator + { + public ExporterLocalizator(List prioritizedTranslationList, LanguageFormatter formatter) + : base(prioritizedTranslationList, formatter) + { + Yes = Format(translation => translation?.Yes); + No = Format(translation => translation?.No); + Unknown = Format(translation => translation?.Unknown); + Portrait = Format(translation => translation?.Portrait); + Landscape = Format(translation => translation?.Landscape); + } + + protected string Yes { get; } + protected string No { get; } + protected string Unknown { get; } + protected string Portrait { get; } + protected string Landscape { get; } + + protected string StringBooleanToYesNoUnknownString(string value) + { + return StringBooleanToLabelString(value, Yes, No, Unknown); + } + + protected string StringBooleanToOrientationString(string value) + { + return StringBooleanToLabelString(value, Portrait, Landscape, Unknown); + } + + protected string Format(Func field) + { + return Format(translation => field(translation?.Exporter)); + } + + private string StringBooleanToLabelString(string value, string value1Label, string value0Label, string valueUnknownLabel) + { + switch (value) + { + case "0": + return value0Label; + case "1": + return value1Label; + default: + return valueUnknownLabel; + } + } + } +} diff --git a/LibgenDesktop/Models/Localization/Localizators/FictionDetailsTabLocalizator.cs b/LibgenDesktop/Models/Localization/Localizators/FictionDetailsTabLocalizator.cs new file mode 100644 index 0000000..4c15b96 --- /dev/null +++ b/LibgenDesktop/Models/Localization/Localizators/FictionDetailsTabLocalizator.cs @@ -0,0 +1,70 @@ +using System; +using System.Collections.Generic; + +namespace LibgenDesktop.Models.Localization.Localizators +{ + internal class FictionDetailsTabLocalizator : DetailsTabLocalizator + { + public FictionDetailsTabLocalizator(List prioritizedTranslationList, LanguageFormatter formatter) + : base(prioritizedTranslationList, formatter) + { + Title = FormatHeader(translation => translation?.Title); + Authors = FormatHeader(translation => translation?.Authors); + RussianAuthor = FormatHeader(translation => translation?.RussianAuthor); + Series = FormatHeader(translation => translation?.Series); + Publisher = FormatHeader(translation => translation?.Publisher); + Edition = FormatHeader(translation => translation?.Edition); + Year = FormatHeader(translation => translation?.Year); + Language = FormatHeader(translation => translation?.Language); + FormatLabel = FormatHeader(translation => translation?.Format); + Pages = FormatHeader(translation => translation?.Pages); + Version = FormatHeader(translation => translation?.Version); + FileSize = FormatHeader(translation => translation?.FileSize); + Added = FormatHeader(translation => translation?.Added); + LastModified = FormatHeader(translation => translation?.LastModified); + Md5Hash = FormatHeader(translation => translation?.Md5Hash); + Comments = FormatHeader(translation => translation?.Comments); + Identifiers = FormatHeader(translation => translation?.Identifiers); + LibgenId = FormatHeader(translation => translation?.LibgenId); + Isbn = FormatHeader(translation => translation?.Isbn); + GoogleBookId = FormatHeader(translation => translation?.GoogleBookId); + Asin = FormatHeader(translation => translation?.Asin); + } + + public string Title { get; } + public string Authors { get; } + public string RussianAuthor { get; } + public string Series { get; } + public string Publisher { get; } + public string Edition { get; } + public string Year { get; } + public string Language { get; } + public string FormatLabel { get; } + public string Pages { get; } + public string Version { get; } + public string FileSize { get; } + public string Added { get; } + public string LastModified { get; } + public string Md5Hash { get; } + public string Comments { get; } + public string Identifiers { get; } + public string LibgenId { get; } + public string Isbn { get; } + public string GoogleBookId { get; } + public string Asin { get; } + + public string GetYearString(string value) => value != "0" ? value : String.Empty; + public string GetPagesString(string value) => value != "0" ? value : Unknown; + public string GetAddedDateTimeString(DateTime? value) => value.HasValue ? Formatter.ToFormattedDateTimeString(value.Value) : Unknown; + + private string Format(Func field) + { + return Format(translation => field(translation?.FictionDetailsTab)); + } + + private string FormatHeader(Func field) + { + return Format(field) + ":"; + } + } +} diff --git a/LibgenDesktop/Models/Localization/Localizators/FictionExporterLocalizator.cs b/LibgenDesktop/Models/Localization/Localizators/FictionExporterLocalizator.cs new file mode 100644 index 0000000..dc1fb29 --- /dev/null +++ b/LibgenDesktop/Models/Localization/Localizators/FictionExporterLocalizator.cs @@ -0,0 +1,64 @@ +using System; +using System.Collections.Generic; + +namespace LibgenDesktop.Models.Localization.Localizators +{ + internal class FictionExporterLocalizator : ExporterLocalizator + { + public FictionExporterLocalizator(List prioritizedTranslationList, LanguageFormatter formatter) + : base(prioritizedTranslationList, formatter) + { + Id = Format(translation => translation?.Id); + Title = Format(translation => translation?.Title); + Authors = Format(translation => translation?.Authors); + RussianAuthor = Format(translation => translation?.RussianAuthor); + Series = Format(translation => translation?.Series); + Publisher = Format(translation => translation?.Publisher); + Edition = Format(translation => translation?.Edition); + Year = Format(translation => translation?.Year); + Language = Format(translation => translation?.Language); + FormatHeader = Format(translation => translation?.Format); + Pages = Format(translation => translation?.Pages); + Version = Format(translation => translation?.Version); + FileSize = Format(translation => translation?.FileSize); + Added = Format(translation => translation?.Added); + LastModified = Format(translation => translation?.LastModified); + Md5Hash = Format(translation => translation?.Md5Hash); + Comments = Format(translation => translation?.Comments); + LibgenId = Format(translation => translation?.LibgenId); + Isbn = Format(translation => translation?.Isbn); + GoogleBookId = Format(translation => translation?.GoogleBookId); + Asin = Format(translation => translation?.Asin); + } + + public string Id { get; } + public string Title { get; } + public string Authors { get; } + public string RussianAuthor { get; } + public string Series { get; } + public string Publisher { get; } + public string Edition { get; } + public string Year { get; } + public string Language { get; } + public string FormatHeader { get; } + public string Pages { get; } + public string Version { get; } + public string FileSize { get; } + public string Added { get; } + public string LastModified { get; } + public string Md5Hash { get; } + public string Comments { get; } + public string LibgenId { get; } + public string Isbn { get; } + public string GoogleBookId { get; } + public string Asin { get; } + + public string GetYearString(string value) => value != "0" ? value : String.Empty; + public string GetPagesString(string value) => !String.IsNullOrWhiteSpace(value) ? value : Unknown; + + private string Format(Func field) + { + return Format(translation => field(translation?.Exporter?.FictionColumns)); + } + } +} diff --git a/LibgenDesktop/Models/Localization/Localizators/FictionSearchResultsGridColumnsLocalizator.cs b/LibgenDesktop/Models/Localization/Localizators/FictionSearchResultsGridColumnsLocalizator.cs new file mode 100644 index 0000000..348c4bf --- /dev/null +++ b/LibgenDesktop/Models/Localization/Localizators/FictionSearchResultsGridColumnsLocalizator.cs @@ -0,0 +1,34 @@ +using System; +using System.Collections.Generic; + +namespace LibgenDesktop.Models.Localization.Localizators +{ + internal class FictionSearchResultsGridColumnsLocalizator : Localizator + { + public FictionSearchResultsGridColumnsLocalizator(List prioritizedTranslationList, LanguageFormatter formatter) + : base(prioritizedTranslationList, formatter) + { + Title = Format(translation => translation?.Title); + Authors = Format(translation => translation?.Authors); + Series = Format(translation => translation?.Series); + Year = Format(translation => translation?.Year); + Publisher = Format(translation => translation?.Publisher); + FormatColumn = Format(translation => translation?.Format); + FileSize = Format(translation => translation?.FileSize); + } + + public string Title { get; } + public string Authors { get; } + public string Series { get; } + public string Year { get; } + public string Publisher { get; } + public string FormatColumn { get; } + public string FileSize { get; } + public string Ocr { get; } + + private string Format(Func field) + { + return Format(translation => field(translation?.FictionSearchResultsTab?.Columns)); + } + } +} diff --git a/LibgenDesktop/Models/Localization/Localizators/FictionSearchResultsTabLocalizator.cs b/LibgenDesktop/Models/Localization/Localizators/FictionSearchResultsTabLocalizator.cs new file mode 100644 index 0000000..651b2c5 --- /dev/null +++ b/LibgenDesktop/Models/Localization/Localizators/FictionSearchResultsTabLocalizator.cs @@ -0,0 +1,26 @@ +using System; +using System.Collections.Generic; + +namespace LibgenDesktop.Models.Localization.Localizators +{ + internal class FictionSearchResultsTabLocalizator : SearchResultsTabLocalizator + { + public FictionSearchResultsTabLocalizator(List prioritizedTranslationList, LanguageFormatter formatter) + : base(prioritizedTranslationList, formatter) + { + SearchBoxTooltip = Format(translation => translation?.SearchBoxTooltip); + Columns = new FictionSearchResultsGridColumnsLocalizator(prioritizedTranslationList, formatter); + } + + public string SearchBoxTooltip { get; } + public FictionSearchResultsGridColumnsLocalizator Columns { get; } + + public string GetSearchProgressText(int count) => Format(translation => translation?.SearchProgress, new { count = Formatter.ToFormattedString(count) }); + public string GetStatusBarText(int count) => Format(translation => translation?.StatusBar, new { count = Formatter.ToFormattedString(count) }); + + private string Format(Func field, object templateArguments = null) + { + return Format(translation => field(translation?.FictionSearchResultsTab), templateArguments); + } + } +} diff --git a/LibgenDesktop/Models/Localization/Localizators/ImportLocalizator.cs b/LibgenDesktop/Models/Localization/Localizators/ImportLocalizator.cs new file mode 100644 index 0000000..43fb5ed --- /dev/null +++ b/LibgenDesktop/Models/Localization/Localizators/ImportLocalizator.cs @@ -0,0 +1,105 @@ +using System; +using System.Collections.Generic; + +namespace LibgenDesktop.Models.Localization.Localizators +{ + internal class ImportLocalizator : Localizator + { + public ImportLocalizator(List prioritizedTranslationList, LanguageFormatter formatter) + : base(prioritizedTranslationList, formatter) + { + WindowTitle = Format(translation => translation?.WindowTitle); + BrowseImportFileDialogTitle = Format(translation => translation?.BrowseImportFileDialogTitle); + AllSupportedFiles = Format(translation => translation?.AllSupportedFiles); + SqlDumps = Format(translation => translation?.SqlDumps); + Archives = Format(translation => translation?.Archives); + AllFiles = Format(translation => translation?.AllFiles); + Interrupt = Format(translation => translation?.Interrupt); + Interrupting = Format(translation => translation?.Interrupting); + Close = Format(translation => translation?.Close); + StatusDataLookup = FormatStatus(translation => translation?.DataLookup); + StatusCreatingIndexes = FormatStatus(translation => translation?.CreatingIndexes); + StatusLoadingIds = FormatStatus(translation => translation?.LoadingIds); + StatusImportingData = FormatStatus(translation => translation?.ImportingData); + StatusImportComplete = FormatStatus(translation => translation?.ImportComplete); + StatusImportCancelled = FormatStatus(translation => translation?.ImportCancelled); + StatusDataNotFound = FormatStatus(translation => translation?.DataNotFound); + StatusImportError = FormatStatus(translation => translation?.ImportError); + LogLineDataLookup = FormatLogLine(translation => translation?.DataLookup); + LogLineScanning = FormatLogLine(translation => translation?.Scanning); + LogLineNonFictionTableFound = FormatLogLine(translation => translation?.NonFictionTableFound); + LogLineFictionTableFound = FormatLogLine(translation => translation?.FictionTableFound); + LogLineSciMagTableFound = FormatLogLine(translation => translation?.SciMagTableFound); + LogLineCreatingIndexes = FormatLogLine(translation => translation?.CreatingIndexes); + LogLineLoadingIds = FormatLogLine(translation => translation?.LoadingIds); + LogLineImportingData = FormatLogLine(translation => translation?.ImportingData); + LogLineImportSuccessful = FormatLogLine(translation => translation?.ImportSuccessful); + LogLineImportCancelled = FormatLogLine(translation => translation?.ImportCancelled); + LogLineDataNotFound = FormatLogLine(translation => translation?.DataNotFound); + LogLineImportError = FormatLogLine(translation => translation?.ImportError); + } + + public string WindowTitle { get; set; } + public string BrowseImportFileDialogTitle { get; set; } + public string AllSupportedFiles { get; set; } + public string SqlDumps { get; set; } + public string Archives { get; set; } + public string AllFiles { get; set; } + public string Interrupt { get; set; } + public string Interrupting { get; set; } + public string Close { get; set; } + public string StatusDataLookup { get; set; } + public string StatusCreatingIndexes { get; set; } + public string StatusLoadingIds { get; set; } + public string StatusImportingData { get; set; } + public string StatusImportComplete { get; set; } + public string StatusImportCancelled { get; set; } + public string StatusDataNotFound { get; set; } + public string StatusImportError { get; set; } + public string LogLineDataLookup { get; set; } + public string LogLineScanning { get; set; } + public string LogLineNonFictionTableFound { get; set; } + public string LogLineFictionTableFound { get; set; } + public string LogLineSciMagTableFound { get; set; } + public string LogLineCreatingIndexes { get; set; } + public string LogLineLoadingIds { get; set; } + public string LogLineImportingData { get; set; } + public string LogLineImportSuccessful { get; set; } + public string LogLineImportCancelled { get; set; } + public string LogLineDataNotFound { get; set; } + public string LogLineImportError { get; set; } + + public string GetElapsedString(string elapsed) => Format(translation => translation?.Elapsed, new { elapsed }); + public string GetStatusStep(int current, int total) => FormatStatus(translation => translation?.Step, new { current, total }); + public string GetLogLineStep(int step) => FormatLogLine(translation => translation?.Step, new { step }); + public string GetLogLineScannedProgress(decimal percent) => + FormatLogLine(translation => translation?.ScannedProgress, new { percent = Formatter.ToFormattedString(percent) }); + public string GetLogLineCreatingIndexForColumn(string column) => FormatLogLine(translation => translation?.CreatingIndexForColumn, new { column }); + public string GetLogLineLoadingColumnValues(string column) => FormatLogLine(translation => translation?.LoadingColumnValues, new { column }); + public string GetLogLineImportBooksProgressNoUpdate(int added) => + FormatLogLine(translation => translation?.ImportBooksProgressNoUpdate, new { added = Formatter.ToFormattedString(added) }); + public string GetLogLineImportBooksProgressWithUpdate(int added, int updated) => + FormatLogLine(translation => translation?.ImportBooksProgressWithUpdate, + new { added = Formatter.ToFormattedString(added), updated = Formatter.ToFormattedString(updated) }); + public string GetLogLineImportArticlesProgressNoUpdate(int added) => + FormatLogLine(translation => translation?.ImportArticlesProgressNoUpdate, new { added = Formatter.ToFormattedString(added) }); + public string GetLogLineImportArticlesProgressWithUpdate(int added, int updated) => + FormatLogLine(translation => translation?.ImportArticlesProgressWithUpdate, + new { added = Formatter.ToFormattedString(added), updated = Formatter.ToFormattedString(updated) }); + + private string Format(Func field, object templateArguments = null) + { + return Format(translation => field(translation?.Import), templateArguments); + } + + private string FormatStatus(Func field, object templateArguments = null) + { + return Format(translation => field(translation?.Import?.StatusMessages), templateArguments); + } + + private string FormatLogLine(Func field, object templateArguments = null) + { + return Format(translation => field(translation?.Import?.LogMessages), templateArguments); + } + } +} diff --git a/LibgenDesktop/Models/Localization/Localizators/Localizator.cs b/LibgenDesktop/Models/Localization/Localizators/Localizator.cs new file mode 100644 index 0000000..f72ce4a --- /dev/null +++ b/LibgenDesktop/Models/Localization/Localizators/Localizator.cs @@ -0,0 +1,42 @@ +using System; +using System.Collections.Generic; +using System.Reflection; + +namespace LibgenDesktop.Models.Localization.Localizators +{ + internal abstract class Localizator + { + private readonly List prioritizedTranslationList; + + public Localizator(List prioritizedTranslationList, LanguageFormatter formatter) + { + this.prioritizedTranslationList = prioritizedTranslationList; + Formatter = formatter; + } + + protected LanguageFormatter Formatter { get; } + + protected string Format(Func translationField, object templateArguments = null) + { + foreach (Translation translation in prioritizedTranslationList) + { + string result = translationField(translation)?.Replace("{new-line}", "\r\n"); + if (result != null) + { + return templateArguments == null ? result : RenderTemplate(result, templateArguments); + } + } + return "Error"; + } + + private string RenderTemplate(string template, object templateArguments) + { + string result = template; + foreach (PropertyInfo property in templateArguments.GetType().GetProperties()) + { + result = result.Replace("{" + property.Name + "}", property.GetValue(templateArguments).ToString()); + } + return result; + } + } +} diff --git a/LibgenDesktop/Models/Localization/Localizators/MainWindowLocalizator.cs b/LibgenDesktop/Models/Localization/Localizators/MainWindowLocalizator.cs new file mode 100644 index 0000000..5eb3675 --- /dev/null +++ b/LibgenDesktop/Models/Localization/Localizators/MainWindowLocalizator.cs @@ -0,0 +1,36 @@ +using System; +using System.Collections.Generic; + +namespace LibgenDesktop.Models.Localization.Localizators +{ + internal class MainWindowLocalizator : Localizator + { + public MainWindowLocalizator(List prioritizedTranslationList, LanguageFormatter formatter) + : base(prioritizedTranslationList, formatter) + { + WindowTitle = Format(translation => translation?.WindowTitle); + ToolbarDownloadManagerTooltip = Format(translation => translation?.DownloadManagerTooltip); + ToolbarUpdate = Format(translation => translation?.Update); + ToolbarImport = Format(translation => translation?.Import); + ToolbarSynchronize = Format(translation => translation?.Synchronize); + ToolbarSettings = Format(translation => translation?.Settings); + } + + public string WindowTitle { get; } + public string ToolbarDownloadManagerTooltip { get; } + public string ToolbarUpdate { get; } + public string ToolbarImport { get; } + public string ToolbarSynchronize { get; } + public string ToolbarSettings { get; } + + private string Format(Func field) + { + return Format(translation => field(translation?.MainWindow)); + } + + private string Format(Func field) + { + return Format(translation => field(translation?.MainWindow?.MainMenu)); + } + } +} diff --git a/LibgenDesktop/Models/Localization/Localizators/MessageBoxLocalizator.cs b/LibgenDesktop/Models/Localization/Localizators/MessageBoxLocalizator.cs new file mode 100644 index 0000000..844d950 --- /dev/null +++ b/LibgenDesktop/Models/Localization/Localizators/MessageBoxLocalizator.cs @@ -0,0 +1,25 @@ +using System; +using System.Collections.Generic; + +namespace LibgenDesktop.Models.Localization.Localizators +{ + internal class MessageBoxLocalizator : Localizator + { + public MessageBoxLocalizator(List prioritizedTranslationList, LanguageFormatter formatter) + : base(prioritizedTranslationList, formatter) + { + Ok = Format(translation => translation?.Ok); + Yes = Format(translation => translation?.Yes); + No = Format(translation => translation?.No); + } + + public string Ok { get; } + public string Yes { get; } + public string No { get; } + + private string Format(Func field) + { + return Format(translation => field(translation?.MessageBox)); + } + } +} diff --git a/LibgenDesktop/Models/Localization/Localizators/NonFictionDetailsTabLocalizator.cs b/LibgenDesktop/Models/Localization/Localizators/NonFictionDetailsTabLocalizator.cs new file mode 100644 index 0000000..bc3e492 --- /dev/null +++ b/LibgenDesktop/Models/Localization/Localizators/NonFictionDetailsTabLocalizator.cs @@ -0,0 +1,136 @@ +using System; +using System.Collections.Generic; +using System.Text; + +namespace LibgenDesktop.Models.Localization.Localizators +{ + internal class NonFictionDetailsTabLocalizator : DetailsTabLocalizator + { + public NonFictionDetailsTabLocalizator(List prioritizedTranslationList, LanguageFormatter formatter) + : base(prioritizedTranslationList, formatter) + { + Title = FormatHeader(translation => translation?.Title); + Authors = FormatHeader(translation => translation?.Authors); + Series = FormatHeader(translation => translation?.Series); + Publisher = FormatHeader(translation => translation?.Publisher); + Year = FormatHeader(translation => translation?.Year); + Language = FormatHeader(translation => translation?.Language); + FormatLabel = FormatHeader(translation => translation?.Format); + Isbn = FormatHeader(translation => translation?.Isbn); + Added = FormatHeader(translation => translation?.Added); + LastModified = FormatHeader(translation => translation?.LastModified); + Library = FormatHeader(translation => translation?.Library); + FileSize = FormatHeader(translation => translation?.FileSize); + Topics = FormatHeader(translation => translation?.Topics); + Volume = FormatHeader(translation => translation?.Volume); + Magazine = FormatHeader(translation => translation?.Magazine); + City = FormatHeader(translation => translation?.City); + Edition = FormatHeader(translation => translation?.Edition); + Pages = FormatHeader(translation => translation?.Pages); + Tags = FormatHeader(translation => translation?.Tags); + Md5Hash = FormatHeader(translation => translation?.Md5Hash); + Comments = FormatHeader(translation => translation?.Comments); + Identifiers = FormatHeader(translation => translation?.Identifiers); + LibgenId = FormatHeader(translation => translation?.LibgenId); + Issn = FormatHeader(translation => translation?.Issn); + Udc = FormatHeader(translation => translation?.Udc); + Lbc = FormatHeader(translation => translation?.Lbc); + Lcc = FormatHeader(translation => translation?.Lcc); + Ddc = FormatHeader(translation => translation?.Ddc); + Doi = FormatHeader(translation => translation?.Doi); + OpenLibraryId = FormatHeader(translation => translation?.OpenLibraryId); + GoogleBookId = FormatHeader(translation => translation?.GoogleBookId); + Asin = FormatHeader(translation => translation?.Asin); + AdditionalAttributes = FormatHeader(translation => translation?.AdditionalAttributes); + Dpi = FormatHeader(translation => translation?.Dpi); + Ocr = FormatHeader(translation => translation?.Ocr); + TableOfContents = FormatHeader(translation => translation?.TableOfContents); + Scanned = FormatHeader(translation => translation?.Scanned); + Orientation = FormatHeader(translation => translation?.Orientation); + Paginated = FormatHeader(translation => translation?.Paginated); + Colored = FormatHeader(translation => translation?.Colored); + Cleaned = FormatHeader(translation => translation?.Cleaned); + } + + public string Title { get; } + public string Authors { get; } + public string Series { get; } + public string Publisher { get; } + public string Year { get; } + public string Language { get; } + public string FormatLabel { get; } + public string Isbn { get; } + public string Added { get; } + public string LastModified { get; } + public string Library { get; } + public string FileSize { get; } + public string Topics { get; } + public string Volume { get; } + public string Magazine { get; } + public string City { get; } + public string Edition { get; } + public string Pages { get; } + public string Tags { get; } + public string Md5Hash { get; } + public string Comments { get; } + public string Identifiers { get; } + public string LibgenId { get; } + public string Issn { get; } + public string Udc { get; } + public string Lbc { get; } + public string Lcc { get; } + public string Ddc { get; } + public string Doi { get; } + public string OpenLibraryId { get; } + public string GoogleBookId { get; } + public string Asin { get; } + public string AdditionalAttributes { get; } + public string Dpi { get; } + public string Ocr { get; } + public string TableOfContents { get; } + public string Scanned { get; } + public string Orientation { get; } + public string Paginated { get; } + public string Colored { get; } + public string Cleaned { get; } + + public string GetPagesText(string bodyMatterPages, int totalPages) + { + StringBuilder resultBuilder = new StringBuilder(); + if (!String.IsNullOrWhiteSpace(bodyMatterPages)) + { + resultBuilder.Append(bodyMatterPages); + } + else + { + resultBuilder.Append(Unknown); + } + resultBuilder.Append(" ("); + resultBuilder.Append(Format(translation => translation?.BodyMatterPages)); + resultBuilder.Append(") / "); + resultBuilder.Append(totalPages.ToString()); + resultBuilder.Append(" ("); + resultBuilder.Append(Format(translation => translation?.TotalPages)); + resultBuilder.Append(")"); + return resultBuilder.ToString(); + } + + public string GetOcrString(string value) => StringBooleanToYesNoUnknownString(value); + public string GetBookmarkedString(string value) => StringBooleanToYesNoUnknownString(value); + public string GetScannedString(string value) => StringBooleanToYesNoUnknownString(value); + public string GetOrientationString(string value) => StringBooleanToOrientationString(value); + public string GetPaginatedString(string value) => StringBooleanToYesNoUnknownString(value); + public string GetColorString(string value) => StringBooleanToYesNoUnknownString(value); + public string GetCleanedString(string value) => StringBooleanToYesNoUnknownString(value); + + private string Format(Func field) + { + return Format(translation => field(translation?.NonFictionDetailsTab)); + } + + private string FormatHeader(Func field) + { + return Format(field) + ":"; + } + } +} diff --git a/LibgenDesktop/Models/Localization/Localizators/NonFictionExporterLocalizator.cs b/LibgenDesktop/Models/Localization/Localizators/NonFictionExporterLocalizator.cs new file mode 100644 index 0000000..aaaa7fd --- /dev/null +++ b/LibgenDesktop/Models/Localization/Localizators/NonFictionExporterLocalizator.cs @@ -0,0 +1,110 @@ +using System; +using System.Collections.Generic; + +namespace LibgenDesktop.Models.Localization.Localizators +{ + internal class NonFictionExporterLocalizator : ExporterLocalizator + { + public NonFictionExporterLocalizator(List prioritizedTranslationList, LanguageFormatter formatter) + : base(prioritizedTranslationList, formatter) + { + Id = Format(translation => translation?.Id); + Title = Format(translation => translation?.Title); + Authors = Format(translation => translation?.Authors); + Series = Format(translation => translation?.Series); + Publisher = Format(translation => translation?.Publisher); + Year = Format(translation => translation?.Year); + Language = Format(translation => translation?.Language); + FormatHeader = Format(translation => translation?.Format); + Isbn = Format(translation => translation?.Isbn); + Added = Format(translation => translation?.Added); + LastModified = Format(translation => translation?.LastModified); + Library = Format(translation => translation?.Library); + FileSize = Format(translation => translation?.FileSize); + Topics = Format(translation => translation?.Topics); + Volume = Format(translation => translation?.Volume); + Magazine = Format(translation => translation?.Magazine); + City = Format(translation => translation?.City); + Edition = Format(translation => translation?.Edition); + BodyMatterPages = Format(translation => translation?.BodyMatterPages); + TotalPages = Format(translation => translation?.TotalPages); + Tags = Format(translation => translation?.Tags); + Md5Hash = Format(translation => translation?.Md5Hash); + Comments = Format(translation => translation?.Comments); + LibgenId = Format(translation => translation?.LibgenId); + Issn = Format(translation => translation?.Issn); + Udc = Format(translation => translation?.Udc); + Lbc = Format(translation => translation?.Lbc); + Lcc = Format(translation => translation?.Lcc); + Ddc = Format(translation => translation?.Ddc); + Doi = Format(translation => translation?.Doi); + OpenLibraryId = Format(translation => translation?.OpenLibraryId); + GoogleBookId = Format(translation => translation?.GoogleBookId); + Asin = Format(translation => translation?.Asin); + Dpi = Format(translation => translation?.Dpi); + Ocr = Format(translation => translation?.Ocr); + TableOfContents = Format(translation => translation?.TableOfContents); + Scanned = Format(translation => translation?.Scanned); + Orientation = Format(translation => translation?.Orientation); + Paginated = Format(translation => translation?.Paginated); + Colored = Format(translation => translation?.Colored); + Cleaned = Format(translation => translation?.Cleaned); + } + + public string Id { get; } + public string Title { get; } + public string Authors { get; } + public string Series { get; } + public string Publisher { get; } + public string Year { get; } + public string Language { get; } + public string FormatHeader { get; } + public string Isbn { get; } + public string Added { get; } + public string LastModified { get; } + public string Library { get; } + public string FileSize { get; } + public string Topics { get; } + public string Volume { get; } + public string Magazine { get; } + public string City { get; } + public string Edition { get; } + public string BodyMatterPages { get; } + public string TotalPages { get; } + public string Tags { get; } + public string Md5Hash { get; } + public string Comments { get; } + public string LibgenId { get; } + public string Issn { get; } + public string Udc { get; } + public string Lbc { get; } + public string Lcc { get; } + public string Ddc { get; } + public string Doi { get; } + public string OpenLibraryId { get; } + public string GoogleBookId { get; } + public string Asin { get; } + public string Dpi { get; } + public string Ocr { get; } + public string TableOfContents { get; } + public string Scanned { get; } + public string Orientation { get; } + public string Paginated { get; } + public string Colored { get; } + public string Cleaned { get; } + + public string GetBodyMatterPageCountString(string value) => !String.IsNullOrWhiteSpace(value) ? value : Unknown; + public string GetOcrString(string value) => StringBooleanToYesNoUnknownString(value); + public string GetBookmarkedString(string value) => StringBooleanToYesNoUnknownString(value); + public string GetScannedString(string value) => StringBooleanToYesNoUnknownString(value); + public string GetOrientationString(string value) => StringBooleanToOrientationString(value); + public string GetPaginatedString(string value) => StringBooleanToYesNoUnknownString(value); + public string GetColorString(string value) => StringBooleanToYesNoUnknownString(value); + public string GetCleanedString(string value) => StringBooleanToYesNoUnknownString(value); + + private string Format(Func field) + { + return Format(translation => field(translation?.Exporter?.NonFictionColumns)); + } + } +} diff --git a/LibgenDesktop/Models/Localization/Localizators/NonFictionSearchResultsGridColumnsLocalizator.cs b/LibgenDesktop/Models/Localization/Localizators/NonFictionSearchResultsGridColumnsLocalizator.cs new file mode 100644 index 0000000..0349762 --- /dev/null +++ b/LibgenDesktop/Models/Localization/Localizators/NonFictionSearchResultsGridColumnsLocalizator.cs @@ -0,0 +1,35 @@ +using System; +using System.Collections.Generic; + +namespace LibgenDesktop.Models.Localization.Localizators +{ + internal class NonFictionSearchResultsGridColumnsLocalizator : Localizator + { + public NonFictionSearchResultsGridColumnsLocalizator(List prioritizedTranslationList, LanguageFormatter formatter) + : base(prioritizedTranslationList, formatter) + { + Title = Format(translation => translation?.Title); + Authors = Format(translation => translation?.Authors); + Series = Format(translation => translation?.Series); + Year = Format(translation => translation?.Year); + Publisher = Format(translation => translation?.Publisher); + FormatColumn = Format(translation => translation?.Format); + FileSize = Format(translation => translation?.FileSize); + Ocr = Format(translation => translation?.Ocr); + } + + public string Title { get; } + public string Authors { get; } + public string Series { get; } + public string Year { get; } + public string Publisher { get; } + public string FormatColumn { get; } + public string FileSize { get; } + public string Ocr { get; } + + private string Format(Func field) + { + return Format(translation => field(translation?.NonFictionSearchResultsTab?.Columns)); + } + } +} diff --git a/LibgenDesktop/Models/Localization/Localizators/NonFictionSearchResultsTabLocalizator.cs b/LibgenDesktop/Models/Localization/Localizators/NonFictionSearchResultsTabLocalizator.cs new file mode 100644 index 0000000..a3bff19 --- /dev/null +++ b/LibgenDesktop/Models/Localization/Localizators/NonFictionSearchResultsTabLocalizator.cs @@ -0,0 +1,26 @@ +using System; +using System.Collections.Generic; + +namespace LibgenDesktop.Models.Localization.Localizators +{ + internal class NonFictionSearchResultsTabLocalizator : SearchResultsTabLocalizator + { + public NonFictionSearchResultsTabLocalizator(List prioritizedTranslationList, LanguageFormatter formatter) + : base(prioritizedTranslationList, formatter) + { + SearchBoxTooltip = Format(translation => translation?.SearchBoxTooltip); + Columns = new NonFictionSearchResultsGridColumnsLocalizator(prioritizedTranslationList, formatter); + } + + public string SearchBoxTooltip { get; } + public NonFictionSearchResultsGridColumnsLocalizator Columns { get; } + + public string GetSearchProgressText(int count) => Format(translation => translation?.SearchProgress, new { count = Formatter.ToFormattedString(count) }); + public string GetStatusBarText(int count) => Format(translation => translation?.StatusBar, new { count = Formatter.ToFormattedString(count) }); + + private string Format(Func field, object templateArguments = null) + { + return Format(translation => field(translation?.NonFictionSearchResultsTab), templateArguments); + } + } +} diff --git a/LibgenDesktop/Models/Localization/Localizators/SciMagDetailsTabLocalizator.cs b/LibgenDesktop/Models/Localization/Localizators/SciMagDetailsTabLocalizator.cs new file mode 100644 index 0000000..ba0c346 --- /dev/null +++ b/LibgenDesktop/Models/Localization/Localizators/SciMagDetailsTabLocalizator.cs @@ -0,0 +1,100 @@ +using System; +using System.Collections.Generic; + +namespace LibgenDesktop.Models.Localization.Localizators +{ + internal class SciMagDetailsTabLocalizator : DetailsTabLocalizator + { + public SciMagDetailsTabLocalizator(List prioritizedTranslationList, LanguageFormatter formatter) + : base(prioritizedTranslationList, formatter) + { + Title = FormatHeader(translation => translation?.Title); + Authors = FormatHeader(translation => translation?.Authors); + Magazine = FormatHeader(translation => translation?.Magazine); + Year = FormatHeader(translation => translation?.Year); + Month = FormatHeader(translation => translation?.Month); + Day = FormatHeader(translation => translation?.Day); + Volume = FormatHeader(translation => translation?.Volume); + Issue = FormatHeader(translation => translation?.Issue); + Pages = FormatHeader(translation => translation?.Pages); + FileSize = FormatHeader(translation => translation?.FileSize); + AddedDateTime = FormatHeader(translation => translation?.AddedDateTime); + Md5Hash = FormatHeader(translation => translation?.Md5Hash); + AbstractUrl = FormatHeader(translation => translation?.AbstractUrl); + Identifiers = FormatHeader(translation => translation?.Identifiers); + LibgenId = FormatHeader(translation => translation?.LibgenId); + Doi = FormatHeader(translation => translation?.Doi); + Isbn = FormatHeader(translation => translation?.Isbn); + MagazineId = FormatHeader(translation => translation?.MagazineId); + Issnp = FormatHeader(translation => translation?.Issnp); + Issne = FormatHeader(translation => translation?.Issne); + PubmedId = FormatHeader(translation => translation?.PubmedId); + Pmc = FormatHeader(translation => translation?.Pmc); + Pii = FormatHeader(translation => translation?.Pii); + AdditionalAttributes = FormatHeader(translation => translation?.AdditionalAttributes); + Attribute1 = FormatHeader(translation => translation?.Attribute1); + Attribute2 = FormatHeader(translation => translation?.Attribute2); + Attribute3 = FormatHeader(translation => translation?.Attribute3); + Attribute4 = FormatHeader(translation => translation?.Attribute4); + Attribute5 = FormatHeader(translation => translation?.Attribute5); + Attribute6 = FormatHeader(translation => translation?.Attribute6); + } + + public string Title { get; } + public string Authors { get; } + public string Magazine { get; } + public string Year { get; } + public string Month { get; } + public string Day { get; } + public string Volume { get; } + public string Issue { get; } + public string Pages { get; } + public string FileSize { get; } + public string AddedDateTime { get; } + public string Md5Hash { get; } + public string AbstractUrl { get; } + public string Identifiers { get; } + public string LibgenId { get; } + public string Doi { get; } + public string Isbn { get; } + public string MagazineId { get; } + public string Issnp { get; } + public string Issne { get; } + public string PubmedId { get; } + public string Pmc { get; } + public string Pii { get; } + public string AdditionalAttributes { get; } + public string Attribute1 { get; } + public string Attribute2 { get; } + public string Attribute3 { get; } + public string Attribute4 { get; } + public string Attribute5 { get; } + public string Attribute6 { get; } + + public string GetPagesString(string firstPage, string lastPage) + { + if ((!String.IsNullOrWhiteSpace(firstPage) && firstPage != "0") || (!String.IsNullOrWhiteSpace(lastPage) && lastPage != "0")) + { + firstPage = firstPage != "0" ? firstPage.Trim() + " " : String.Empty; + lastPage = lastPage != "0" ? " " + lastPage.Trim() : String.Empty; + return firstPage + "–" + lastPage; + } + else + { + return Unknown; + } + } + + public string GetAddedDateTimeString(DateTime? value) => value.HasValue ? Formatter.ToFormattedDateTimeString(value.Value) : Unknown; + + private string Format(Func field) + { + return Format(translation => field(translation?.SciMagDetailsTab)); + } + + private string FormatHeader(Func field) + { + return Format(field) + ":"; + } + } +} diff --git a/LibgenDesktop/Models/Localization/Localizators/SciMagExporterLocalizator.cs b/LibgenDesktop/Models/Localization/Localizators/SciMagExporterLocalizator.cs new file mode 100644 index 0000000..d7667dc --- /dev/null +++ b/LibgenDesktop/Models/Localization/Localizators/SciMagExporterLocalizator.cs @@ -0,0 +1,93 @@ +using System; +using System.Collections.Generic; + +namespace LibgenDesktop.Models.Localization.Localizators +{ + internal class SciMagExporterLocalizator : ExporterLocalizator + { + public SciMagExporterLocalizator(List prioritizedTranslationList, LanguageFormatter formatter) + : base(prioritizedTranslationList, formatter) + { + Id = Format(translation => translation?.Id); + Title = Format(translation => translation?.Title); + Authors = Format(translation => translation?.Authors); + Magazine = Format(translation => translation?.Magazine); + Year = Format(translation => translation?.Year); + Month = Format(translation => translation?.Month); + Day = Format(translation => translation?.Day); + Volume = Format(translation => translation?.Volume); + Issue = Format(translation => translation?.Issue); + Pages = Format(translation => translation?.Pages); + FileSize = Format(translation => translation?.FileSize); + AddedDateTime = Format(translation => translation?.AddedDateTime); + Md5Hash = Format(translation => translation?.Md5Hash); + AbstractUrl = Format(translation => translation?.AbstractUrl); + LibgenId = Format(translation => translation?.LibgenId); + Doi1 = Format(translation => translation?.Doi1); + Doi2 = Format(translation => translation?.Doi2); + Isbn = Format(translation => translation?.Isbn); + MagazineId = Format(translation => translation?.MagazineId); + Issnp = Format(translation => translation?.Issnp); + Issne = Format(translation => translation?.Issne); + PubmedId = Format(translation => translation?.PubmedId); + Pmc = Format(translation => translation?.Pmc); + Pii = Format(translation => translation?.Pii); + Attribute1 = Format(translation => translation?.Attribute1); + Attribute2 = Format(translation => translation?.Attribute2); + Attribute3 = Format(translation => translation?.Attribute3); + Attribute4 = Format(translation => translation?.Attribute4); + Attribute5 = Format(translation => translation?.Attribute5); + Attribute6 = Format(translation => translation?.Attribute6); + } + + public string Id { get; } + public string Title { get; } + public string Authors { get; } + public string Magazine { get; } + public string Year { get; } + public string Month { get; } + public string Day { get; } + public string Volume { get; } + public string Issue { get; } + public string Pages { get; } + public string FileSize { get; } + public string AddedDateTime { get; } + public string Md5Hash { get; } + public string AbstractUrl { get; } + public string LibgenId { get; } + public string Doi1 { get; } + public string Doi2 { get; } + public string Isbn { get; } + public string MagazineId { get; } + public string Issnp { get; } + public string Issne { get; } + public string PubmedId { get; } + public string Pmc { get; } + public string Pii { get; } + public string Attribute1 { get; } + public string Attribute2 { get; } + public string Attribute3 { get; } + public string Attribute4 { get; } + public string Attribute5 { get; } + public string Attribute6 { get; } + + public string GetPagesString(string firstPage, string lastPage) + { + if ((!String.IsNullOrWhiteSpace(firstPage) && firstPage != "0") || (!String.IsNullOrWhiteSpace(lastPage) && lastPage != "0")) + { + firstPage = firstPage != "0" ? firstPage.Trim() + " " : String.Empty; + lastPage = lastPage != "0" ? " " + lastPage.Trim() : String.Empty; + return firstPage + "–" + lastPage; + } + else + { + return Unknown; + } + } + + private string Format(Func field) + { + return Format(translation => field(translation?.Exporter?.SciMagColumns)); + } + } +} diff --git a/LibgenDesktop/Models/Localization/Localizators/SciMagSearchResultsGridColumnsLocalizator.cs b/LibgenDesktop/Models/Localization/Localizators/SciMagSearchResultsGridColumnsLocalizator.cs new file mode 100644 index 0000000..1e157b3 --- /dev/null +++ b/LibgenDesktop/Models/Localization/Localizators/SciMagSearchResultsGridColumnsLocalizator.cs @@ -0,0 +1,31 @@ +using System; +using System.Collections.Generic; + +namespace LibgenDesktop.Models.Localization.Localizators +{ + internal class SciMagSearchResultsGridColumnsLocalizator : Localizator + { + public SciMagSearchResultsGridColumnsLocalizator(List prioritizedTranslationList, LanguageFormatter formatter) + : base(prioritizedTranslationList, formatter) + { + Title = Format(translation => translation?.Title); + Authors = Format(translation => translation?.Authors); + Magazine = Format(translation => translation?.Magazine); + Year = Format(translation => translation?.Year); + FileSize = Format(translation => translation?.FileSize); + Doi = Format(translation => translation?.Doi); + } + + public string Title { get; } + public string Authors { get; } + public string Magazine { get; } + public string Year { get; } + public string FileSize { get; } + public string Doi { get; } + + private string Format(Func field) + { + return Format(translation => field(translation?.SciMagSearchResultsTab?.Columns)); + } + } +} diff --git a/LibgenDesktop/Models/Localization/Localizators/SciMagSearchResultsTabLocalizator.cs b/LibgenDesktop/Models/Localization/Localizators/SciMagSearchResultsTabLocalizator.cs new file mode 100644 index 0000000..5246a10 --- /dev/null +++ b/LibgenDesktop/Models/Localization/Localizators/SciMagSearchResultsTabLocalizator.cs @@ -0,0 +1,26 @@ +using System; +using System.Collections.Generic; + +namespace LibgenDesktop.Models.Localization.Localizators +{ + internal class SciMagSearchResultsTabLocalizator : SearchResultsTabLocalizator + { + public SciMagSearchResultsTabLocalizator(List prioritizedTranslationList, LanguageFormatter formatter) + : base(prioritizedTranslationList, formatter) + { + SearchBoxTooltip = Format(translation => translation?.SearchBoxTooltip); + Columns = new SciMagSearchResultsGridColumnsLocalizator(prioritizedTranslationList, formatter); + } + + public string SearchBoxTooltip { get; } + public SciMagSearchResultsGridColumnsLocalizator Columns { get; } + + public string GetSearchProgressText(int count) => Format(translation => translation?.SearchProgress, new { count = Formatter.ToFormattedString(count) }); + public string GetStatusBarText(int count) => Format(translation => translation?.StatusBar, new { count = Formatter.ToFormattedString(count) }); + + private string Format(Func field, object templateArguments = null) + { + return Format(translation => field(translation?.SciMagSearchResultsTab), templateArguments); + } + } +} diff --git a/LibgenDesktop/Models/Localization/Localizators/SearchResultsTabLocalizator.cs b/LibgenDesktop/Models/Localization/Localizators/SearchResultsTabLocalizator.cs new file mode 100644 index 0000000..b450fce --- /dev/null +++ b/LibgenDesktop/Models/Localization/Localizators/SearchResultsTabLocalizator.cs @@ -0,0 +1,25 @@ +using System; +using System.Collections.Generic; + +namespace LibgenDesktop.Models.Localization.Localizators +{ + internal abstract class SearchResultsTabLocalizator : Localizator + { + public SearchResultsTabLocalizator(List prioritizedTranslationList, LanguageFormatter formatter) + : base(prioritizedTranslationList, formatter) + { + SearchPlaceHolder = Format(translation => translation?.SearchPlaceHolder); + SearchInProgress = Format(translation => translation?.SearchInProgress); + ExportButtonTooltip = Format(translation => translation?.ExportButtonTooltip); + } + + public string SearchPlaceHolder { get; } + public string SearchInProgress { get; } + public string ExportButtonTooltip { get; } + + private string Format(Func field, object templateArguments = null) + { + return Format(translation => field(translation?.SearchResultsTabs), templateArguments); + } + } +} diff --git a/LibgenDesktop/Models/Localization/Localizators/SearchTabLocalizator.cs b/LibgenDesktop/Models/Localization/Localizators/SearchTabLocalizator.cs new file mode 100644 index 0000000..52d5edf --- /dev/null +++ b/LibgenDesktop/Models/Localization/Localizators/SearchTabLocalizator.cs @@ -0,0 +1,49 @@ +using System; +using System.Collections.Generic; + +namespace LibgenDesktop.Models.Localization.Localizators +{ + internal class SearchTabLocalizator : Localizator + { + public SearchTabLocalizator(List prioritizedTranslationList, LanguageFormatter formatter) + : base(prioritizedTranslationList, formatter) + { + TabTitle = Format(translation => translation?.TabTitle); + NonFictionSelector = Format(translation => translation?.NonFictionSelector); + SearchPlaceHolder = Format(translation => translation?.SearchPlaceHolder); + NonFictionSelector = Format(translation => translation?.NonFictionSelector); + FictionSelector = Format(translation => translation?.FictionSelector); + SciMagSelector = Format(translation => translation?.SciMagSelector); + NonFictionSearchBoxTooltip = Format(translation => translation?.NonFictionSearchBoxTooltip); + FictionSearchBoxTooltip = Format(translation => translation?.FictionSearchBoxTooltip); + SciMagSearchBoxTooltip = Format(translation => translation?.SciMagSearchBoxTooltip); + SearchInProgress = Format(translation => translation?.SearchInProgress); + DatabaseIsEmpty = Format(translation => translation?.DatabaseIsEmpty); + ImportButton = Format(translation => translation?.ImportButton); + } + + public string TabTitle { get; } + public string SearchPlaceHolder { get; } + public string NonFictionSelector { get; } + public string FictionSelector { get; } + public string SciMagSelector { get; } + public string NonFictionSearchBoxTooltip { get; } + public string FictionSearchBoxTooltip { get; } + public string SciMagSearchBoxTooltip { get; } + public string SearchInProgress { get; } + public string DatabaseIsEmpty { get; } + public string ImportButton { get; } + + public string GetNonFictionSearchProgressText(int count) => + Format(translation => translation?.NonFictionSearchProgress, new { count = Formatter.ToFormattedString(count) }); + public string GetFictionSearchProgressText(int count) => + Format(translation => translation?.FictionSearchProgress, new { count = Formatter.ToFormattedString(count) }); + public string GetSciMagSearchProgressText(int count) => + Format(translation => translation?.SciMagSearchProgress, new { count = Formatter.ToFormattedString(count) }); + + private string Format(Func field, object templateArguments = null) + { + return Format(translation => field(translation?.SearchTab), templateArguments); + } + } +} diff --git a/LibgenDesktop/Models/Localization/Localizators/SettingsWindowLocalizator.cs b/LibgenDesktop/Models/Localization/Localizators/SettingsWindowLocalizator.cs new file mode 100644 index 0000000..f4cbac9 --- /dev/null +++ b/LibgenDesktop/Models/Localization/Localizators/SettingsWindowLocalizator.cs @@ -0,0 +1,141 @@ +using System; +using System.Collections.Generic; + +namespace LibgenDesktop.Models.Localization.Localizators +{ + internal class SettingsWindowLocalizator : Localizator + { + public SettingsWindowLocalizator(List prioritizedTranslationList, LanguageFormatter formatter) + : base(prioritizedTranslationList, formatter) + { + WindowTitle = Format(translation => translation?.WindowTitle); + Ok = Format(translation => translation?.Ok); + Cancel = Format(translation => translation?.Cancel); + DiscardChangesPromptTitle = Format(translation => translation?.DiscardChangesPromptTitle); + DiscardChangesPromptText = Format(translation => translation?.DiscardChangesPromptText); + GeneralTabHeader = Format(translation => translation?.General?.TabHeader); + GeneralLanguage = Format(translation => translation?.General?.Language); + GeneralCheckUpdates = Format(translation => translation?.General?.CheckUpdates); + GeneralUpdateCheckIntervalNever = Format(translation => translation?.General?.UpdateCheckIntervals?.Never); + GeneralUpdateCheckIntervalDaily = Format(translation => translation?.General?.UpdateCheckIntervals?.Daily); + GeneralUpdateCheckIntervalWeekly = Format(translation => translation?.General?.UpdateCheckIntervals?.Weekly); + GeneralUpdateCheckIntervalMonthly = Format(translation => translation?.General?.UpdateCheckIntervals?.Monthly); + NetworkTabHeader = Format(translation => translation?.Network?.TabHeader); + NetworkOfflineMode = Format(translation => translation?.Network?.OfflineMode); + NetworkUseHttpProxy = Format(translation => translation?.Network?.UseHttpProxy); + NetworkProxyAddress = Format(translation => translation?.Network?.ProxyAddress); + NetworkProxyAddressRequired = Format(translation => translation?.Network?.ProxyAddressRequired); + NetworkProxyPort = Format(translation => translation?.Network?.ProxyPort); + NetworkProxyUserName = Format(translation => translation?.Network?.ProxyUserName); + NetworkProxyPassword = Format(translation => translation?.Network?.ProxyPassword); + NetworkProxyPasswordWarning = Format(translation => translation?.Network?.ProxyPasswordWarning); + DownloadTabHeader = Format(translation => translation?.Download?.TabHeader); + DownloadDownloadMode = Format(translation => translation?.Download?.DownloadMode) + ":"; + DownloadOpenInBrowser = Format(translation => translation?.Download?.OpenInBrowser); + DownloadUseDownloadManager = Format(translation => translation?.Download?.UseDownloadManager); + DownloadDownloadDirectory = Format(translation => translation?.Download?.DownloadDirectory) + ":"; + DownloadBrowseDirectoryDialogTitle = Format(translation => translation?.Download?.BrowseDirectoryDialogTitle); + DownloadDownloadDirectoryNotFound = Format(translation => translation?.Download?.DownloadDirectoryNotFound); + DownloadTimeout = Format(translation => translation?.Download?.Timeout); + DownloadSeconds = Format(translation => translation?.Download?.Seconds); + DownloadDownloadAttempts = Format(translation => translation?.Download?.DownloadAttempts); + DownloadTimes = Format(translation => translation?.Download?.Times); + DownloadRetryDelay = Format(translation => translation?.Download?.RetryDelay); + MirrorsTabHeader = Format(translation => translation?.Mirrors?.TabHeader); + MirrorsNonFiction = Format(translation => translation?.Mirrors?.NonFiction); + MirrorsFiction = Format(translation => translation?.Mirrors?.Fiction); + MirrorsSciMagArticles = Format(translation => translation?.Mirrors?.SciMagArticles); + MirrorsBooks = Format(translation => translation?.Mirrors?.Books); + MirrorsArticles = Format(translation => translation?.Mirrors?.Articles); + MirrorsCovers = Format(translation => translation?.Mirrors?.Covers); + MirrorsSynchronization = Format(translation => translation?.Mirrors?.Synchronization); + MirrorsNoMirror = Format(translation => translation?.Mirrors?.NoMirror); + SearchTabHeader = Format(translation => translation?.Search?.TabHeader); + SearchLimitResults = Format(translation => translation?.Search?.LimitResults); + SearchMaximumResults = Format(translation => translation?.Search?.MaximumResults); + SearchPositiveNumbersOnly = Format(translation => translation?.Search?.PositiveNumbersOnly); + SearchOpenDetails = Format(translation => translation?.Search?.OpenDetails) + ":"; + SearchInModalWindow = Format(translation => translation?.Search?.InModalWindow); + SearchInNonModalWindow = Format(translation => translation?.Search?.InNonModalWindow); + SearchInNewTab = Format(translation => translation?.Search?.InNewTab); + ExportTabHeader = Format(translation => translation?.Export?.TabHeader); + ExportOpenResults = Format(translation => translation?.Export?.OpenResults); + ExportSplitIntoMultipleFiles = Format(translation => translation?.Export?.SplitIntoMultipleFiles); + ExportMaximumRowsPerFile = Format(translation => translation?.Export?.MaximumRowsPerFile); + AdvancedTabHeader = Format(translation => translation?.Advanced?.TabHeader); + AdvancedUseLogging = Format(translation => translation?.Advanced?.UseLogging); + } + + public string WindowTitle { get; set; } + public string Ok { get; set; } + public string Cancel { get; set; } + public string DiscardChangesPromptTitle { get; set; } + public string DiscardChangesPromptText { get; set; } + public string GeneralTabHeader { get; set; } + public string GeneralLanguage { get; set; } + public string GeneralCheckUpdates { get; set; } + public string GeneralUpdateCheckIntervalNever { get; set; } + public string GeneralUpdateCheckIntervalDaily { get; set; } + public string GeneralUpdateCheckIntervalWeekly { get; set; } + public string GeneralUpdateCheckIntervalMonthly { get; set; } + public string NetworkTabHeader { get; set; } + public string NetworkOfflineMode { get; set; } + public string NetworkUseHttpProxy { get; set; } + public string NetworkProxyAddress { get; set; } + public string NetworkProxyAddressRequired { get; set; } + public string NetworkProxyPort { get; set; } + public string NetworkProxyUserName { get; set; } + public string NetworkProxyPassword { get; set; } + public string NetworkProxyPasswordWarning { get; set; } + public string DownloadTabHeader { get; set; } + public string DownloadDownloadMode { get; set; } + public string DownloadOpenInBrowser { get; set; } + public string DownloadUseDownloadManager { get; set; } + public string DownloadDownloadDirectory { get; set; } + public string DownloadBrowseDirectoryDialogTitle { get; set; } + public string DownloadDownloadDirectoryNotFound { get; set; } + public string DownloadTimeout { get; set; } + public string DownloadSeconds { get; set; } + public string DownloadDownloadAttempts { get; set; } + public string DownloadTimes { get; set; } + public string DownloadRetryDelay { get; set; } + public string MirrorsTabHeader { get; set; } + public string MirrorsNonFiction { get; set; } + public string MirrorsFiction { get; set; } + public string MirrorsSciMagArticles { get; set; } + public string MirrorsBooks { get; set; } + public string MirrorsArticles { get; set; } + public string MirrorsCovers { get; set; } + public string MirrorsSynchronization { get; set; } + public string MirrorsNoMirror { get; set; } + public string SearchTabHeader { get; set; } + public string SearchLimitResults { get; set; } + public string SearchMaximumResults { get; set; } + public string SearchPositiveNumbersOnly { get; set; } + public string SearchOpenDetails { get; set; } + public string SearchInModalWindow { get; set; } + public string SearchInNonModalWindow { get; set; } + public string SearchInNewTab { get; set; } + public string ExportTabHeader { get; set; } + public string ExportOpenResults { get; set; } + public string ExportSplitIntoMultipleFiles { get; set; } + public string ExportMaximumRowsPerFile { get; set; } + public string AdvancedTabHeader { get; set; } + public string AdvancedUseLogging { get; set; } + + public string GetNetworkProxyPortValidation(int min, int max) => Format(translation => translation?.Network?.ProxyPortValidation, new { min, max }); + public string GetDownloadTimeoutValidation(int min, int max) => Format(translation => translation?.Download?.TimeoutValidation, new { min, max }); + public string GetDownloadDownloadAttemptsValidation(int min, int max) => + Format(translation => translation?.Download?.DownloadAttemptsValidation, new { min, max }); + public string GetDownloadRetryDelayValidation(int min, int max) => Format(translation => translation?.Download?.RetryDelayValidation, new { min, max }); + public string GetExportMaximumRowsPerFileValidation(int min, int max) => + Format(translation => translation?.Export?.MaximumRowsPerFileValidation, new { min, max }); + public string GetExportExcelLimitNote(int count) => + Format(translation => translation?.Export?.ExcelLimitNote, new { count = Formatter.ToFormattedString(count) }); + + private string Format(Func field, object templateArguments = null) + { + return Format(translation => field(translation?.Settings), templateArguments); + } + } +} diff --git a/LibgenDesktop/Models/Localization/Localizators/SynchronizationLocalizator.cs b/LibgenDesktop/Models/Localization/Localizators/SynchronizationLocalizator.cs new file mode 100644 index 0000000..292f811 --- /dev/null +++ b/LibgenDesktop/Models/Localization/Localizators/SynchronizationLocalizator.cs @@ -0,0 +1,89 @@ +using System; +using System.Collections.Generic; + +namespace LibgenDesktop.Models.Localization.Localizators +{ + internal class SynchronizationLocalizator : Localizator + { + public SynchronizationLocalizator(List prioritizedTranslationList, LanguageFormatter formatter) + : base(prioritizedTranslationList, formatter) + { + WindowTitle = Format(translation => translation?.WindowTitle); + ErrorMessageTitle = Format(translation => translation?.ErrorMessageTitle); + ImportRequired = Format(translation => translation?.ImportRequired); + NoSynchronizationMirror = Format(translation => translation?.NoSynchronizationMirror); + OfflineModePromptTitle = Format(translation => translation?.OfflineModePromptTitle); + OfflineModePromptText = Format(translation => translation?.OfflineModePromptText); + Interrupt = Format(translation => translation?.Interrupt); + Interrupting = Format(translation => translation?.Interrupting); + Close = Format(translation => translation?.Close); + StatusPreparation = FormatStatus(translation => translation?.Preparation); + StatusCreatingIndexes = FormatStatus(translation => translation?.CreatingIndexes); + StatusLoadingIds = FormatStatus(translation => translation?.LoadingIds); + StatusSynchronizingData = FormatStatus(translation => translation?.SynchronizingData); + StatusSynchronizationComplete = FormatStatus(translation => translation?.SynchronizationComplete); + StatusSynchronizationCancelled = FormatStatus(translation => translation?.SynchronizationCancelled); + StatusSynchronizationError = FormatStatus(translation => translation?.SynchronizationError); + LogLineCreatingIndexes = FormatLogLine(translation => translation?.CreatingIndexes); + LogLineLoadingIds = FormatLogLine(translation => translation?.LoadingIds); + LogLineSynchronizingBookList = FormatLogLine(translation => translation?.SynchronizingBookList); + LogLineDownloadingNewBooks = FormatLogLine(translation => translation?.DownloadingNewBooks); + LogLineSynchronizationSuccessful = FormatLogLine(translation => translation?.SynchronizationSuccessful); + LogLineSynchronizationCancelled = FormatLogLine(translation => translation?.SynchronizationCancelled); + LogLineSynchronizationError = FormatLogLine(translation => translation?.SynchronizationError); + } + + public string WindowTitle { get; } + public string ErrorMessageTitle { get; } + public string ImportRequired { get; } + public string NoSynchronizationMirror { get; } + public string OfflineModePromptTitle { get; } + public string OfflineModePromptText { get; } + public string Interrupt { get; } + public string Interrupting { get; } + public string Close { get; } + public string StatusPreparation { get; set; } + public string StatusCreatingIndexes { get; set; } + public string StatusLoadingIds { get; set; } + public string StatusSynchronizingData { get; set; } + public string StatusSynchronizationComplete { get; set; } + public string StatusSynchronizationCancelled { get; set; } + public string StatusSynchronizationError { get; set; } + public string LogLineCreatingIndexes { get; set; } + public string LogLineLoadingIds { get; set; } + public string LogLineSynchronizingBookList { get; set; } + public string LogLineDownloadingNewBooks { get; set; } + public string LogLineSynchronizationSuccessful { get; set; } + public string LogLineSynchronizationCancelled { get; set; } + public string LogLineSynchronizationError { get; set; } + + public string GetElapsedString(string elapsed) => Format(translation => translation?.Elapsed, new { elapsed }); + public string GetStatusStep(int current, int total) => FormatStatus(translation => translation?.Step, new { current, total }); + public string GetLogLineStep(int step) => FormatLogLine(translation => translation?.Step, new { step }); + public string GetLogLineCreatingIndexForColumn(string column) => FormatLogLine(translation => translation?.CreatingIndexForColumn, new { column }); + public string GetLogLineLoadingColumnValues(string column) => FormatLogLine(translation => translation?.LoadingColumnValues, new { column }); + public string GetLogLineSynchronizationProgressNoAddedNoUpdated(int downloaded) => + FormatLogLine(translation => translation?.SynchronizationProgressNoAddedNoUpdated, new { downloaded }); + public string GetLogLineSynchronizationProgressAdded(int downloaded, int added) => + FormatLogLine(translation => translation?.SynchronizationProgressAdded, new { downloaded, added }); + public string GetLogLineSynchronizationProgressUpdated(int downloaded, int updated) => + FormatLogLine(translation => translation?.SynchronizationProgressUpdated, new { downloaded, updated }); + public string GetLogLineSynchronizationProgressAddedAndUpdated(int downloaded, int added, int updated) => + FormatLogLine(translation => translation?.SynchronizationProgressAddedAndUpdated, new { downloaded, added, updated }); + + private string Format(Func field, object templateArguments = null) + { + return Format(translation => field(translation?.Synchronization), templateArguments); + } + + private string FormatStatus(Func field, object templateArguments = null) + { + return Format(translation => field(translation?.Synchronization?.StatusMessages), templateArguments); + } + + private string FormatLogLine(Func field, object templateArguments = null) + { + return Format(translation => field(translation?.Synchronization?.LogMessages), templateArguments); + } + } +} diff --git a/LibgenDesktop/Models/Localization/Translation.cs b/LibgenDesktop/Models/Localization/Translation.cs new file mode 100644 index 0000000..136c9d7 --- /dev/null +++ b/LibgenDesktop/Models/Localization/Translation.cs @@ -0,0 +1,757 @@ +namespace LibgenDesktop.Models.Localization +{ + internal class Translation + { + internal class GeneralInfo + { + public string Name { get; set; } + public string LocalizedName { get; set; } + public string CultureCode { get; set; } + public string TranslatorName { get; set; } + } + + internal class FileSizePostfixList + { + public string Byte { get; set; } + public string Kilobyte { get; set; } + public string Megabyte { get; set; } + public string Gigabyte { get; set; } + public string Terabyte { get; set; } + } + + internal class FormattingInfo + { + public string DecimalSeparator { get; set; } + public string ThousandsSeparator { get; set; } + public string DateFormat { get; set; } + public string TimeFormat { get; set; } + public FileSizePostfixList FileSizePostfixes { get; set; } + } + + internal class MainMenuTranslation + { + public string DownloadManagerTooltip { get; set; } + public string Update { get; set; } + public string Import { get; set; } + public string Synchronize { get; set; } + public string Settings { get; set; } + } + + internal class MainWindowTranslation + { + public string WindowTitle { get; set; } + public MainMenuTranslation MainMenu { get; set; } + } + + internal class DatabaseWindowTranslation + { + public string WindowTitle { get; set; } + public string FirstRunMessage { get; set; } + public string DatabaseNotFound { get; set; } + public string DatabaseCorrupted { get; set; } + public string ChooseOption { get; set; } + public string CreateNewDatabase { get; set; } + public string OpenExistingDatabase { get; set; } + public string BrowseNewDatabaseDialogTitle { get; set; } + public string BrowseExistingDatabaseDialogTitle { get; set; } + public string Databases { get; set; } + public string AllFiles { get; set; } + public string Error { get; set; } + public string CannotCreateDatabase { get; set; } + public string Ok { get; set; } + public string Cancel { get; set; } + } + + internal class SearchTabTranslation + { + public string TabTitle { get; set; } + public string SearchPlaceHolder { get; set; } + public string NonFictionSelector { get; set; } + public string FictionSelector { get; set; } + public string SciMagSelector { get; set; } + public string NonFictionSearchBoxTooltip { get; set; } + public string FictionSearchBoxTooltip { get; set; } + public string SciMagSearchBoxTooltip { get; set; } + public string SearchInProgress { get; set; } + public string NonFictionSearchProgress { get; set; } + public string FictionSearchProgress { get; set; } + public string SciMagSearchProgress { get; set; } + public string DatabaseIsEmpty { get; set; } + public string ImportButton { get; set; } + } + + internal class SearchResultsTabsTranslation + { + public string SearchPlaceHolder { get; set; } + public string SearchInProgress { get; set; } + public string ExportButtonTooltip { get; set; } + } + + internal class NonFictionSearchResultsGridColumnsTranslation + { + public string Title { get; set; } + public string Authors { get; set; } + public string Series { get; set; } + public string Year { get; set; } + public string Publisher { get; set; } + public string Format { get; set; } + public string FileSize { get; set; } + public string Ocr { get; set; } + } + + internal class NonFictionSearchResultsTabTranslation + { + public string SearchBoxTooltip { get; set; } + public string SearchProgress { get; set; } + public string StatusBar { get; set; } + public NonFictionSearchResultsGridColumnsTranslation Columns { get; set; } + } + + internal class FictionSearchResultsGridColumnsTranslation + { + public string Title { get; set; } + public string Authors { get; set; } + public string Series { get; set; } + public string Year { get; set; } + public string Publisher { get; set; } + public string Format { get; set; } + public string FileSize { get; set; } + } + + internal class FictionSearchResultsTabTranslation + { + public string SearchBoxTooltip { get; set; } + public string SearchProgress { get; set; } + public string StatusBar { get; set; } + public FictionSearchResultsGridColumnsTranslation Columns { get; set; } + } + + internal class SciMagSearchResultsGridColumnsTranslation + { + public string Title { get; set; } + public string Authors { get; set; } + public string Magazine { get; set; } + public string Year { get; set; } + public string FileSize { get; set; } + public string Doi { get; set; } + } + + internal class SciMagSearchResultsTabTranslation + { + public string SearchBoxTooltip { get; set; } + public string SearchProgress { get; set; } + public string StatusBar { get; set; } + public SciMagSearchResultsGridColumnsTranslation Columns { get; set; } + } + + internal class DetailsTabsTranslation + { + public string CoverIsLoading { get; set; } + public string NoCover { get; set; } + public string NoCoverMirror { get; set; } + public string NoCoverDueToOfflineMode { get; set; } + public string CoverLoadingError { get; set; } + public string Yes { get; set; } + public string No { get; set; } + public string Unknown { get; set; } + public string Portrait { get; set; } + public string Landscape { get; set; } + public string CopyContextMenu { get; set; } + public string Download { get; set; } + public string DownloadFromMirror { get; set; } + public string Queued { get; set; } + public string Downloading { get; set; } + public string Stopped { get; set; } + public string Error { get; set; } + public string Open { get; set; } + public string ErrorMessageTitle { get; set; } + public string FileNotFoundError { get; set; } + public string NoDownloadMirrorTooltip { get; set; } + public string OfflineModeIsOnTooltip { get; set; } + public string Close { get; set; } + } + + internal class NonFictionDetailsTabTranslation + { + public string Title { get; set; } + public string Authors { get; set; } + public string Series { get; set; } + public string Publisher { get; set; } + public string Year { get; set; } + public string Language { get; set; } + public string Format { get; set; } + public string Isbn { get; set; } + public string Added { get; set; } + public string LastModified { get; set; } + public string Library { get; set; } + public string FileSize { get; set; } + public string Topics { get; set; } + public string Volume { get; set; } + public string Magazine { get; set; } + public string City { get; set; } + public string Edition { get; set; } + public string Pages { get; set; } + public string BodyMatterPages { get; set; } + public string TotalPages { get; set; } + public string Tags { get; set; } + public string Md5Hash { get; set; } + public string Comments { get; set; } + public string Identifiers { get; set; } + public string LibgenId { get; set; } + public string Issn { get; set; } + public string Udc { get; set; } + public string Lbc { get; set; } + public string Lcc { get; set; } + public string Ddc { get; set; } + public string Doi { get; set; } + public string OpenLibraryId { get; set; } + public string GoogleBookId { get; set; } + public string Asin { get; set; } + public string AdditionalAttributes { get; set; } + public string Dpi { get; set; } + public string Ocr { get; set; } + public string TableOfContents { get; set; } + public string Scanned { get; set; } + public string Orientation { get; set; } + public string Paginated { get; set; } + public string Colored { get; set; } + public string Cleaned { get; set; } + } + + internal class FictionDetailsTabTranslation + { + public string Title { get; set; } + public string Authors { get; set; } + public string RussianAuthor { get; set; } + public string Series { get; set; } + public string Publisher { get; set; } + public string Edition { get; set; } + public string Year { get; set; } + public string Language { get; set; } + public string Format { get; set; } + public string Pages { get; set; } + public string Version { get; set; } + public string FileSize { get; set; } + public string Added { get; set; } + public string LastModified { get; set; } + public string Md5Hash { get; set; } + public string Comments { get; set; } + public string Identifiers { get; set; } + public string LibgenId { get; set; } + public string Isbn { get; set; } + public string GoogleBookId { get; set; } + public string Asin { get; set; } + } + + internal class SciMagDetailsTabTranslation + { + public string Title { get; set; } + public string Authors { get; set; } + public string Magazine { get; set; } + public string Year { get; set; } + public string Month { get; set; } + public string Day { get; set; } + public string Volume { get; set; } + public string Issue { get; set; } + public string Pages { get; set; } + public string FileSize { get; set; } + public string AddedDateTime { get; set; } + public string Md5Hash { get; set; } + public string AbstractUrl { get; set; } + public string Identifiers { get; set; } + public string LibgenId { get; set; } + public string Doi { get; set; } + public string Isbn { get; set; } + public string MagazineId { get; set; } + public string Issnp { get; set; } + public string Issne { get; set; } + public string PubmedId { get; set; } + public string Pmc { get; set; } + public string Pii { get; set; } + public string AdditionalAttributes { get; set; } + public string Attribute1 { get; set; } + public string Attribute2 { get; set; } + public string Attribute3 { get; set; } + public string Attribute4 { get; set; } + public string Attribute5 { get; set; } + public string Attribute6 { get; set; } + } + + internal class ImportStatusMessagesTranslation + { + public string Step { get; set; } + public string DataLookup { get; set; } + public string CreatingIndexes { get; set; } + public string LoadingIds { get; set; } + public string ImportingData { get; set; } + public string ImportComplete { get; set; } + public string ImportCancelled { get; set; } + public string DataNotFound { get; set; } + public string ImportError { get; set; } + } + + internal class ImportLogMessagesTranslation + { + public string Step { get; set; } + public string DataLookup { get; set; } + public string Scanning { get; set; } + public string ScannedProgress { get; set; } + public string NonFictionTableFound { get; set; } + public string FictionTableFound { get; set; } + public string SciMagTableFound { get; set; } + public string CreatingIndexes { get; set; } + public string CreatingIndexForColumn { get; set; } + public string LoadingIds { get; set; } + public string LoadingColumnValues { get; set; } + public string ImportingData { get; set; } + public string ImportBooksProgressNoUpdate { get; set; } + public string ImportBooksProgressWithUpdate { get; set; } + public string ImportArticlesProgressNoUpdate { get; set; } + public string ImportArticlesProgressWithUpdate { get; set; } + public string ImportSuccessful { get; set; } + public string ImportCancelled { get; set; } + public string DataNotFound { get; set; } + public string ImportError { get; set; } + } + + internal class ImportTranslation + { + public string WindowTitle { get; set; } + public string BrowseImportFileDialogTitle { get; set; } + public string AllSupportedFiles { get; set; } + public string SqlDumps { get; set; } + public string Archives { get; set; } + public string AllFiles { get; set; } + public string Elapsed { get; set; } + public string Interrupt { get; set; } + public string Interrupting { get; set; } + public string Close { get; set; } + public ImportStatusMessagesTranslation StatusMessages { get; set; } + public ImportLogMessagesTranslation LogMessages { get; set; } + } + + internal class ExportPanelTranslation + { + public string Header { get; set; } + public string Format { get; set; } + public string Excel { get; set; } + public string Csv { get; set; } + public string Separator { get; set; } + public string Comma { get; set; } + public string Semicolon { get; set; } + public string Tab { get; set; } + public string SaveAs { get; set; } + public string Browse { get; set; } + public string BrowseDialogTitle { get; set; } + public string ExcelFiles { get; set; } + public string CsvFiles { get; set; } + public string TsvFiles { get; set; } + public string AllFiles { get; set; } + public string ExportRange { get; set; } + public string NoLimit { get; set; } + public string Limit { get; set; } + public string Export { get; set; } + public string Cancel { get; set; } + public string SavingFile { get; set; } + public string RowCountSingleFile { get; set; } + public string RowCountMultipleFiles { get; set; } + public string ErrorWarningTitle { get; set; } + public string InvalidExportPath { get; set; } + public string DirectoryNotFound { get; set; } + public string InvalidExportFileName { get; set; } + public string OverwritePromptTitle { get; set; } + public string OverwritePromptText { get; set; } + public string RowLimitWarningTitle { get; set; } + public string RowLimitWarningText { get; set; } + public string ExportError { get; set; } + public string Interrupt { get; set; } + public string Interrupting { get; set; } + public string ExportInterrupted { get; set; } + public string Results { get; set; } + public string Close { get; set; } + } + + internal class NonFictionExporterColumnsTranslation + { + public string Id { get; set; } + public string Title { get; set; } + public string Authors { get; set; } + public string Series { get; set; } + public string Publisher { get; set; } + public string Year { get; set; } + public string Language { get; set; } + public string Format { get; set; } + public string Isbn { get; set; } + public string Added { get; set; } + public string LastModified { get; set; } + public string Library { get; set; } + public string FileSize { get; set; } + public string Topics { get; set; } + public string Volume { get; set; } + public string Magazine { get; set; } + public string City { get; set; } + public string Edition { get; set; } + public string BodyMatterPages { get; set; } + public string TotalPages { get; set; } + public string Tags { get; set; } + public string Md5Hash { get; set; } + public string Comments { get; set; } + public string LibgenId { get; set; } + public string Issn { get; set; } + public string Udc { get; set; } + public string Lbc { get; set; } + public string Lcc { get; set; } + public string Ddc { get; set; } + public string Doi { get; set; } + public string OpenLibraryId { get; set; } + public string GoogleBookId { get; set; } + public string Asin { get; set; } + public string Dpi { get; set; } + public string Ocr { get; set; } + public string TableOfContents { get; set; } + public string Scanned { get; set; } + public string Orientation { get; set; } + public string Paginated { get; set; } + public string Colored { get; set; } + public string Cleaned { get; set; } + } + + internal class FictionExporterColumnsTranslation + { + public string Id { get; set; } + public string Title { get; set; } + public string Authors { get; set; } + public string RussianAuthor { get; set; } + public string Series { get; set; } + public string Publisher { get; set; } + public string Edition { get; set; } + public string Year { get; set; } + public string Language { get; set; } + public string Format { get; set; } + public string Pages { get; set; } + public string Version { get; set; } + public string FileSize { get; set; } + public string Added { get; set; } + public string LastModified { get; set; } + public string Md5Hash { get; set; } + public string Comments { get; set; } + public string LibgenId { get; set; } + public string Isbn { get; set; } + public string GoogleBookId { get; set; } + public string Asin { get; set; } + } + + internal class SciMagExporterColumnsTranslation + { + public string Id { get; set; } + public string Title { get; set; } + public string Authors { get; set; } + public string Magazine { get; set; } + public string Year { get; set; } + public string Month { get; set; } + public string Day { get; set; } + public string Volume { get; set; } + public string Issue { get; set; } + public string Pages { get; set; } + public string FileSize { get; set; } + public string AddedDateTime { get; set; } + public string Md5Hash { get; set; } + public string AbstractUrl { get; set; } + public string LibgenId { get; set; } + public string Doi1 { get; set; } + public string Doi2 { get; set; } + public string Isbn { get; set; } + public string MagazineId { get; set; } + public string Issnp { get; set; } + public string Issne { get; set; } + public string PubmedId { get; set; } + public string Pmc { get; set; } + public string Pii { get; set; } + public string Attribute1 { get; set; } + public string Attribute2 { get; set; } + public string Attribute3 { get; set; } + public string Attribute4 { get; set; } + public string Attribute5 { get; set; } + public string Attribute6 { get; set; } + } + + internal class ExporterTranslation + { + public string Yes { get; set; } + public string No { get; set; } + public string Unknown { get; set; } + public string Portrait { get; set; } + public string Landscape { get; set; } + public NonFictionExporterColumnsTranslation NonFictionColumns { get; set; } + public FictionExporterColumnsTranslation FictionColumns { get; set; } + public SciMagExporterColumnsTranslation SciMagColumns { get; set; } + } + + internal class SynchronizationStatusMessagesTranslation + { + public string Step { get; set; } + public string Preparation { get; set; } + public string CreatingIndexes { get; set; } + public string LoadingIds { get; set; } + public string SynchronizingData { get; set; } + public string SynchronizationComplete { get; set; } + public string SynchronizationCancelled { get; set; } + public string SynchronizationError { get; set; } + } + + internal class SynchronizationLogMessagesTranslation + { + public string Step { get; set; } + public string CreatingIndexes { get; set; } + public string CreatingIndexForColumn { get; set; } + public string LoadingIds { get; set; } + public string LoadingColumnValues { get; set; } + public string SynchronizingBookList { get; set; } + public string DownloadingNewBooks { get; set; } + public string SynchronizationProgressNoAddedNoUpdated { get; set; } + public string SynchronizationProgressAdded { get; set; } + public string SynchronizationProgressUpdated { get; set; } + public string SynchronizationProgressAddedAndUpdated { get; set; } + public string SynchronizationSuccessful { get; set; } + public string SynchronizationCancelled { get; set; } + public string SynchronizationError { get; set; } + } + + internal class SynchronizationTranslation + { + public string WindowTitle { get; set; } + public string ErrorMessageTitle { get; set; } + public string ImportRequired { get; set; } + public string NoSynchronizationMirror { get; set; } + public string OfflineModePromptTitle { get; set; } + public string OfflineModePromptText { get; set; } + public string Elapsed { get; set; } + public string Interrupt { get; set; } + public string Interrupting { get; set; } + public string Close { get; set; } + public SynchronizationStatusMessagesTranslation StatusMessages { get; set; } + public SynchronizationLogMessagesTranslation LogMessages { get; set; } + } + + internal class DownloadManagerLogMessagesTranslation + { + public string Queued { get; set; } + public string Started { get; set; } + public string Stopped { get; set; } + public string RetryDelay { get; set; } + public string Completed { get; set; } + public string OfflineModeIsOn { get; set; } + public string TransformationError { get; set; } + public string TransformationReturnedIncorrectUrl { get; set; } + public string Attempt { get; set; } + public string MaximumDownloadAttempts { get; set; } + public string DownloadingPage { get; set; } + public string DownloadingFile { get; set; } + public string StartingFileDownloadKnownFileSize { get; set; } + public string StartingFileDownloadUnknownFileSize { get; set; } + public string ResumingFileDownloadKnownFileSize { get; set; } + public string ResumingFileDownloadUnknownFileSize { get; set; } + public string Request { get; set; } + public string Response { get; set; } + public string Redirect { get; set; } + public string TooManyRedirects { get; set; } + public string NonSuccessfulStatusCode { get; set; } + public string CannotCreateDownloadDirectory { get; set; } + public string CannotCreateOrOpenFile { get; set; } + public string CannotRenamePartFile { get; set; } + public string HtmlPageReturned { get; set; } + public string NoPartialDownloadSupport { get; set; } + public string NoContentLengthWarning { get; set; } + public string ServerResponseTimeout { get; set; } + public string DownloadIncompleteError { get; set; } + public string FileWriteError { get; set; } + public string UnexpectedError { get; set; } + } + + internal class DownloadManagerTranslation + { + public string TabTitle { get; set; } + public string Start { get; set; } + public string Stop { get; set; } + public string Remove { get; set; } + public string StartAll { get; set; } + public string StopAll { get; set; } + public string RemoveCompleted { get; set; } + public string QueuedStatus { get; set; } + public string DownloadingStatus { get; set; } + public string StoppedStatus { get; set; } + public string RetryDelayStatus { get; set; } + public string ErrorStatus { get; set; } + public string DownloadProgressKnownFileSize { get; set; } + public string DownloadProgressUnknownFileSize { get; set; } + public string Log { get; set; } + public string TechnicalDetails { get; set; } + public string Copy { get; set; } + public string FileNotFoundErrorTitle { get; set; } + public string FileNotFoundErrorText { get; set; } + public DownloadManagerLogMessagesTranslation LogMessages { get; set; } + } + + internal class ApplicationUpdateTranslation + { + public string WindowTitle { get; set; } + public string UpdateAvailable { get; set; } + public string NewVersion { get; set; } + public string Download { get; set; } + public string DownloadAndInstall { get; set; } + public string SkipThisVersion { get; set; } + public string Cancel { get; set; } + public string Interrupt { get; set; } + public string Interrupting { get; set; } + public string InterruptPromptTitle { get; set; } + public string InterruptPromptText { get; set; } + public string Error { get; set; } + public string IncompleteDownload { get; set; } + public string Close { get; set; } + } + + internal class UpdateCheckIntervalTranslation + { + public string Never { get; set; } + public string Daily { get; set; } + public string Weekly { get; set; } + public string Monthly { get; set; } + } + + internal class GeneralSettingsTranslation + { + public string TabHeader { get; set; } + public string Language { get; set; } + public string CheckUpdates { get; set; } + public UpdateCheckIntervalTranslation UpdateCheckIntervals { get; set; } + } + + internal class NetworkSettingsTranslation + { + public string TabHeader { get; set; } + public string OfflineMode { get; set; } + public string UseHttpProxy { get; set; } + public string ProxyAddress { get; set; } + public string ProxyAddressRequired { get; set; } + public string ProxyPort { get; set; } + public string ProxyPortValidation { get; set; } + public string ProxyUserName { get; set; } + public string ProxyPassword { get; set; } + public string ProxyPasswordWarning { get; set; } + } + + internal class DownloadSettingsTranslation + { + public string TabHeader { get; set; } + public string DownloadMode { get; set; } + public string OpenInBrowser { get; set; } + public string UseDownloadManager { get; set; } + public string DownloadDirectory { get; set; } + public string BrowseDirectoryDialogTitle { get; set; } + public string DownloadDirectoryNotFound { get; set; } + public string Timeout { get; set; } + public string TimeoutValidation { get; set; } + public string Seconds { get; set; } + public string DownloadAttempts { get; set; } + public string DownloadAttemptsValidation { get; set; } + public string Times { get; set; } + public string RetryDelay { get; set; } + public string RetryDelayValidation { get; set; } + } + + internal class MirrorsSettingsTranslation + { + public string TabHeader { get; set; } + public string NonFiction { get; set; } + public string Fiction { get; set; } + public string SciMagArticles { get; set; } + public string Books { get; set; } + public string Articles { get; set; } + public string Covers { get; set; } + public string Synchronization { get; set; } + public string NoMirror { get; set; } + } + + internal class SearchSettingsTranslation + { + public string TabHeader { get; set; } + public string LimitResults { get; set; } + public string MaximumResults { get; set; } + public string PositiveNumbersOnly { get; set; } + public string OpenDetails { get; set; } + public string InModalWindow { get; set; } + public string InNonModalWindow { get; set; } + public string InNewTab { get; set; } + } + + internal class ExportSettingsTranslation + { + public string TabHeader { get; set; } + public string OpenResults { get; set; } + public string SplitIntoMultipleFiles { get; set; } + public string MaximumRowsPerFile { get; set; } + public string MaximumRowsPerFileValidation { get; set; } + public string ExcelLimitNote { get; set; } + } + + internal class AdvancedSettingsTranslation + { + public string TabHeader { get; set; } + public string UseLogging { get; set; } + } + + internal class SettingsTranslation + { + public string WindowTitle { get; set; } + public string Ok { get; set; } + public string Cancel { get; set; } + public string DiscardChangesPromptTitle { get; set; } + public string DiscardChangesPromptText { get; set; } + public GeneralSettingsTranslation General { get; set; } + public NetworkSettingsTranslation Network { get; set; } + public DownloadSettingsTranslation Download { get; set; } + public MirrorsSettingsTranslation Mirrors { get; set; } + public SearchSettingsTranslation Search { get; set; } + public ExportSettingsTranslation Export { get; set; } + public AdvancedSettingsTranslation Advanced { get; set; } + } + + internal class MessageBoxTranslation + { + public string Ok { get; set; } + public string Yes { get; set; } + public string No { get; set; } + } + + internal class ErrorWindowTranslation + { + public string WindowTitle { get; set; } + public string UnexpectedError { get; set; } + public string Copy { get; set; } + public string Close { get; set; } + } + + public GeneralInfo General { get; set; } + public FormattingInfo Formatting { get; set; } + public MainWindowTranslation MainWindow { get; set; } + public DatabaseWindowTranslation DatabaseWindow { get; set; } + public SearchTabTranslation SearchTab { get; set; } + public SearchResultsTabsTranslation SearchResultsTabs { get; set; } + public NonFictionSearchResultsTabTranslation NonFictionSearchResultsTab { get; set; } + public FictionSearchResultsTabTranslation FictionSearchResultsTab { get; set; } + public SciMagSearchResultsTabTranslation SciMagSearchResultsTab { get; set; } + public DetailsTabsTranslation DetailsTabs { get; set; } + public NonFictionDetailsTabTranslation NonFictionDetailsTab { get; set; } + public FictionDetailsTabTranslation FictionDetailsTab { get; set; } + public SciMagDetailsTabTranslation SciMagDetailsTab { get; set; } + public ImportTranslation Import { get; set; } + public ExportPanelTranslation ExportPanel { get; set; } + public ExporterTranslation Exporter { get; set; } + public SynchronizationTranslation Synchronization { get; set; } + public DownloadManagerTranslation DownloadManager { get; set; } + public ApplicationUpdateTranslation ApplicationUpdate { get; set; } + public SettingsTranslation Settings { get; set; } + public MessageBoxTranslation MessageBox { get; set; } + public ErrorWindowTranslation ErrorWindow { get; set; } + } +} diff --git a/LibgenDesktop/Models/MainModel.cs b/LibgenDesktop/Models/MainModel.cs index bf451a9..82a9c57 100644 --- a/LibgenDesktop/Models/MainModel.cs +++ b/LibgenDesktop/Models/MainModel.cs @@ -1,7 +1,6 @@ using System; using System.Collections; using System.Collections.Generic; -using System.Collections.ObjectModel; using System.IO; using System.Linq; using System.Net; @@ -10,6 +9,7 @@ using System.Threading.Tasks; using LibgenDesktop.Common; using LibgenDesktop.Models.Database; +using LibgenDesktop.Models.Download; using LibgenDesktop.Models.Entities; using LibgenDesktop.Models.Export; using LibgenDesktop.Models.Import; @@ -19,6 +19,7 @@ using LibgenDesktop.Models.Settings; using LibgenDesktop.Models.SqlDump; using LibgenDesktop.Models.Update; +using LibgenDesktop.Models.Utils; using static LibgenDesktop.Common.Constants; using Environment = LibgenDesktop.Common.Environment; @@ -64,32 +65,37 @@ public MainModel() { EnableLogging(); } - Mirrors = MirrorStorage.LoadMirrors(Environment.MirrorsFilePath); + ValidateAndCorrectDirectoryPaths(); + Mirrors = MirrorStorage.LoadMirrors(Path.Combine(Environment.MirrorsDirectoryPath, MIRRORS_FILE_NAME)); ValidateAndCorrectSelectedMirrors(); - Languages = LocalizationStorage.LoadLanguages(); - AppSettings.General.Language = Languages.First().Key; + Localization = new LocalizationStorage(Environment.LanguagesDirectoryPath, AppSettings.General.Language); CreateNewHttpClient(); OpenDatabase(AppSettings.DatabaseFileName); + LastApplicationUpdateCheckDateTime = AppSettings.LastUpdate.LastCheckedAt; LastApplicationUpdateCheckResult = null; updater = new Updater(); updater.UpdateCheck += ApplicationUpdateCheck; ConfigureUpdater(); + Downloader = new Downloader(); + ConfigureDownloader(); } public AppSettings AppSettings { get; } public DatabaseStatus LocalDatabaseStatus { get; private set; } public DatabaseMetadata DatabaseMetadata { get; private set; } public Mirrors Mirrors { get; } - public Dictionary Languages { get; } + public LocalizationStorage Localization { get; } public HttpClient HttpClient { get; private set; } + public Downloader Downloader { get; } public int NonFictionBookCount { get; private set; } public int FictionBookCount { get; private set; } public int SciMagArticleCount { get; private set; } + public DateTime? LastApplicationUpdateCheckDateTime { get; set; } public Updater.UpdateCheckResult LastApplicationUpdateCheckResult { get; set; } public event EventHandler ApplicationUpdateCheckCompleted; - public Task> SearchNonFictionAsync(string searchQuery, IProgress progressHandler, + public Task> SearchNonFictionAsync(string searchQuery, IProgress progressHandler, CancellationToken cancellationToken) { return SearchItemsAsync(localDatabase.SearchNonFictionBooks, searchQuery, progressHandler, cancellationToken); @@ -115,7 +121,7 @@ public Task LoadNonFictionBookAsync(int bookId) return LoadItemAsync(localDatabase.GetNonFictionBookById, bookId); } - public Task> SearchFictionAsync(string searchQuery, IProgress progressHandler, + public Task> SearchFictionAsync(string searchQuery, IProgress progressHandler, CancellationToken cancellationToken) { return SearchItemsAsync(localDatabase.SearchFictionBooks, searchQuery, progressHandler, cancellationToken); @@ -141,7 +147,7 @@ public Task LoadFictionBookAsync(int bookId) return LoadItemAsync(localDatabase.GetFictionBookById, bookId); } - public Task> SearchSciMagAsync(string searchQuery, IProgress progressHandler, + public Task> SearchSciMagAsync(string searchQuery, IProgress progressHandler, CancellationToken cancellationToken) { return SearchItemsAsync(localDatabase.SearchSciMagArticles, searchQuery, progressHandler, cancellationToken); @@ -594,24 +600,12 @@ public bool CreateDatabase(string databaseFilePath) public void CreateNewHttpClient() { - AppSettings.NetworkSettings networkSettings = AppSettings.Network; - WebProxy webProxy; - if (networkSettings.UseProxy) - { - webProxy = new WebProxy(networkSettings.ProxyAddress, networkSettings.ProxyPort.Value); - if (!String.IsNullOrEmpty(networkSettings.ProxyUserName)) - { - webProxy.Credentials = new NetworkCredential(networkSettings.ProxyUserName, networkSettings.ProxyPassword); - } - } - else - { - webProxy = new WebProxy(); - } HttpClientHandler httpClientHandler = new HttpClientHandler { - Proxy = webProxy, - UseProxy = true + Proxy = NetworkUtils.CreateProxy(AppSettings.Network), + UseProxy = true, + AllowAutoRedirect = true, + UseCookies = false }; HttpClient = new HttpClient(httpClientHandler); HttpClient.DefaultRequestHeaders.UserAgent.ParseAdd(USER_AGENT); @@ -636,8 +630,7 @@ public void ConfigureUpdater() } else { - DateTime? lastUpdateCheck = AppSettings.LastUpdate.LastCheckedAt; - if (!lastUpdateCheck.HasValue) + if (!LastApplicationUpdateCheckDateTime.HasValue) { nextUpdateCheck = DateTime.Now; } @@ -646,13 +639,13 @@ public void ConfigureUpdater() switch (AppSettings.General.UpdateCheck) { case AppSettings.GeneralSettings.UpdateCheckInterval.DAILY: - nextUpdateCheck = lastUpdateCheck.Value.AddDays(1); + nextUpdateCheck = LastApplicationUpdateCheckDateTime.Value.AddDays(1); break; case AppSettings.GeneralSettings.UpdateCheckInterval.WEEKLY: - nextUpdateCheck = lastUpdateCheck.Value.AddDays(7); + nextUpdateCheck = LastApplicationUpdateCheckDateTime.Value.AddDays(7); break; case AppSettings.GeneralSettings.UpdateCheckInterval.MONTHLY: - nextUpdateCheck = lastUpdateCheck.Value.AddMonths(1); + nextUpdateCheck = LastApplicationUpdateCheckDateTime.Value.AddMonths(1); break; default: throw new Exception($"Unexpected update check interval: {AppSettings.General.UpdateCheck}."); @@ -662,7 +655,12 @@ public void ConfigureUpdater() updater.Configure(HttpClient, nextUpdateCheck, AppSettings.LastUpdate.IgnoreReleaseName); } - private Task> SearchItemsAsync(Func> searchFunction, string searchQuery, + public void ConfigureDownloader() + { + Downloader.Configure(Localization.CurrentLanguage, AppSettings.Network, AppSettings.Download); + } + + private Task> SearchItemsAsync(Func> searchFunction, string searchQuery, IProgress progressHandler, CancellationToken cancellationToken) { return Task.Run(() => @@ -673,7 +671,7 @@ private Task> SearchItemsAsync(Func result = new ObservableCollection(); + List result = new List(); DateTime lastReportDateTime = DateTime.Now; foreach (T item in searchFunction(searchQuery, resultLimit)) { @@ -710,7 +708,7 @@ private Task ExportToXlsxAsync(Func, IProgress, CancellationToken, ExportResult> exportFunction = xlsxExporterFunction(xlsxExporter); return ExportAsync(searchFunction, exportFunction, searchQuery, searchResultLimit, progressHandler, cancellationToken); } @@ -729,7 +727,8 @@ private Task ExportToCsvAsync(Func { rowsPerFile = null; } - CsvExporter csvExporter = new CsvExporter(filePathTemplate, fileExtension, rowsPerFile, splitIntoMultipleFiles, separator); + CsvExporter csvExporter = new CsvExporter(filePathTemplate, fileExtension, rowsPerFile, splitIntoMultipleFiles, separator, + Localization.CurrentLanguage); Func, IProgress, CancellationToken, ExportResult> exportFunction = csvExporterFunction(csvExporter); return ExportAsync(searchFunction, exportFunction, searchQuery, searchResultLimit, progressHandler, cancellationToken); } @@ -889,12 +888,25 @@ private string ValidateAndCorrectSelectedMirror(string selectedMirrorName, Func< return null; } + private void ValidateAndCorrectDirectoryPaths() + { + string downloadDirectory = AppSettings.Download.DownloadDirectory; + if (String.IsNullOrWhiteSpace(downloadDirectory) || !Directory.Exists(downloadDirectory)) + { + AppSettings.Download.DownloadDirectory = Path.Combine(Environment.AppDataDirectory, DEFAULT_DOWNLOAD_DIRECTORY_NAME); + } + } + private void ApplicationUpdateCheck(object sender, Updater.UpdateCheckEventArgs e) { LastApplicationUpdateCheckResult = e.Result; ApplicationUpdateCheckCompleted?.Invoke(this, EventArgs.Empty); - AppSettings.LastUpdate.LastCheckedAt = DateTime.Now; - SaveSettings(); + LastApplicationUpdateCheckDateTime = DateTime.Now; + if (e.Result == null) + { + AppSettings.LastUpdate.LastCheckedAt = LastApplicationUpdateCheckDateTime; + SaveSettings(); + } ConfigureUpdater(); } } diff --git a/LibgenDesktop/Models/Settings/AppSettings.cs b/LibgenDesktop/Models/Settings/AppSettings.cs index 5c795c5..0e3c109 100644 --- a/LibgenDesktop/Models/Settings/AppSettings.cs +++ b/LibgenDesktop/Models/Settings/AppSettings.cs @@ -260,6 +260,24 @@ public static SciMagSettings Default public ExportPanelSettngs ExportPanel { get; set; } } + internal class DownloadManagerTabSettings + { + public static DownloadManagerTabSettings Default + { + get + { + return new DownloadManagerTabSettings + { + LogPanelHeight = DOWNLOAD_MANAGER_TAB_LOG_PANEL_DEFAULT_HEIGHT, + ShowDebugLogs = false + }; + } + } + + public int LogPanelHeight { get; set; } + public bool ShowDebugLogs { get; set; } + } + internal class LastUpdateSettings { public static LastUpdateSettings Default @@ -330,6 +348,30 @@ public static NetworkSettings Default public string ProxyPassword { get; set; } } + internal class DownloadSettings + { + public static DownloadSettings Default + { + get + { + return new DownloadSettings + { + UseDownloadManager = false, + DownloadDirectory = null, + Timeout = DEFAULT_DOWNLOAD_TIMEOUT, + Attempts = DEFAULT_DOWNLOAD_ATTEMPT_COUNT, + RetryDelay = DEFAULT_DOWNLOAD_RETRY_DELAY + }; + } + } + + public bool UseDownloadManager { get; set; } + public string DownloadDirectory { get; set; } + public int Timeout { get; set; } + public int Attempts { get; set; } + public int RetryDelay { get; set; } + } + internal class MirrorSettings { public static MirrorSettings Default @@ -430,9 +472,11 @@ public static AppSettings Default NonFiction = NonFictionSettings.Default, Fiction = FictionSettings.Default, SciMag = SciMagSettings.Default, + DownloadManagerTab = DownloadManagerTabSettings.Default, LastUpdate = LastUpdateSettings.Default, General = GeneralSettings.Default, Network = NetworkSettings.Default, + Download = DownloadSettings.Default, Mirrors = MirrorSettings.Default, Search = SearchSettings.Default, Export = ExportSettings.Default, @@ -446,9 +490,11 @@ public static AppSettings Default public NonFictionSettings NonFiction { get; set; } public FictionSettings Fiction { get; set; } public SciMagSettings SciMag { get; set; } + public DownloadManagerTabSettings DownloadManagerTab { get; set; } public LastUpdateSettings LastUpdate { get; set; } public GeneralSettings General { get; set; } public NetworkSettings Network { get; set; } + public DownloadSettings Download { get; set; } public MirrorSettings Mirrors { get; set; } public SearchSettings Search { get; set; } public ExportSettings Export { get; set; } @@ -467,9 +513,11 @@ public static AppSettings ValidateAndCorrect(AppSettings appSettings) appSettings.ValidateAndCorrectNonFictionSettings(); appSettings.ValidateAndCorrectFictionSettings(); appSettings.ValidateAndCorrectSciMagSettings(); + appSettings.ValidateAndCorrectDownloadManagerTabSettings(); appSettings.ValidateAndCorrectLastUpdateSettings(); appSettings.ValidateAndCorrectGeneralSettings(); appSettings.ValidateAndCorrectNetworkSettings(); + appSettings.ValidateAndCorrectDownloadSettings(); appSettings.ValidateAndCorrectMirrorSettings(); appSettings.ValidateAndCorrectSearchSettings(); appSettings.ValidateAndCorrectExportSettings(); @@ -713,6 +761,21 @@ private void ValidateAndCorrectSciMagSettings() } } + private void ValidateAndCorrectDownloadManagerTabSettings() + { + if (DownloadManagerTab == null) + { + DownloadManagerTab = DownloadManagerTabSettings.Default; + } + else + { + if (DownloadManagerTab.LogPanelHeight < DOWNLOAD_MANAGER_TAB_LOG_PANEL_MIN_HEIGHT) + { + DownloadManagerTab.LogPanelHeight = DOWNLOAD_MANAGER_TAB_LOG_PANEL_MIN_HEIGHT; + } + } + } + private ExportPanelSettngs ValidateAndCorrectExportPanelSettings(ExportPanelSettngs exportPanelSettngs) { if (exportPanelSettngs == null) @@ -773,6 +836,49 @@ private void ValidateAndCorrectNetworkSettings() Network.ProxyPort = null; Network.UseProxy = false; } + if (Network.ProxyUserName == null) + { + Network.ProxyUserName = String.Empty; + } + if (Network.ProxyPassword == null) + { + Network.ProxyPassword = String.Empty; + } + } + } + + private void ValidateAndCorrectDownloadSettings() + { + if (Download == null) + { + Download = DownloadSettings.Default; + } + else + { + if (Download.Timeout < MIN_DOWNLOAD_TIMEOUT) + { + Download.Timeout = MIN_DOWNLOAD_TIMEOUT; + } + else if (Download.Timeout > MAX_DOWNLOAD_TIMEOUT) + { + Download.Timeout = MAX_DOWNLOAD_TIMEOUT; + } + if (Download.Attempts < 1) + { + Download.Attempts = 1; + } + else if (Download.Attempts > MAX_DOWNLOAD_ATTEMPT_COUNT) + { + Download.Attempts = MAX_DOWNLOAD_ATTEMPT_COUNT; + } + if (Download.RetryDelay < 0) + { + Download.RetryDelay = 0; + } + else if (Download.RetryDelay > MAX_DOWNLOAD_RETRY_DELAY) + { + Download.RetryDelay = MAX_DOWNLOAD_RETRY_DELAY; + } } } diff --git a/LibgenDesktop/Models/Settings/Mirrors.cs b/LibgenDesktop/Models/Settings/Mirrors.cs index afd9810..58604c5 100644 --- a/LibgenDesktop/Models/Settings/Mirrors.cs +++ b/LibgenDesktop/Models/Settings/Mirrors.cs @@ -7,11 +7,14 @@ internal class Mirrors : Dictionary internal class MirrorConfiguration { public string NonFictionDownloadUrl { get; set; } + public string NonFictionDownloadTransformations { get; set; } public string NonFictionCoverUrl { get; set; } public string NonFictionSynchronizationUrl { get; set; } public string FictionDownloadUrl { get; set; } + public string FictionDownloadTransformations { get; set; } public string FictionCoverUrl { get; set; } public string SciMagDownloadUrl { get; set; } + public string SciMagDownloadTransformations { get; set; } } } } diff --git a/LibgenDesktop/Models/SqlDump/ParsedTableDefinition.cs b/LibgenDesktop/Models/SqlDump/ParsedTableDefinition.cs deleted file mode 100644 index 303c4ea..0000000 --- a/LibgenDesktop/Models/SqlDump/ParsedTableDefinition.cs +++ /dev/null @@ -1,9 +0,0 @@ -using System; -using System.Collections.Generic; -using System.Linq; -using System.Text; -using System.Threading.Tasks; - -namespace LibgenDesktop.Models.SqlDump -{ -} diff --git a/LibgenDesktop/Models/Update/Updater.cs b/LibgenDesktop/Models/Update/Updater.cs index c581cc7..d0ae9ab 100644 --- a/LibgenDesktop/Models/Update/Updater.cs +++ b/LibgenDesktop/Models/Update/Updater.cs @@ -14,6 +14,11 @@ namespace LibgenDesktop.Models.Update { internal class Updater : IDisposable { + private const string ASSET_NAME_SETUP_64BIT = "LibgenDesktop.Setup.64-bit.msi"; + private const string ASSET_NAME_SETUP_32BIT = "LibgenDesktop.Setup.32-bit.msi"; + private const string ASSET_NAME_PORTABLE_64BIT = "LibgenDesktop.Portable.64-bit.zip"; + private const string ASSET_NAME_PORTABLE_32BIT = "LibgenDesktop.Portable.32-bit.zip"; + internal class UpdateCheckResult { public UpdateCheckResult(string newReleaseName, DateTime publishedAt, string fileName, int fileSize, string downloadUrl) @@ -65,7 +70,6 @@ public void Configure(HttpClient httpClient, DateTime? nextUpdateCheck, string i this.ignoreReleaseName = ignoreReleaseName; if (timer != null) { - return; timer.Dispose(); timer = null; } @@ -156,11 +160,11 @@ private string GetExpectedAssetName() { if (Environment.IsInPortableMode) { - return Environment.IsIn64BitProcess ? "LibgenDesktop.Portable.64-bit.zip" : "LibgenDesktop.Portable.32-bit.zip"; + return Environment.IsIn64BitProcess ? ASSET_NAME_PORTABLE_64BIT : ASSET_NAME_PORTABLE_32BIT; } else { - return Environment.IsIn64BitProcess ? "LibgenDesktop.Setup.64-bit.msi" : "LibgenDesktop.Setup.32-bit.msi"; + return Environment.IsIn64BitProcess ? ASSET_NAME_SETUP_64BIT : ASSET_NAME_SETUP_32BIT; } } } diff --git a/LibgenDesktop/Models/Utils/FileUtils.cs b/LibgenDesktop/Models/Utils/FileUtils.cs new file mode 100644 index 0000000..cc58480 --- /dev/null +++ b/LibgenDesktop/Models/Utils/FileUtils.cs @@ -0,0 +1,27 @@ +using System; +using System.IO; +using System.Linq; + +namespace LibgenDesktop.Models.Utils +{ + internal static class FileUtils + { + public static string RemoveInvalidFileNameCharacters(string fileName, string emptyFileNameReplacement) + { + if (String.IsNullOrWhiteSpace(fileName)) + { + return emptyFileNameReplacement; + } + string result = fileName.Trim(); + foreach (char invalidChar in Path.GetInvalidFileNameChars()) + { + result = result.Replace(invalidChar, '_'); + } + if (String.IsNullOrEmpty(result) || result.All(c => c == '_')) + { + result = emptyFileNameReplacement; + } + return result; + } + } +} diff --git a/LibgenDesktop/Models/Utils/Formatters.cs b/LibgenDesktop/Models/Utils/Formatters.cs deleted file mode 100644 index bf3330f..0000000 --- a/LibgenDesktop/Models/Utils/Formatters.cs +++ /dev/null @@ -1,45 +0,0 @@ -using System; -using System.Globalization; -using System.Runtime.CompilerServices; -using System.Text; - -namespace LibgenDesktop.Models.Utils -{ - internal static class Formatters - { - private static readonly NumberFormatInfo thousandsSeparatedNumberFormat; - private static readonly string[] fileSizePostfixes; - - static Formatters() - { - thousandsSeparatedNumberFormat = (NumberFormatInfo)CultureInfo.InvariantCulture.NumberFormat.Clone(); - thousandsSeparatedNumberFormat.NumberGroupSeparator = " "; - fileSizePostfixes = new[] { "байт", "КБ", "МБ", "ГБ", "ТБ" }; - } - - public static NumberFormatInfo ThousandsSeparatedNumberFormat => thousandsSeparatedNumberFormat; - - [MethodImpl(MethodImplOptions.AggressiveInlining)] - public static string ToFormattedString(this int value) - { - return value.ToString("N0", ThousandsSeparatedNumberFormat); - } - - [MethodImpl(MethodImplOptions.AggressiveInlining)] - public static string FileSizeToString(long fileSize, bool showBytes) - { - int postfixIndex = fileSize != 0 ? (int)Math.Floor(Math.Log(fileSize) / Math.Log(1024)) : 0; - StringBuilder resultBuilder = new StringBuilder(); - resultBuilder.Append((fileSize / Math.Pow(1024, postfixIndex)).ToString("N2")); - resultBuilder.Append(" "); - resultBuilder.Append(fileSizePostfixes[postfixIndex]); - if (showBytes && postfixIndex != 0) - { - resultBuilder.Append(" ("); - resultBuilder.Append(fileSize.ToString("N0", thousandsSeparatedNumberFormat)); - resultBuilder.Append(" байт)"); - } - return resultBuilder.ToString(); - } - } -} diff --git a/LibgenDesktop/Models/Utils/NetworkUtils.cs b/LibgenDesktop/Models/Utils/NetworkUtils.cs new file mode 100644 index 0000000..9900529 --- /dev/null +++ b/LibgenDesktop/Models/Utils/NetworkUtils.cs @@ -0,0 +1,27 @@ +using System; +using System.Net; +using static LibgenDesktop.Models.Settings.AppSettings; + +namespace LibgenDesktop.Models.Utils +{ + internal static class NetworkUtils + { + public static WebProxy CreateProxy(NetworkSettings networkSettings) + { + WebProxy result; + if (networkSettings.UseProxy) + { + result = new WebProxy(networkSettings.ProxyAddress, networkSettings.ProxyPort.Value); + if (!String.IsNullOrEmpty(networkSettings.ProxyUserName)) + { + result.Credentials = new NetworkCredential(networkSettings.ProxyUserName, networkSettings.ProxyPassword); + } + } + else + { + result = new WebProxy(); + } + return result; + } + } +} diff --git a/LibgenDesktop/Models/Utils/StringExtensions.cs b/LibgenDesktop/Models/Utils/StringExtensions.cs new file mode 100644 index 0000000..d6a0053 --- /dev/null +++ b/LibgenDesktop/Models/Utils/StringExtensions.cs @@ -0,0 +1,12 @@ +using System; + +namespace LibgenDesktop.Models.Utils +{ + internal static class StringExtensions + { + public static bool CompareOrdinalIgnoreCase(this string currentString, string otherString) + { + return String.Compare(currentString, otherString, StringComparison.OrdinalIgnoreCase) == 0; + } + } +} diff --git a/LibgenDesktop/Resources/Languages/English.lng b/LibgenDesktop/Resources/Languages/English.lng new file mode 100644 index 0000000..cd92c68 --- /dev/null +++ b/LibgenDesktop/Resources/Languages/English.lng @@ -0,0 +1,667 @@ +{ + "General": + { + "Name": "English", + "LocalizedName": "US English", + "CultureCode": "en-US", + "TranslatorName": "Put your name here" + }, + "Formatting": + { + "DecimalSeparator": ".", + "ThousandsSeparator": ",", + "DateFormat": "MM/dd/yyyy", + "TimeFormat": "hh:mm:ss tt", + "FileSizePostfixes": + { + "Byte": "bytes", + "Kilobyte": "KB", + "Megabyte": "MB", + "Gigabyte": "GB", + "Terabyte": "TB" + } + }, + "MainWindow": + { + "WindowTitle": "Libgen Desktop", + "MainMenu": + { + "DownloadManagerTooltip": "Download Manager", + "Update": "Update...", + "Import": "Import...", + "Synchronize": "Synchronization...", + "Settings": "Settings" + } + }, + "DatabaseWindow": + { + "WindowTitle": "Libgen Desktop", + "FirstRunMessage": "Добро пожаловать в Libgen Desktop.", + "DatabaseNotFound": "База данных {database} не найдена.", + "DatabaseCorrupted": "База данных {database} повреждена.", + "ChooseOption": "Выберите действие", + "CreateNewDatabase": "Создать новую базу данных", + "OpenExistingDatabase": "Открыть существующую базу данных", + "BrowseNewDatabaseDialogTitle": "Сохранение новой базы данных", + "BrowseExistingDatabaseDialogTitle": "Выбор базы данных", + "Databases": "Базы данных", + "AllFiles": "Все файлы", + "Error": "Ошибка", + "CannotCreateDatabase": "Не удалось создать базу данных.", + "Ok": "OK", + "Cancel": "ОТМЕНА" + }, + "SearchTab": + { + "TabTitle": "Search", + "SearchPlaceHolder": "Search", + "NonFictionSelector": "Non-fiction books", + "FictionSelector": "Fiction books", + "SciMagSelector": "Scientific articles", + "NonFictionSearchBoxTooltip": "Search by title, authors, series, publisher, and ISBN without dashes", + "FictionSearchBoxTooltip": "Search by title, authors, series, publisher, and ISBN with dashes", + "SciMagSearchBoxTooltip": "Search by title, authors, magazine name, DOI, Pubmed ID, and ISSN (p/e)", + "SearchInProgress": "Search in progress...", + "NonFictionSearchProgress": "Books found: {count}", + "FictionSearchProgress": "Books found: {count}", + "SciMagSearchProgress": "Articles found: {count}", + "DatabaseIsEmpty": "The database is empty. Download a database dump from the Library Genesis web site and import it here.", + "ImportButton": "Import" + }, + "SearchResultsTabs": + { + "SearchPlaceHolder": "Поиск", + "SearchInProgress": "Идет поиск...", + "ExportButtonTooltip": "Экспорт результатов поиска в файл" + }, + "NonFictionSearchResultsTab": + { + "SearchBoxTooltip": "Поиск по наименованию, авторам, серии, издателю и ISBN без дефисов", + "SearchProgress": "Найдено книг: {count}", + "StatusBar": "Найдено книг: {count}", + "Columns": + { + "Title": "Наименование", + "Authors": "Авторы", + "Series": "Серия", + "Year": "Год", + "Publisher": "Издатель", + "Format": "Формат", + "FileSize": "Размер файла", + "Ocr": "OCR" + } + }, + "FictionSearchResultsTab": + { + "SearchBoxTooltip": "Поиск по наименованию, авторам, серии, издателю и ISBN с дефисами", + "SearchProgress": "Найдено книг: {count}", + "StatusBar": "Найдено книг: {count}", + "Columns": + { + "Title": "Наименование", + "Authors": "Авторы", + "Series": "Серия", + "Year": "Год", + "Publisher": "Издатель", + "Format": "Формат", + "FileSize": "Размер файла" + } + }, + "SciMagSearchResultsTab": + { + "SearchBoxTooltip": "Поиск по наименованию, авторам, журналу, DOI, Pubmed ID и ISSN (p/e)", + "SearchProgress": "Найдено статей: {count}", + "StatusBar": "Найдено статей: {count}", + "Columns": + { + "Title": "Наименование", + "Authors": "Авторы", + "Magazine": "Журнал", + "Year": "Год", + "FileSize": "Размер файла", + "Doi": "DOI" + } + }, + "DetailsTabs": + { + "CoverIsLoading": "Обложка загружается...", + "NoCover": "Обложка отсутствует", + "NoCoverMirror": "Не выбрано зеркало{new-line}для загрузки обложек", + "NoCoverDueToOfflineMode": "Обложка не загружена, потому что{new-line}включен автономный режим", + "CoverLoadingError": "Не удалось загрузить обложку", + "Yes": "да", + "No": "нет", + "Unknown": "неизвестно", + "Portrait": "портретная", + "Landscape": "альбомная", + "CopyContextMenu": "Копировать \"{text}\"", + "Download": "СКАЧАТЬ", + "DownloadFromMirror": "СКАЧАТЬ С {mirror}", + "Queued": "В ОЧЕРЕДИ", + "Downloading": "ЗАГРУЖАЕТСЯ", + "Stopped": "ЗАГРУЗКА ОСТАНОВЛЕНА", + "Error": "ОШИБКА ЗАГРУЗКИ", + "Open": "ОТКРЫТЬ", + "ErrorMessageTitle": "Ошибка", + "FileNotFoundError": "Файл {file} не найден.", + "NoDownloadMirrorTooltip": "Не выбрано зеркало для загрузки книг", + "OfflineModeIsOnTooltip": "Включен автономный режим", + "Close": "ЗАКРЫТЬ" + }, + "NonFictionDetailsTab": + { + "Title": "Наименование", + "Authors": "Авторы", + "Series": "Серия", + "Publisher": "Издатель", + "Year": "Год", + "Language": "Язык", + "Format": "Формат", + "Isbn": "ISBN", + "Added": "Добавлено", + "LastModified": "Обновлено", + "Library": "Библиотека", + "FileSize": "Размер файла", + "Topics": "Темы", + "Volume": "Том", + "Magazine": "Журнал", + "City": "Город", + "Edition": "Издание", + "Pages": "Страниц", + "BodyMatterPages": "содержательная часть", + "TotalPages": "всего в файле", + "Tags": "Теги", + "Md5Hash": "MD5-хэш", + "Comments": "Комментарий", + "Identifiers": "Идентификаторы", + "LibgenId": "Libgen ID", + "Issn": "ISSN", + "Udc": "UDC", + "Lbc": "LBC", + "Lcc": "LCC", + "Ddc": "DDC", + "Doi": "DOI", + "OpenLibraryId": "OpenLibraryID", + "GoogleBookId": "GoogleID", + "Asin": "ASIN", + "AdditionalAttributes": "Дополнительные атрибуты", + "Dpi": "DPI", + "Ocr": "OCR", + "TableOfContents": "Оглавление", + "Scanned": "Отсканирована", + "Orientation": "Ориентация", + "Paginated": "Постраничная", + "Colored": "Цветная", + "Cleaned": "Вычищенная" + }, + "FictionDetailsTab": + { + "Title": "Наименование", + "Authors": "Авторы", + "RussianAuthor": "Автор (рус.)", + "Series": "Серия", + "Publisher": "Издатель", + "Edition": "Издание", + "Year": "Год", + "Language": "Язык", + "Format": "Формат", + "Pages": "Страниц", + "Version": "Версия", + "FileSize": "Размер файла", + "Added": "Добавлено", + "LastModified": "Обновлено", + "Md5Hash": "MD5-хэш", + "Comments": "Комментарий", + "Identifiers": "Идентификаторы", + "LibgenId": "Libgen ID", + "Isbn": "ISBN", + "GoogleBookId": "Google Books ID", + "Asin": "ASIN" + }, + "SciMagDetailsTab": + { + "Title": "Наименование", + "Authors": "Авторы", + "Magazine": "Журнал", + "Year": "Год", + "Month": "Месяц", + "Day": "День", + "Volume": "Том", + "Issue": "Выпуск", + "Pages": "Страницы", + "FileSize": "Размер файла", + "AddedDateTime": "Добавлено", + "Md5Hash": "MD5-хэш", + "AbstractUrl": "Abstract URL", + "Identifiers": "Идентификаторы", + "LibgenId": "Libgen ID", + "Doi": "DOI", + "Isbn": "ISBN", + "MagazineId": "ID журнала", + "Issnp": "ISSN (p)", + "Issne": "ISSN (e)", + "PubmedId": "Pubmed ID", + "Pmc": "PMC", + "Pii": "PII", + "AdditionalAttributes": "Дополнительные атрибуты", + "Attribute1": "Атрибут 1", + "Attribute2": "Атрибут 2", + "Attribute3": "Атрибут 3", + "Attribute4": "Атрибут 4", + "Attribute5": "Атрибут 5", + "Attribute6": "Атрибут 6" + }, + "Import": + { + "WindowTitle": "Импорт из SQL-дампа", + "BrowseImportFileDialogTitle": "Выбор SQL-дампа", + "AllSupportedFiles": "Все поддерживаемые файлы", + "SqlDumps": "SQL-дампы", + "Archives": "Архивы", + "AllFiles": "Все файлы", + "Elapsed": "Прошло {elapsed}", + "Interrupt": "ПРЕРВАТЬ", + "Interrupting": "ПРЕРЫВАЕТСЯ...", + "Close": "ЗАКРЫТЬ", + "StatusMessages": + { + "Step": "Шаг {current} из {total}.", + "DataLookup": "Поиск данных для импорта", + "CreatingIndexes": "Создание индексов", + "LoadingIds": "Загрузка идентификаторов", + "ImportingData": "Импорт данных", + "ImportComplete": "Импорт завершен", + "ImportCancelled": "Импорт прерван", + "DataNotFound": "Данные не найдены", + "ImportError": "Импорт завершен с ошибками" + }, + "LogMessages": + { + "Step": "Шаг {step}", + "DataLookup": "Поиск данных для импорта в файле", + "Scanning": "Идет сканирование файла...", + "ScannedProgress": "Просканировано {percent}% файла...", + "NonFictionTableFound": "Найдена таблица с нехудожественными книгами.", + "FictionTableFound": "Найдена таблица с художественными книгами.", + "SciMagTableFound": "Найдена таблица с научными статьями.", + "CreatingIndexes": "Создание недостающих индексов", + "CreatingIndexForColumn": "Создается индекс для столбца {column}...", + "LoadingIds": "Загрузка идентификаторов существующих данных", + "LoadingColumnValues": "Загрузка значений столбца {column}...", + "ImportingData": "Импорт данных", + "ImportBooksProgressNoUpdate": "Добавлено книг: {added}.", + "ImportBooksProgressWithUpdate": "Добавлено книг: {added}, обновлено книг: {updated}.", + "ImportArticlesProgressNoUpdate": "Добавлено статей: {added}.", + "ImportArticlesProgressWithUpdate": "Добавлено статей: {added}, обновлено статей: {updated}.", + "ImportSuccessful": "Импорт выполнен успешно.", + "ImportCancelled": "Импорт был прерван пользователем.", + "DataNotFound": "Не найдены данные для импорта.", + "ImportError": "Импорт завершился с ошибками." + } + }, + "ExportPanel": + { + "Header": "Экспорт результатов поиска в файл", + "Format": "Формат", + "Excel": "Файл Microsoft Excel 2007-2016 (XLSX)", + "Csv": "Текстовый файл с разделителями (CSV)", + "Separator": "Разделитель", + "Comma": "Запятая", + "Semicolon": "Точка с запятой", + "Tab": "Знак табуляции", + "SaveAs": "Сохранить в", + "Browse": "Выбрать...", + "BrowseDialogTitle": "Экспортировать результаты поиска", + "ExcelFiles": "Файлы Microsoft Excel", + "CsvFiles": "Файлы CSV", + "TsvFiles": "Файлы TSV", + "AllFiles": "Все файлы", + "ExportRange": "Экспортировать", + "NoLimit": "все", + "Limit": "только первые {count} результатов", + "Export": "ЭКСПОРТ", + "Cancel": "ОТМЕНА", + "SavingFile": "Сохранение файла.", + "RowCountSingleFile": "Экспортировано строк: {rows}.", + "RowCountMultipleFiles": "Экспортировано строк: {rows}, файлов создано: {files}.", + "ErrorWarningTitle": "Ошибка", + "InvalidExportPath": "Указанный путь файла для экспорта некорректен.", + "DirectoryNotFound": "Директория {directory} не существует.", + "InvalidExportFileName": "Указанное имя файла для экспорта некорректно.", + "OverwritePromptTitle": "Перезаписать файл?", + "OverwritePromptText": "Файл {file} существует. Вы действительно хотите перезаписать его?", + "RowLimitWarningTitle": "Предел количества строк", + "RowLimitWarningText": "Достигнут предел количества строк, допустимый для записи в файл Microsoft Excel. Дальнейший экспорт невозможен. Для экспорта большего числа строк включите опцию \"Делить на несколько файлов\" в настройках программы.", + "ExportError": "Экспорт завершился с ошибкой.", + "Interrupt": "ПРЕРВАТЬ", + "Interrupting": "ПРЕРЫВАЕТСЯ...", + "ExportInterrupted": "Экспорт прерван.", + "Results": "ПОСМОТРЕТЬ РЕЗУЛЬТАТ", + "Close": "ЗАКРЫТЬ" + }, + "Exporter": + { + "Yes": "да", + "No": "нет", + "Unknown": "неизвестно", + "Portrait": "портретная", + "Landscape": "альбомная", + "NonFictionColumns": + { + "Id": "ID", + "Title": "Наименование", + "Authors": "Авторы", + "Series": "Серия", + "Publisher": "Издатель", + "Year": "Год", + "Language": "Язык", + "Format": "Формат", + "Isbn": "ISBN", + "Added": "Добавлено", + "LastModified": "Обновлено", + "Library": "Библиотека", + "FileSize": "Размер файла", + "Topics": "Темы", + "Volume": "Том", + "Magazine": "Журнал", + "City": "Город", + "Edition": "Издание", + "BodyMatterPages": "Страниц (содержательная часть)", + "TotalPages": "Страниц (всего в файле)", + "Tags": "Теги", + "Md5Hash": "MD5-хэш", + "Comments": "Комментарий", + "LibgenId": "Libgen ID", + "Issn": "ISSN", + "Udc": "UDC", + "Lbc": "LBC", + "Lcc": "LCC", + "Ddc": "DDC", + "Doi": "DOI", + "OpenLibraryId": "OpenLibraryID", + "GoogleBookId": "GoogleID", + "Asin": "ASIN", + "Dpi": "DPI", + "Ocr": "OCR", + "TableOfContents": "Оглавление", + "Scanned": "Отсканирована", + "Orientation": "Ориентация", + "Paginated": "Постраничная", + "Colored": "Цветная", + "Cleaned": "Вычищенная" + }, + "FictionColumns": + { + "Id": "ID", + "Title": "Наименование", + "Authors": "Авторы", + "RussianAuthor": "Автор (рус.)", + "Series": "Серия", + "Publisher": "Издатель", + "Edition": "Издание", + "Year": "Год", + "Language": "Язык", + "Format": "Формат", + "Pages": "Страниц", + "Version": "Версия", + "FileSize": "Размер файла", + "Added": "Добавлено", + "LastModified": "Обновлено", + "Md5Hash": "MD5-хэш", + "Comments": "Комментарий", + "LibgenId": "Libgen ID", + "Isbn": "ISBN", + "GoogleBookId": "Google Books ID", + "Asin": "ASIN" + }, + "SciMagColumns": + { + "Id": "ID", + "Title": "Наименование", + "Authors": "Авторы", + "Magazine": "Журнал", + "Year": "Год", + "Month": "Месяц", + "Day": "День", + "Volume": "Том", + "Issue": "Выпуск", + "Pages": "Страницы", + "FileSize": "Размер файла", + "AddedDateTime": "Добавлено", + "Md5Hash": "MD5-хэш", + "AbstractUrl": "Abstract URL", + "LibgenId": "Libgen ID", + "Doi1": "DOI 1", + "Doi2": "DOI 2", + "Isbn": "ISBN", + "MagazineId": "ID журнала", + "Issnp": "ISSN (p)", + "Issne": "ISSN (e)", + "PubmedId": "Pubmed ID", + "Pmc": "PMC", + "Pii": "PII", + "Attribute1": "Атрибут 1", + "Attribute2": "Атрибут 2", + "Attribute3": "Атрибут 3", + "Attribute4": "Атрибут 4", + "Attribute5": "Атрибут 5", + "Attribute6": "Атрибут 6" + } + }, + "Synchronization": + { + "WindowTitle": "Синхронизация списка нехудожественной литературы", + "ErrorMessageTitle": "Ошибка", + "ImportRequired": "Перед синхронизацией списка нехудожественной литературы необходимо выполнить импорт из дампа базы данных (пункт \"Импорт\" в меню).", + "NoSynchronizationMirror": "Не выбрано зеркало для синхронизации списка нехудожественной литературы.", + "OfflineModePromptTitle": "Автономный режим", + "OfflineModePromptText": "Синхронизация невозможна при включенном автономном режиме. Выключить автономный режим?", + "Elapsed": "Прошло {elapsed}", + "Interrupt": "ПРЕРВАТЬ", + "Interrupting": "ПРЕРЫВАЕТСЯ...", + "Close": "ЗАКРЫТЬ", + "StatusMessages": + { + "Step": "Шаг {current} из {total}.", + "Preparation": "Подготовка к синхронизации", + "CreatingIndexes": "Создание индексов", + "LoadingIds": "Загрузка идентификаторов", + "SynchronizingData": "Синхронизация", + "SynchronizationComplete": "Синхронизация завершена", + "SynchronizationCancelled": "Синхронизация прервана", + "SynchronizationError": "Синхронизация завершилась с ошибками" + }, + "LogMessages": + { + "Step": "Шаг {step}", + "CreatingIndexes": "Создание недостающих индексов", + "CreatingIndexForColumn": "Создается индекс для столбца {column}...", + "LoadingIds": "Загрузка идентификаторов существующих данных", + "LoadingColumnValues": "Загрузка значений столбца {column}...", + "SynchronizingBookList": "Синхронизация списка книг", + "DownloadingNewBooks": "Скачивание информации о новых книгах", + "SynchronizationProgressNoAddedNoUpdated": "Скачано книг: {downloaded}.", + "SynchronizationProgressAdded": "Скачано книг: {downloaded}, добавлено книг: {added}.", + "SynchronizationProgressUpdated": "Скачано книг: {downloaded}, обновлено книг: {updated}.", + "SynchronizationProgressAddedAndUpdated": "Скачано книг: {downloaded}, добавлено книг: {added}, обновлено книг: {updated}.", + "SynchronizationSuccessful": "Синхронизация выполнена успешно.", + "SynchronizationCancelled": "Синхронизация была прервана пользователем.", + "SynchronizationError": "Синхронизация завершилась с ошибками." + } + }, + "DownloadManager": + { + "TabTitle": "Загрузки", + "Start": "ЗАПУСТИТЬ", + "Stop": "ОСТАНОВИТЬ", + "Remove": "УДАЛИТЬ", + "StartAll": "ЗАПУСТИТЬ ВСЕ", + "StopAll": "ОСТАНОВИТЬ ВСЕ", + "RemoveCompleted": "УДАЛИТЬ ЗАВЕРШЕННЫЕ", + "QueuedStatus": "В очереди", + "DownloadingStatus": "Загружается", + "StoppedStatus": "Остановлено", + "RetryDelayStatus": "Пауза", + "ErrorStatus": "Ошибка", + "DownloadProgressKnownFileSize": "Загружено: {downloaded} из {total} байт ({percent}%)", + "DownloadProgressUnknownFileSize": "Загружено: {downloaded} из неизвестно", + "Log": "Журнал", + "TechnicalDetails": "Технические детали", + "Copy": "Копировать", + "FileNotFoundErrorTitle": "Ошибка", + "FileNotFoundErrorText": "Файл {file} не найден.", + "LogMessages": + { + "Queued": "Загрузка добавлена в очередь.", + "Started": "Загрузка начата.", + "Stopped": "Загрузка остановлена.", + "RetryDelay": "Пауза {count} секунд...", + "Completed": "Загрузка завершена.", + "OfflineModeIsOn": "Включен автономный режим.", + "TransformationError": "Трансформация {transformation} завершилась с ошибкой.", + "TransformationReturnedIncorrectUrl": "Трансформация {transformation} вернула недопустимый URL.", + "Attempt": "Попытка {current} из {total}.", + "MaximumDownloadAttempts": "Достигнуто максимальное количество попыток загрузки.", + "DownloadingPage": "Загружается страница: {url}", + "DownloadingFile": "Загружается файл: {url}", + "StartingFileDownloadKnownFileSize": "Загрузка файла начата, размер файла: {size} байт.", + "StartingFileDownloadUnknownFileSize": "Загрузка файла начата, размер файла: неизвестно.", + "ResumingFileDownloadKnownFileSize": "Загрузка файла возобновлена, осталось загрузить: {remaining} байт.", + "ResumingFileDownloadUnknownFileSize": "Загрузка файла возобновлена, осталось загрузить: неизвестно.", + "Request": "Запрос", + "Response": "Ответ сервера", + "Redirect": "Переадресация на {url}", + "TooManyRedirects": "Сервер выполнил слишком много попыток переадресации.", + "NonSuccessfulStatusCode": "Сервер вернул код {status}.", + "CannotCreateDownloadDirectory": "Не удалось создать директорию {directory}", + "CannotCreateOrOpenFile": "Не удалось создать или открыть файл {file}", + "CannotRenamePartFile": "Ошибка переименования файла {source} в {destination}.", + "HtmlPageReturned": "Сервер вернул HTML-страницу вместо файла.", + "NoPartialDownloadSupport": "Сервер не поддерживает докачку.", + "NoContentLengthWarning": "Предупреждение: сервер не указал размер файла.", + "ServerResponseTimeout": "Превышено время ожидания ответа от сервера.", + "DownloadIncompleteError": "Загрузка завершена не полностью.", + "FileWriteError": "Ошибка записи файла на диск.", + "UnexpectedError": "Возникла непредвиденная ошибка." + } + }, + "ApplicationUpdate": + { + "WindowTitle": "Обновление программы", + "UpdateAvailable": "Доступно обновление Libgen Desktop", + "NewVersion": "Новая версия: {version} от {date}", + "Download": "СКАЧАТЬ", + "DownloadAndInstall": "СКАЧАТЬ И УСТАНОВИТЬ", + "SkipThisVersion": "ПРОПУСТИТЬ ЭТУ ВЕРСИЮ", + "Cancel": "ОТМЕНА", + "Interrupt": "ПРЕРВАТЬ", + "Interrupting": "ПРЕРЫВАЕТСЯ...", + "InterruptPromptTitle": "Прервать загрузку?", + "InterruptPromptText": "Прервать загрузку обновления?", + "Error": "Ошибка", + "IncompleteDownload": "Файл обновления не был загружен полностью.", + "Close": "ЗАКРЫТЬ" + }, + "Settings": + { + "WindowTitle": "Настройки", + "Ok": "OK", + "Cancel": "ОТМЕНА", + "DiscardChangesPromptTitle": "Отменить изменения?", + "DiscardChangesPromptText": "Настройки были изменены. Вы действительно хотите отменить сделанные изменения?", + "General": + { + "TabHeader": "Общие", + "Language": "Язык интерфейса", + "CheckUpdates": "Проверять обновления программы", + "UpdateCheckIntervals": + { + "Never": "никогда", + "Daily": "один раз в день", + "Weekly": "один раз в неделю", + "Monthly": "один раз в месяц" + } + }, + "Network": + { + "TabHeader": "Сеть", + "OfflineMode": "Автономный режим", + "UseHttpProxy": "Использовать HTTP прокси-сервер", + "ProxyAddress": "Адрес", + "ProxyAddressRequired": "Обязательное поле", + "ProxyPort": "Порт", + "ProxyPortValidation": "Числа от {min} до {max}", + "ProxyUserName": "Имя пользователя", + "ProxyPassword": "Пароль", + "ProxyPasswordWarning": "Хранить здесь пароль небезопасно." + }, + "Download": + { + "TabHeader": "Загрузки", + "DownloadMode": "При скачивании книги / статьи", + "OpenInBrowser": "открывать ссылку на скачивание в браузере", + "UseDownloadManager": "использовать встроенный менеджер загрузок", + "DownloadDirectory": "Загружать в", + "BrowseDirectoryDialogTitle": "Директория для загруженных файлов", + "DownloadDirectoryNotFound": "Директория не существует", + "Timeout": "Таймаут загрузки", + "TimeoutValidation": "Числа от {min} до {max}", + "Seconds": "секунд", + "DownloadAttempts": "Количество попыток", + "DownloadAttemptsValidation": "Числа от {min} до {max}", + "Times": "раз", + "RetryDelay": "Пауза между попытками", + "RetryDelayValidation": "Числа от {min} до {max}" + }, + "Mirrors": + { + "TabHeader": "Зеркала", + "NonFiction": "Нехудожественная литература", + "Fiction": "Художественная литература", + "SciMagArticles": "Научные статьи", + "Books": "Книги", + "Articles": "Статьи", + "Covers": "Обложки", + "Synchronization": "Синхронизация", + "NoMirror": "нет" + }, + "Search": + { + "TabHeader": "Поиск", + "LimitResults": "Ограничивать количество результатов", + "MaximumResults": "Максимальное количество результатов", + "PositiveNumbersOnly": "Только положительные числа", + "OpenDetails": "Открывать детали книги / статьи", + "InModalWindow": "в модальном окне", + "InNonModalWindow": "в немодальном окне", + "InNewTab": "в новой вкладке" + }, + "Export": + { + "TabHeader": "Экспорт", + "OpenResults": "Открывать результат после экспорта", + "SplitIntoMultipleFiles": "Делить на несколько файлов", + "MaximumRowsPerFile": "Максимальное количество строк в файле", + "MaximumRowsPerFileValidation": "Числа от {min} до {max}", + "ExcelLimitNote": "Примечание: Microsoft Excel не поддерживает файлы более {count} строк." + }, + "Advanced": + { + "TabHeader": "Дополнительно", + "UseLogging": "Записывать отладочную информацию в файл" + } + }, + "MessageBox": + { + "Ok": "OK", + "Yes": "ДА", + "No": "НЕТ" + }, + "ErrorWindow": + { + "WindowTitle": "Ошибка", + "UnexpectedError": "Возникла непредвиденная ошибка", + "Copy": "СКОПИРОВАТЬ В БУФЕР ОБМЕНА", + "Close": "ЗАКРЫТЬ" + } +} diff --git a/LibgenDesktop/Resources/Languages/Russian.lng b/LibgenDesktop/Resources/Languages/Russian.lng new file mode 100644 index 0000000..157ded4 --- /dev/null +++ b/LibgenDesktop/Resources/Languages/Russian.lng @@ -0,0 +1,667 @@ +{ + "General": + { + "Name": "Russian", + "LocalizedName": "русский", + "CultureCode": "ru-RU", + "TranslatorName": "libgenapps" + }, + "Formatting": + { + "DecimalSeparator": ",", + "ThousandsSeparator": " ", + "DateFormat": "dd.MM.yyyy", + "TimeFormat": "HH:mm:ss", + "FileSizePostfixes": + { + "Byte": "байт", + "Kilobyte": "КБ", + "Megabyte": "МБ", + "Gigabyte": "ГБ", + "Terabyte": "ТБ" + } + }, + "MainWindow": + { + "WindowTitle": "Libgen Desktop", + "MainMenu": + { + "DownloadManagerTooltip": "Менеджер загрузок", + "Update": "Обновление...", + "Import": "Импорт...", + "Synchronize": "Синхронизация...", + "Settings": "Настройки" + } + }, + "DatabaseWindow": + { + "WindowTitle": "Libgen Desktop", + "FirstRunMessage": "Добро пожаловать в Libgen Desktop.", + "DatabaseNotFound": "База данных {database} не найдена.", + "DatabaseCorrupted": "База данных {database} повреждена.", + "ChooseOption": "Выберите действие", + "CreateNewDatabase": "Создать новую базу данных", + "OpenExistingDatabase": "Открыть существующую базу данных", + "BrowseNewDatabaseDialogTitle": "Сохранение новой базы данных", + "BrowseExistingDatabaseDialogTitle": "Выбор базы данных", + "Databases": "Базы данных", + "AllFiles": "Все файлы", + "Error": "Ошибка", + "CannotCreateDatabase": "Не удалось создать базу данных.", + "Ok": "OK", + "Cancel": "ОТМЕНА" + }, + "SearchTab": + { + "TabTitle": "Поиск", + "SearchPlaceHolder": "Поиск", + "NonFictionSelector": "Нехудожественная литература", + "FictionSelector": "Художественная литература", + "SciMagSelector": "Научные статьи", + "NonFictionSearchBoxTooltip": "Поиск по наименованию, авторам, серии, издателю и ISBN без дефисов", + "FictionSearchBoxTooltip": "Поиск по наименованию, авторам, серии, издателю и ISBN с дефисами", + "SciMagSearchBoxTooltip": "Поиск по наименованию, авторам, журналу, DOI, Pubmed ID и ISSN (p/e)", + "SearchInProgress": "Идет поиск...", + "NonFictionSearchProgress": "Найдено книг: {count}", + "FictionSearchProgress": "Найдено книг: {count}", + "SciMagSearchProgress": "Найдено статей: {count}", + "DatabaseIsEmpty": "База данных пуста. Скачайте дамп базы данных с сайта Library Genesis, а затем импортируйте его.", + "ImportButton": "Импортировать" + }, + "SearchResultsTabs": + { + "SearchPlaceHolder": "Поиск", + "SearchInProgress": "Идет поиск...", + "ExportButtonTooltip": "Экспорт результатов поиска в файл" + }, + "NonFictionSearchResultsTab": + { + "SearchBoxTooltip": "Поиск по наименованию, авторам, серии, издателю и ISBN без дефисов", + "SearchProgress": "Найдено книг: {count}", + "StatusBar": "Найдено книг: {count}", + "Columns": + { + "Title": "Наименование", + "Authors": "Авторы", + "Series": "Серия", + "Year": "Год", + "Publisher": "Издатель", + "Format": "Формат", + "FileSize": "Размер файла", + "Ocr": "OCR" + } + }, + "FictionSearchResultsTab": + { + "SearchBoxTooltip": "Поиск по наименованию, авторам, серии, издателю и ISBN с дефисами", + "SearchProgress": "Найдено книг: {count}", + "StatusBar": "Найдено книг: {count}", + "Columns": + { + "Title": "Наименование", + "Authors": "Авторы", + "Series": "Серия", + "Year": "Год", + "Publisher": "Издатель", + "Format": "Формат", + "FileSize": "Размер файла" + } + }, + "SciMagSearchResultsTab": + { + "SearchBoxTooltip": "Поиск по наименованию, авторам, журналу, DOI, Pubmed ID и ISSN (p/e)", + "SearchProgress": "Найдено статей: {count}", + "StatusBar": "Найдено статей: {count}", + "Columns": + { + "Title": "Наименование", + "Authors": "Авторы", + "Magazine": "Журнал", + "Year": "Год", + "FileSize": "Размер файла", + "Doi": "DOI" + } + }, + "DetailsTabs": + { + "CoverIsLoading": "Обложка загружается...", + "NoCover": "Обложка отсутствует", + "NoCoverMirror": "Не выбрано зеркало{new-line}для загрузки обложек", + "NoCoverDueToOfflineMode": "Обложка не загружена, потому что{new-line}включен автономный режим", + "CoverLoadingError": "Не удалось загрузить обложку", + "Yes": "да", + "No": "нет", + "Unknown": "неизвестно", + "Portrait": "портретная", + "Landscape": "альбомная", + "CopyContextMenu": "Копировать \"{text}\"", + "Download": "СКАЧАТЬ", + "DownloadFromMirror": "СКАЧАТЬ С {mirror}", + "Queued": "В ОЧЕРЕДИ", + "Downloading": "ЗАГРУЖАЕТСЯ", + "Stopped": "ЗАГРУЗКА ОСТАНОВЛЕНА", + "Error": "ОШИБКА ЗАГРУЗКИ", + "Open": "ОТКРЫТЬ", + "ErrorMessageTitle": "Ошибка", + "FileNotFoundError": "Файл {file} не найден.", + "NoDownloadMirrorTooltip": "Не выбрано зеркало для загрузки книг", + "OfflineModeIsOnTooltip": "Включен автономный режим", + "Close": "ЗАКРЫТЬ" + }, + "NonFictionDetailsTab": + { + "Title": "Наименование", + "Authors": "Авторы", + "Series": "Серия", + "Publisher": "Издатель", + "Year": "Год", + "Language": "Язык", + "Format": "Формат", + "Isbn": "ISBN", + "Added": "Добавлено", + "LastModified": "Обновлено", + "Library": "Библиотека", + "FileSize": "Размер файла", + "Topics": "Темы", + "Volume": "Том", + "Magazine": "Журнал", + "City": "Город", + "Edition": "Издание", + "Pages": "Страниц", + "BodyMatterPages": "содержательная часть", + "TotalPages": "всего в файле", + "Tags": "Теги", + "Md5Hash": "MD5-хэш", + "Comments": "Комментарий", + "Identifiers": "Идентификаторы", + "LibgenId": "Libgen ID", + "Issn": "ISSN", + "Udc": "UDC", + "Lbc": "LBC", + "Lcc": "LCC", + "Ddc": "DDC", + "Doi": "DOI", + "OpenLibraryId": "OpenLibraryID", + "GoogleBookId": "GoogleID", + "Asin": "ASIN", + "AdditionalAttributes": "Дополнительные атрибуты", + "Dpi": "DPI", + "Ocr": "OCR", + "TableOfContents": "Оглавление", + "Scanned": "Отсканирована", + "Orientation": "Ориентация", + "Paginated": "Постраничная", + "Colored": "Цветная", + "Cleaned": "Вычищенная" + }, + "FictionDetailsTab": + { + "Title": "Наименование", + "Authors": "Авторы", + "RussianAuthor": "Автор (рус.)", + "Series": "Серия", + "Publisher": "Издатель", + "Edition": "Издание", + "Year": "Год", + "Language": "Язык", + "Format": "Формат", + "Pages": "Страниц", + "Version": "Версия", + "FileSize": "Размер файла", + "Added": "Добавлено", + "LastModified": "Обновлено", + "Md5Hash": "MD5-хэш", + "Comments": "Комментарий", + "Identifiers": "Идентификаторы", + "LibgenId": "Libgen ID", + "Isbn": "ISBN", + "GoogleBookId": "Google Books ID", + "Asin": "ASIN" + }, + "SciMagDetailsTab": + { + "Title": "Наименование", + "Authors": "Авторы", + "Magazine": "Журнал", + "Year": "Год", + "Month": "Месяц", + "Day": "День", + "Volume": "Том", + "Issue": "Выпуск", + "Pages": "Страницы", + "FileSize": "Размер файла", + "AddedDateTime": "Добавлено", + "Md5Hash": "MD5-хэш", + "AbstractUrl": "Abstract URL", + "Identifiers": "Идентификаторы", + "LibgenId": "Libgen ID", + "Doi": "DOI", + "Isbn": "ISBN", + "MagazineId": "ID журнала", + "Issnp": "ISSN (p)", + "Issne": "ISSN (e)", + "PubmedId": "Pubmed ID", + "Pmc": "PMC", + "Pii": "PII", + "AdditionalAttributes": "Дополнительные атрибуты", + "Attribute1": "Атрибут 1", + "Attribute2": "Атрибут 2", + "Attribute3": "Атрибут 3", + "Attribute4": "Атрибут 4", + "Attribute5": "Атрибут 5", + "Attribute6": "Атрибут 6" + }, + "Import": + { + "WindowTitle": "Импорт из SQL-дампа", + "BrowseImportFileDialogTitle": "Выбор SQL-дампа", + "AllSupportedFiles": "Все поддерживаемые файлы", + "SqlDumps": "SQL-дампы", + "Archives": "Архивы", + "AllFiles": "Все файлы", + "Elapsed": "Прошло {elapsed}", + "Interrupt": "ПРЕРВАТЬ", + "Interrupting": "ПРЕРЫВАЕТСЯ...", + "Close": "ЗАКРЫТЬ", + "StatusMessages": + { + "Step": "Шаг {current} из {total}.", + "DataLookup": "Поиск данных для импорта", + "CreatingIndexes": "Создание индексов", + "LoadingIds": "Загрузка идентификаторов", + "ImportingData": "Импорт данных", + "ImportComplete": "Импорт завершен", + "ImportCancelled": "Импорт прерван", + "DataNotFound": "Данные не найдены", + "ImportError": "Импорт завершен с ошибками" + }, + "LogMessages": + { + "Step": "Шаг {step}", + "DataLookup": "Поиск данных для импорта в файле", + "Scanning": "Идет сканирование файла...", + "ScannedProgress": "Просканировано {percent}% файла...", + "NonFictionTableFound": "Найдена таблица с нехудожественными книгами.", + "FictionTableFound": "Найдена таблица с художественными книгами.", + "SciMagTableFound": "Найдена таблица с научными статьями.", + "CreatingIndexes": "Создание недостающих индексов", + "CreatingIndexForColumn": "Создается индекс для столбца {column}...", + "LoadingIds": "Загрузка идентификаторов существующих данных", + "LoadingColumnValues": "Загрузка значений столбца {column}...", + "ImportingData": "Импорт данных", + "ImportBooksProgressNoUpdate": "Добавлено книг: {added}.", + "ImportBooksProgressWithUpdate": "Добавлено книг: {added}, обновлено книг: {updated}.", + "ImportArticlesProgressNoUpdate": "Добавлено статей: {added}.", + "ImportArticlesProgressWithUpdate": "Добавлено статей: {added}, обновлено статей: {updated}.", + "ImportSuccessful": "Импорт выполнен успешно.", + "ImportCancelled": "Импорт был прерван пользователем.", + "DataNotFound": "Не найдены данные для импорта.", + "ImportError": "Импорт завершился с ошибками." + } + }, + "ExportPanel": + { + "Header": "Экспорт результатов поиска в файл", + "Format": "Формат", + "Excel": "Файл Microsoft Excel 2007-2016 (XLSX)", + "Csv": "Текстовый файл с разделителями (CSV)", + "Separator": "Разделитель", + "Comma": "Запятая", + "Semicolon": "Точка с запятой", + "Tab": "Знак табуляции", + "SaveAs": "Сохранить в", + "Browse": "Выбрать...", + "BrowseDialogTitle": "Экспортировать результаты поиска", + "ExcelFiles": "Файлы Microsoft Excel", + "CsvFiles": "Файлы CSV", + "TsvFiles": "Файлы TSV", + "AllFiles": "Все файлы", + "ExportRange": "Экспортировать", + "NoLimit": "все", + "Limit": "только первые {count} результатов", + "Export": "ЭКСПОРТ", + "Cancel": "ОТМЕНА", + "SavingFile": "Сохранение файла.", + "RowCountSingleFile": "Экспортировано строк: {rows}.", + "RowCountMultipleFiles": "Экспортировано строк: {rows}, файлов создано: {files}.", + "ErrorWarningTitle": "Ошибка", + "InvalidExportPath": "Указанный путь файла для экспорта некорректен.", + "DirectoryNotFound": "Директория {directory} не существует.", + "InvalidExportFileName": "Указанное имя файла для экспорта некорректно.", + "OverwritePromptTitle": "Перезаписать файл?", + "OverwritePromptText": "Файл {file} существует. Вы действительно хотите перезаписать его?", + "RowLimitWarningTitle": "Предел количества строк", + "RowLimitWarningText": "Достигнут предел количества строк, допустимый для записи в файл Microsoft Excel. Дальнейший экспорт невозможен. Для экспорта большего числа строк включите опцию \"Делить на несколько файлов\" в настройках программы.", + "ExportError": "Экспорт завершился с ошибкой.", + "Interrupt": "ПРЕРВАТЬ", + "Interrupting": "ПРЕРЫВАЕТСЯ...", + "ExportInterrupted": "Экспорт прерван.", + "Results": "ПОСМОТРЕТЬ РЕЗУЛЬТАТ", + "Close": "ЗАКРЫТЬ" + }, + "Exporter": + { + "Yes": "да", + "No": "нет", + "Unknown": "неизвестно", + "Portrait": "портретная", + "Landscape": "альбомная", + "NonFictionColumns": + { + "Id": "ID", + "Title": "Наименование", + "Authors": "Авторы", + "Series": "Серия", + "Publisher": "Издатель", + "Year": "Год", + "Language": "Язык", + "Format": "Формат", + "Isbn": "ISBN", + "Added": "Добавлено", + "LastModified": "Обновлено", + "Library": "Библиотека", + "FileSize": "Размер файла", + "Topics": "Темы", + "Volume": "Том", + "Magazine": "Журнал", + "City": "Город", + "Edition": "Издание", + "BodyMatterPages": "Страниц (содержательная часть)", + "TotalPages": "Страниц (всего в файле)", + "Tags": "Теги", + "Md5Hash": "MD5-хэш", + "Comments": "Комментарий", + "LibgenId": "Libgen ID", + "Issn": "ISSN", + "Udc": "UDC", + "Lbc": "LBC", + "Lcc": "LCC", + "Ddc": "DDC", + "Doi": "DOI", + "OpenLibraryId": "OpenLibraryID", + "GoogleBookId": "GoogleID", + "Asin": "ASIN", + "Dpi": "DPI", + "Ocr": "OCR", + "TableOfContents": "Оглавление", + "Scanned": "Отсканирована", + "Orientation": "Ориентация", + "Paginated": "Постраничная", + "Colored": "Цветная", + "Cleaned": "Вычищенная" + }, + "FictionColumns": + { + "Id": "ID", + "Title": "Наименование", + "Authors": "Авторы", + "RussianAuthor": "Автор (рус.)", + "Series": "Серия", + "Publisher": "Издатель", + "Edition": "Издание", + "Year": "Год", + "Language": "Язык", + "Format": "Формат", + "Pages": "Страниц", + "Version": "Версия", + "FileSize": "Размер файла", + "Added": "Добавлено", + "LastModified": "Обновлено", + "Md5Hash": "MD5-хэш", + "Comments": "Комментарий", + "LibgenId": "Libgen ID", + "Isbn": "ISBN", + "GoogleBookId": "Google Books ID", + "Asin": "ASIN" + }, + "SciMagColumns": + { + "Id": "ID", + "Title": "Наименование", + "Authors": "Авторы", + "Magazine": "Журнал", + "Year": "Год", + "Month": "Месяц", + "Day": "День", + "Volume": "Том", + "Issue": "Выпуск", + "Pages": "Страницы", + "FileSize": "Размер файла", + "AddedDateTime": "Добавлено", + "Md5Hash": "MD5-хэш", + "AbstractUrl": "Abstract URL", + "LibgenId": "Libgen ID", + "Doi1": "DOI 1", + "Doi2": "DOI 2", + "Isbn": "ISBN", + "MagazineId": "ID журнала", + "Issnp": "ISSN (p)", + "Issne": "ISSN (e)", + "PubmedId": "Pubmed ID", + "Pmc": "PMC", + "Pii": "PII", + "Attribute1": "Атрибут 1", + "Attribute2": "Атрибут 2", + "Attribute3": "Атрибут 3", + "Attribute4": "Атрибут 4", + "Attribute5": "Атрибут 5", + "Attribute6": "Атрибут 6" + } + }, + "Synchronization": + { + "WindowTitle": "Синхронизация списка нехудожественной литературы", + "ErrorMessageTitle": "Ошибка", + "ImportRequired": "Перед синхронизацией списка нехудожественной литературы необходимо выполнить импорт из дампа базы данных (пункт \"Импорт\" в меню).", + "NoSynchronizationMirror": "Не выбрано зеркало для синхронизации списка нехудожественной литературы.", + "OfflineModePromptTitle": "Автономный режим", + "OfflineModePromptText": "Синхронизация невозможна при включенном автономном режиме. Выключить автономный режим?", + "Elapsed": "Прошло {elapsed}", + "Interrupt": "ПРЕРВАТЬ", + "Interrupting": "ПРЕРЫВАЕТСЯ...", + "Close": "ЗАКРЫТЬ", + "StatusMessages": + { + "Step": "Шаг {current} из {total}.", + "Preparation": "Подготовка к синхронизации", + "CreatingIndexes": "Создание индексов", + "LoadingIds": "Загрузка идентификаторов", + "SynchronizingData": "Синхронизация", + "SynchronizationComplete": "Синхронизация завершена", + "SynchronizationCancelled": "Синхронизация прервана", + "SynchronizationError": "Синхронизация завершилась с ошибками" + }, + "LogMessages": + { + "Step": "Шаг {step}", + "CreatingIndexes": "Создание недостающих индексов", + "CreatingIndexForColumn": "Создается индекс для столбца {column}...", + "LoadingIds": "Загрузка идентификаторов существующих данных", + "LoadingColumnValues": "Загрузка значений столбца {column}...", + "SynchronizingBookList": "Синхронизация списка книг", + "DownloadingNewBooks": "Скачивание информации о новых книгах", + "SynchronizationProgressNoAddedNoUpdated": "Скачано книг: {downloaded}.", + "SynchronizationProgressAdded": "Скачано книг: {downloaded}, добавлено книг: {added}.", + "SynchronizationProgressUpdated": "Скачано книг: {downloaded}, обновлено книг: {updated}.", + "SynchronizationProgressAddedAndUpdated": "Скачано книг: {downloaded}, добавлено книг: {added}, обновлено книг: {updated}.", + "SynchronizationSuccessful": "Синхронизация выполнена успешно.", + "SynchronizationCancelled": "Синхронизация была прервана пользователем.", + "SynchronizationError": "Синхронизация завершилась с ошибками." + } + }, + "DownloadManager": + { + "TabTitle": "Загрузки", + "Start": "ЗАПУСТИТЬ", + "Stop": "ОСТАНОВИТЬ", + "Remove": "УДАЛИТЬ", + "StartAll": "ЗАПУСТИТЬ ВСЕ", + "StopAll": "ОСТАНОВИТЬ ВСЕ", + "RemoveCompleted": "УДАЛИТЬ ЗАВЕРШЕННЫЕ", + "QueuedStatus": "В очереди", + "DownloadingStatus": "Загружается", + "StoppedStatus": "Остановлено", + "RetryDelayStatus": "Пауза", + "ErrorStatus": "Ошибка", + "DownloadProgressKnownFileSize": "Загружено: {downloaded} из {total} байт ({percent}%)", + "DownloadProgressUnknownFileSize": "Загружено: {downloaded} из неизвестно", + "Log": "Журнал", + "TechnicalDetails": "Технические детали", + "Copy": "Копировать", + "FileNotFoundErrorTitle": "Ошибка", + "FileNotFoundErrorText": "Файл {file} не найден.", + "LogMessages": + { + "Queued": "Загрузка добавлена в очередь.", + "Started": "Загрузка начата.", + "Stopped": "Загрузка остановлена.", + "RetryDelay": "Пауза {count} секунд...", + "Completed": "Загрузка завершена.", + "OfflineModeIsOn": "Включен автономный режим.", + "TransformationError": "Трансформация {transformation} завершилась с ошибкой.", + "TransformationReturnedIncorrectUrl": "Трансформация {transformation} вернула недопустимый URL.", + "Attempt": "Попытка {current} из {total}.", + "MaximumDownloadAttempts": "Достигнуто максимальное количество попыток загрузки.", + "DownloadingPage": "Загружается страница: {url}", + "DownloadingFile": "Загружается файл: {url}", + "StartingFileDownloadKnownFileSize": "Загрузка файла начата, размер файла: {size} байт.", + "StartingFileDownloadUnknownFileSize": "Загрузка файла начата, размер файла: неизвестно.", + "ResumingFileDownloadKnownFileSize": "Загрузка файла возобновлена, осталось загрузить: {remaining} байт.", + "ResumingFileDownloadUnknownFileSize": "Загрузка файла возобновлена, осталось загрузить: неизвестно.", + "Request": "Запрос", + "Response": "Ответ сервера", + "Redirect": "Переадресация на {url}", + "TooManyRedirects": "Сервер выполнил слишком много попыток переадресации.", + "NonSuccessfulStatusCode": "Сервер вернул код {status}.", + "CannotCreateDownloadDirectory": "Не удалось создать директорию {directory}", + "CannotCreateOrOpenFile": "Не удалось создать или открыть файл {file}", + "CannotRenamePartFile": "Ошибка переименования файла {source} в {destination}.", + "HtmlPageReturned": "Сервер вернул HTML-страницу вместо файла.", + "NoPartialDownloadSupport": "Сервер не поддерживает докачку.", + "NoContentLengthWarning": "Предупреждение: сервер не указал размер файла.", + "ServerResponseTimeout": "Превышено время ожидания ответа от сервера.", + "DownloadIncompleteError": "Загрузка завершена не полностью.", + "FileWriteError": "Ошибка записи файла на диск.", + "UnexpectedError": "Возникла непредвиденная ошибка." + } + }, + "ApplicationUpdate": + { + "WindowTitle": "Обновление программы", + "UpdateAvailable": "Доступно обновление Libgen Desktop", + "NewVersion": "Новая версия: {version} от {date}", + "Download": "СКАЧАТЬ", + "DownloadAndInstall": "СКАЧАТЬ И УСТАНОВИТЬ", + "SkipThisVersion": "ПРОПУСТИТЬ ЭТУ ВЕРСИЮ", + "Cancel": "ОТМЕНА", + "Interrupt": "ПРЕРВАТЬ", + "Interrupting": "ПРЕРЫВАЕТСЯ...", + "InterruptPromptTitle": "Прервать загрузку?", + "InterruptPromptText": "Прервать загрузку обновления?", + "Error": "Ошибка", + "IncompleteDownload": "Файл обновления не был загружен полностью.", + "Close": "ЗАКРЫТЬ" + }, + "Settings": + { + "WindowTitle": "Настройки", + "Ok": "OK", + "Cancel": "ОТМЕНА", + "DiscardChangesPromptTitle": "Отменить изменения?", + "DiscardChangesPromptText": "Настройки были изменены. Вы действительно хотите отменить сделанные изменения?", + "General": + { + "TabHeader": "Общие", + "Language": "Язык интерфейса", + "CheckUpdates": "Проверять обновления программы", + "UpdateCheckIntervals": + { + "Never": "никогда", + "Daily": "один раз в день", + "Weekly": "один раз в неделю", + "Monthly": "один раз в месяц" + } + }, + "Network": + { + "TabHeader": "Сеть", + "OfflineMode": "Автономный режим", + "UseHttpProxy": "Использовать HTTP прокси-сервер", + "ProxyAddress": "Адрес", + "ProxyAddressRequired": "Обязательное поле", + "ProxyPort": "Порт", + "ProxyPortValidation": "Числа от {min} до {max}", + "ProxyUserName": "Имя пользователя", + "ProxyPassword": "Пароль", + "ProxyPasswordWarning": "Хранить здесь пароль небезопасно." + }, + "Download": + { + "TabHeader": "Загрузки", + "DownloadMode": "При скачивании книги / статьи", + "OpenInBrowser": "открывать ссылку на скачивание в браузере", + "UseDownloadManager": "использовать встроенный менеджер загрузок", + "DownloadDirectory": "Загружать в", + "BrowseDirectoryDialogTitle": "Директория для загруженных файлов", + "DownloadDirectoryNotFound": "Директория не существует", + "Timeout": "Таймаут загрузки", + "TimeoutValidation": "Числа от {min} до {max}", + "Seconds": "секунд", + "DownloadAttempts": "Количество попыток", + "DownloadAttemptsValidation": "Числа от {min} до {max}", + "Times": "раз", + "RetryDelay": "Пауза между попытками", + "RetryDelayValidation": "Числа от {min} до {max}" + }, + "Mirrors": + { + "TabHeader": "Зеркала", + "NonFiction": "Нехудожественная литература", + "Fiction": "Художественная литература", + "SciMagArticles": "Научные статьи", + "Books": "Книги", + "Articles": "Статьи", + "Covers": "Обложки", + "Synchronization": "Синхронизация", + "NoMirror": "нет" + }, + "Search": + { + "TabHeader": "Поиск", + "LimitResults": "Ограничивать количество результатов", + "MaximumResults": "Максимальное количество результатов", + "PositiveNumbersOnly": "Только положительные числа", + "OpenDetails": "Открывать детали книги / статьи", + "InModalWindow": "в модальном окне", + "InNonModalWindow": "в немодальном окне", + "InNewTab": "в новой вкладке" + }, + "Export": + { + "TabHeader": "Экспорт", + "OpenResults": "Открывать результат после экспорта", + "SplitIntoMultipleFiles": "Делить на несколько файлов", + "MaximumRowsPerFile": "Максимальное количество строк в файле", + "MaximumRowsPerFileValidation": "Числа от {min} до {max}", + "ExcelLimitNote": "Примечание: Microsoft Excel не поддерживает файлы более {count} строк." + }, + "Advanced": + { + "TabHeader": "Дополнительно", + "UseLogging": "Записывать отладочную информацию в файл" + } + }, + "MessageBox": + { + "Ok": "OK", + "Yes": "ДА", + "No": "НЕТ" + }, + "ErrorWindow": + { + "WindowTitle": "Ошибка", + "UnexpectedError": "Возникла непредвиденная ошибка", + "Copy": "СКОПИРОВАТЬ В БУФЕР ОБМЕНА", + "Close": "ЗАКРЫТЬ" + } +} diff --git a/LibgenDesktop/Resources/Mirrors/b_ok_org.xslt b/LibgenDesktop/Resources/Mirrors/b_ok_org.xslt new file mode 100644 index 0000000..8e8ba3f --- /dev/null +++ b/LibgenDesktop/Resources/Mirrors/b_ok_org.xslt @@ -0,0 +1,8 @@ + + + + + + + + diff --git a/LibgenDesktop/Resources/Mirrors/bookfi_net.xslt b/LibgenDesktop/Resources/Mirrors/bookfi_net.xslt new file mode 100644 index 0000000..f816116 --- /dev/null +++ b/LibgenDesktop/Resources/Mirrors/bookfi_net.xslt @@ -0,0 +1,8 @@ + + + + + + + + diff --git a/LibgenDesktop/Resources/Mirrors/booksc_org.xslt b/LibgenDesktop/Resources/Mirrors/booksc_org.xslt new file mode 100644 index 0000000..115a92e --- /dev/null +++ b/LibgenDesktop/Resources/Mirrors/booksc_org.xslt @@ -0,0 +1,8 @@ + + + + + + + + diff --git a/LibgenDesktop/Resources/Mirrors/libgen_io_fiction.xslt b/LibgenDesktop/Resources/Mirrors/libgen_io_fiction.xslt new file mode 100644 index 0000000..d12cbb1 --- /dev/null +++ b/LibgenDesktop/Resources/Mirrors/libgen_io_fiction.xslt @@ -0,0 +1,8 @@ + + + + + + + + diff --git a/LibgenDesktop/Resources/Mirrors/libgen_io_nonfiction.xslt b/LibgenDesktop/Resources/Mirrors/libgen_io_nonfiction.xslt new file mode 100644 index 0000000..d408b2a --- /dev/null +++ b/LibgenDesktop/Resources/Mirrors/libgen_io_nonfiction.xslt @@ -0,0 +1,17 @@ + + + + + + + + + + + + + + + + + diff --git a/LibgenDesktop/Resources/Mirrors/libgen_io_scimag.xslt b/LibgenDesktop/Resources/Mirrors/libgen_io_scimag.xslt new file mode 100644 index 0000000..e15b287 --- /dev/null +++ b/LibgenDesktop/Resources/Mirrors/libgen_io_scimag.xslt @@ -0,0 +1,8 @@ + + + + + + + + diff --git a/LibgenDesktop/Resources/Mirrors/libgen_pw_fiction_step1.xslt b/LibgenDesktop/Resources/Mirrors/libgen_pw_fiction_step1.xslt new file mode 100644 index 0000000..d6f97e8 --- /dev/null +++ b/LibgenDesktop/Resources/Mirrors/libgen_pw_fiction_step1.xslt @@ -0,0 +1,9 @@ + + + + + https://fiction.libgen.pw + + + + diff --git a/LibgenDesktop/Resources/Mirrors/libgen_pw_fiction_step2.xslt b/LibgenDesktop/Resources/Mirrors/libgen_pw_fiction_step2.xslt new file mode 100644 index 0000000..7f85690 --- /dev/null +++ b/LibgenDesktop/Resources/Mirrors/libgen_pw_fiction_step2.xslt @@ -0,0 +1,9 @@ + + + + + https://fiction.libgen.pw + + + + diff --git a/LibgenDesktop/Resources/Mirrors/libgen_pw_nonfiction_step1.xslt b/LibgenDesktop/Resources/Mirrors/libgen_pw_nonfiction_step1.xslt new file mode 100644 index 0000000..e06edfe --- /dev/null +++ b/LibgenDesktop/Resources/Mirrors/libgen_pw_nonfiction_step1.xslt @@ -0,0 +1,9 @@ + + + + + https://libgen.pw + + + + diff --git a/LibgenDesktop/Resources/Mirrors/libgen_pw_nonfiction_step2.xslt b/LibgenDesktop/Resources/Mirrors/libgen_pw_nonfiction_step2.xslt new file mode 100644 index 0000000..9a7750a --- /dev/null +++ b/LibgenDesktop/Resources/Mirrors/libgen_pw_nonfiction_step2.xslt @@ -0,0 +1,9 @@ + + + + + https://libgen.pw + + + + diff --git a/LibgenDesktop/Resources/Mirrors/libgen_pw_scimag_step1.xslt b/LibgenDesktop/Resources/Mirrors/libgen_pw_scimag_step1.xslt new file mode 100644 index 0000000..50a7c1d --- /dev/null +++ b/LibgenDesktop/Resources/Mirrors/libgen_pw_scimag_step1.xslt @@ -0,0 +1,9 @@ + + + + + https://sci.libgen.pw + + + + diff --git a/LibgenDesktop/Resources/Mirrors/libgen_pw_scimag_step2.xslt b/LibgenDesktop/Resources/Mirrors/libgen_pw_scimag_step2.xslt new file mode 100644 index 0000000..bba8c4f --- /dev/null +++ b/LibgenDesktop/Resources/Mirrors/libgen_pw_scimag_step2.xslt @@ -0,0 +1,9 @@ + + + + + https://sci.libgen.pw + + + + diff --git a/LibgenDesktop/Resources/mirrors.config b/LibgenDesktop/Resources/Mirrors/mirrors.config similarity index 50% rename from LibgenDesktop/Resources/mirrors.config rename to LibgenDesktop/Resources/Mirrors/mirrors.config index 91fb6c0..c435c43 100644 --- a/LibgenDesktop/Resources/mirrors.config +++ b/LibgenDesktop/Resources/Mirrors/mirrors.config @@ -1,39 +1,49 @@ { "libgen.io": { - "NonFictionDownloadUrl": "http://libgen.io/book/index.php?md5={md5}", + "NonFictionDownloadUrl": "http://libgen.io/ads.php?md5={md5}", + "NonFictionDownloadTransformations": "libgen_io_nonfiction", "NonFictionCoverUrl": "http://libgen.io/covers/{cover-url}", "NonFictionSynchronizationUrl": "http://libgen.io/json.php", "FictionDownloadUrl": "http://libgen.io/foreignfiction/ads.php?md5={md5}", + "FictionDownloadTransformations": "libgen_io_fiction", "FictionCoverUrl": "http://libgen.io/fictioncovers/{thousand-bucket}/{md5}.jpg", - "SciMagDownloadUrl": "http://libgen.io/scimag/ads.php?doi={doi}" + "SciMagDownloadUrl": "http://libgen.io/scimag/ads.php?doi={doi}", + "SciMagDownloadTransformations": "libgen_io_scimag" }, "gen.lib.rus.ec": { - "NonFictionDownloadUrl": "http://gen.lib.rus.ec/book/index.php?md5={md5}", "NonFictionCoverUrl": "http://gen.lib.rus.ec/covers/{cover-url}", "NonFictionSynchronizationUrl": "http://gen.lib.rus.ec/json.php" }, "libgen.pw": { "NonFictionDownloadUrl": "https://libgen.pw/item/detail/id/{id}?id={id}", + "NonFictionDownloadTransformations": "libgen_pw_nonfiction_step1,libgen_pw_nonfiction_step2", "NonFictionCoverUrl": "https://libgen.pw/covers/{cover-url}", - "FictionDownloadUrl": "http://fiction.libgen.pw/item/detail/{md5}", - "SciMagDownloadUrl": "https://sci.libgen.pw/item/detail/{md5}" + "FictionDownloadUrl": "https://fiction.libgen.pw/item/detail/{md5}", + "FictionDownloadTransformations": "libgen_pw_fiction_step1,libgen_pw_fiction_step2", + "SciMagDownloadUrl": "https://sci.libgen.pw/item/detail/{md5}", + "SciMagDownloadTransformations": "libgen_pw_scimag_step1,libgen_pw_scimag_step2" }, "bookfi.net": { "NonFictionDownloadUrl": "http://bookfi.net/md5/{md5}", + "NonFictionDownloadTransformations": "bookfi_net", "NonFictionCoverUrl": "http://i.bookfi.net/covers/{cover-url}", - "FictionDownloadUrl": "http://bookfi.net/md5/{md5}" + "FictionDownloadUrl": "http://bookfi.net/md5/{md5}", + "FictionDownloadTransformations": "bookfi_net" }, "b-ok.org": { "NonFictionDownloadUrl": "http://b-ok.org/md5/{md5}", - "FictionDownloadUrl": "http://b-ok.org/md5/{md5}" + "NonFictionDownloadTransformations": "b_ok_org", + "FictionDownloadUrl": "http://b-ok.org/md5/{md5}", + "FictionDownloadTransformations": "b_ok_org" }, "booksc.org": { - "SciMagDownloadUrl": "http://booksc.org/s/?q={doi}&t=0" + "SciMagDownloadUrl": "http://booksc.org/s/?q={doi}&t=0", + "SciMagDownloadTransformations": "booksc_org" } } \ No newline at end of file diff --git a/LibgenDesktop/ViewModels/ContainerViewModel.cs b/LibgenDesktop/ViewModels/ContainerViewModel.cs new file mode 100644 index 0000000..66c8398 --- /dev/null +++ b/LibgenDesktop/ViewModels/ContainerViewModel.cs @@ -0,0 +1,46 @@ +using System; +using LibgenDesktop.Common; +using LibgenDesktop.Infrastructure; +using LibgenDesktop.Models; +using LibgenDesktop.ViewModels.Windows; + +namespace LibgenDesktop.ViewModels +{ + internal abstract class ContainerViewModel : ViewModel + { + protected ContainerViewModel(MainModel mainModel) + { + MainModel = mainModel; + } + + protected MainModel MainModel { get; } + + protected void ShowErrorWindow(Exception exception, IWindowContext parentWindowContext) + { + try + { + Logger.Exception(exception); + } + catch + { + } + ErrorWindowViewModel errorWindowViewModel = new ErrorWindowViewModel(exception.ToString(), MainModel.Localization.CurrentLanguage); + IWindowContext errorWindowContext = WindowManager.CreateWindow(RegisteredWindows.WindowKey.ERROR_WINDOW, errorWindowViewModel, parentWindowContext); + errorWindowContext.ShowDialog(); + } + + protected virtual void ShowMessage(string title, string text, IWindowContext parentWindowContext) + { + string ok = MainModel.Localization.CurrentLanguage.MessageBox.Ok; + WindowManager.ShowMessage(title, text, ok, parentWindowContext); + } + + protected virtual bool ShowPrompt(string title, string text, IWindowContext parentWindowContext) + { + string yes = MainModel.Localization.CurrentLanguage.MessageBox.Yes; + string no = MainModel.Localization.CurrentLanguage.MessageBox.No; + return WindowManager.ShowPrompt(title, text, yes, no, parentWindowContext); + } + + } +} diff --git a/LibgenDesktop/ViewModels/DetailsItems/DetailsItemViewModel.cs b/LibgenDesktop/ViewModels/DetailsItems/DetailsItemViewModel.cs new file mode 100644 index 0000000..b9d4cb4 --- /dev/null +++ b/LibgenDesktop/ViewModels/DetailsItems/DetailsItemViewModel.cs @@ -0,0 +1,37 @@ +using System; +using LibgenDesktop.Models.Entities; +using LibgenDesktop.Models.Localization; + +namespace LibgenDesktop.ViewModels.DetailsItems +{ + internal abstract class DetailsItemViewModel : ViewModel where T : LibgenObject + { + public DetailsItemViewModel(T libgenObject, Language currentLanguage) + { + LibgenObject = libgenObject; + CurrentLanguage = currentLanguage; + } + + protected T LibgenObject { get; } + protected Language CurrentLanguage { get; private set; } + + + public void UpdateLocalization(Language newLanguage) + { + CurrentLanguage = newLanguage; + UpdateLocalizableProperties(); + } + + protected abstract void UpdateLocalizableProperties(); + + protected string FormatDateTime(DateTime dateTime) + { + return CurrentLanguage.Formatter.ToFormattedDateTimeString(dateTime); + } + + protected string FormatFileSize(long fileSize) + { + return CurrentLanguage.Formatter.FileSizeToString(fileSize, true); + } + } +} diff --git a/LibgenDesktop/ViewModels/DetailsItems/FictionDetailsItemViewModel.cs b/LibgenDesktop/ViewModels/DetailsItems/FictionDetailsItemViewModel.cs new file mode 100644 index 0000000..1fa47ce --- /dev/null +++ b/LibgenDesktop/ViewModels/DetailsItems/FictionDetailsItemViewModel.cs @@ -0,0 +1,49 @@ +using LibgenDesktop.Models.Entities; +using LibgenDesktop.Models.Localization; +using LibgenDesktop.Models.Localization.Localizators; + +namespace LibgenDesktop.ViewModels.DetailsItems +{ + internal class FictionDetailsItemViewModel : DetailsItemViewModel + { + private FictionDetailsTabLocalizator localization; + + public FictionDetailsItemViewModel(FictionBook libgenObject, Language currentLanguage) + : base(libgenObject, currentLanguage) + { + localization = currentLanguage.FictionDetailsTab; + } + + public FictionBook Book => LibgenObject; + public string Title => Book.Title; + public string Authors => Book.Authors; + public string RussianAuthor => Book.RussianAuthor; + public string Series => Book.Series; + public string Publisher => Book.Publisher; + public string Edition => Book.Edition; + public string Year => localization.GetYearString(Book.Year); + public string Language => Book.Language; + public string Format => Book.Format; + public string Pages => localization.GetPagesString(Book.Pages); + public string Version => Book.Version; + public string FileSize => FormatFileSize(Book.SizeInBytes); + public string AddedDateTime => localization.GetAddedDateTimeString(Book.AddedDateTime); + public string LastModifiedDateTime => FormatDateTime(Book.LastModifiedDateTime); + public string Md5Hash => Book.Md5Hash; + public string Commentary => Book.Commentary; + public string LibgenId => Book.LibgenId.ToString(); + public string Identifier => Book.Identifier; + public string GoogleBookId => Book.GoogleBookId; + public string Asin => Book.Asin; + + protected override void UpdateLocalizableProperties() + { + localization = CurrentLanguage.FictionDetailsTab; + NotifyPropertyChanged(nameof(Year)); + NotifyPropertyChanged(nameof(Pages)); + NotifyPropertyChanged(nameof(FileSize)); + NotifyPropertyChanged(nameof(AddedDateTime)); + NotifyPropertyChanged(nameof(LastModifiedDateTime)); + } + } +} diff --git a/LibgenDesktop/ViewModels/DetailsItems/NonFictionDetailsItemViewModel.cs b/LibgenDesktop/ViewModels/DetailsItems/NonFictionDetailsItemViewModel.cs new file mode 100644 index 0000000..88d8a00 --- /dev/null +++ b/LibgenDesktop/ViewModels/DetailsItems/NonFictionDetailsItemViewModel.cs @@ -0,0 +1,74 @@ +using LibgenDesktop.Models.Entities; +using LibgenDesktop.Models.Localization; +using LibgenDesktop.Models.Localization.Localizators; + +namespace LibgenDesktop.ViewModels.DetailsItems +{ + internal class NonFictionDetailsItemViewModel : DetailsItemViewModel + { + private NonFictionDetailsTabLocalizator localization; + + public NonFictionDetailsItemViewModel(NonFictionBook book, Language currentLanguage) + : base(book, currentLanguage) + { + localization = currentLanguage.NonFictionDetailsTab; + } + + public NonFictionBook Book => LibgenObject; + public string Title => Book.Title; + public string Authors => Book.Authors; + public string Series => Book.Series; + public string Publisher => Book.Publisher; + public string Year => Book.Year; + public string Language => Book.Language; + public string Format => Book.Format; + public string Identifier => Book.Identifier; + public string AddedDateTime => FormatDateTime(Book.AddedDateTime); + public string LastModifiedDateTime => FormatDateTime(Book.LastModifiedDateTime); + public string Library => Book.Library; + public string FileSize => FormatFileSize(Book.SizeInBytes); + public string Topic => Book.Topic; + public string Volume => Book.VolumeInfo; + public string Periodical => Book.Periodical; + public string City => Book.City; + public string Edition => Book.Edition; + public string Pages => localization.GetPagesText(Book.Pages, Book.PagesInFile); + public string Tags => Book.Tags; + public string Md5Hash => Book.Md5Hash; + public string Commentary => Book.Commentary; + public string LibgenId => Book.LibgenId.ToString(); + public string Issn => Book.Issn; + public string Udc => Book.Udc; + public string Lbc => Book.Lbc; + public string Lcc => Book.Lcc; + public string Ddc => Book.Ddc; + public string Doi => Book.Doi; + public string OpenLibraryId => Book.OpenLibraryId; + public string GoogleBookId => Book.GoogleBookId; + public string Asin => Book.Asin; + public string Dpi => Book.Dpi.ToString(); + public string Ocr => localization.GetOcrString(Book.Searchable); + public string Bookmarked => localization.GetBookmarkedString(Book.Bookmarked); + public string Scanned => localization.GetScannedString(Book.Scanned); + public string Orientation => localization.GetOrientationString(Book.Orientation); + public string Paginated => localization.GetPaginatedString(Book.Paginated); + public string Color => localization.GetColorString(Book.Color); + public string Cleaned => localization.GetCleanedString(Book.Cleaned); + + protected override void UpdateLocalizableProperties() + { + localization = CurrentLanguage.NonFictionDetailsTab; + NotifyPropertyChanged(nameof(AddedDateTime)); + NotifyPropertyChanged(nameof(LastModifiedDateTime)); + NotifyPropertyChanged(nameof(FileSize)); + NotifyPropertyChanged(nameof(Pages)); + NotifyPropertyChanged(nameof(Ocr)); + NotifyPropertyChanged(nameof(Bookmarked)); + NotifyPropertyChanged(nameof(Scanned)); + NotifyPropertyChanged(nameof(Orientation)); + NotifyPropertyChanged(nameof(Paginated)); + NotifyPropertyChanged(nameof(Color)); + NotifyPropertyChanged(nameof(Cleaned)); + } + } +} diff --git a/LibgenDesktop/ViewModels/DetailsItems/SciMagDetailsItemViewModel.cs b/LibgenDesktop/ViewModels/DetailsItems/SciMagDetailsItemViewModel.cs new file mode 100644 index 0000000..0891bf7 --- /dev/null +++ b/LibgenDesktop/ViewModels/DetailsItems/SciMagDetailsItemViewModel.cs @@ -0,0 +1,56 @@ +using System; +using LibgenDesktop.Models.Entities; +using LibgenDesktop.Models.Localization; +using LibgenDesktop.Models.Localization.Localizators; + +namespace LibgenDesktop.ViewModels.DetailsItems +{ + internal class SciMagDetailsItemViewModel : DetailsItemViewModel + { + private SciMagDetailsTabLocalizator localization; + + public SciMagDetailsItemViewModel(SciMagArticle libgenObject, Language currentLanguage) + : base(libgenObject, currentLanguage) + { + localization = currentLanguage.SciMagDetailsTab; + } + + public SciMagArticle Article => LibgenObject; + public string Title => Article.Title; + public string Authors => Article.Authors; + public string Journal => Article.Journal; + public string Year => Article.Year; + public string Month => Article.Month; + public string Day => Article.Day; + public string Volume => Article.Volume; + public string Issue => Article.Issue; + public string Pages => localization.GetPagesString(Article.FirstPage, Article.LastPage); + public string FileSize => FormatFileSize(Article.SizeInBytes); + public string AddedDateTime => localization.GetAddedDateTimeString(Article.AddedDateTime); + public string Md5Hash => Article.Md5Hash; + public string AbstractUrl => Article.AbstractUrl; + public string LibgenId => Article.LibgenId.ToString(); + public string Doi => Article.DoiString; + public string Isbn => Article.Isbn; + public string JournalId => Article.JournalId; + public string Issnp => Article.Issnp; + public string Issne => Article.Issne; + public string PubmedId => Article.PubmedId; + public string Pmc => Article.Pmc; + public string Pii => Article.Pii; + public string Attribute1 => Article.Attribute1; + public string Attribute2 => Article.Attribute2; + public string Attribute3 => Article.Attribute3; + public string Attribute4 => Article.Attribute4; + public string Attribute5 => Article.Attribute5; + public string Attribute6 => Article.Attribute6; + + protected override void UpdateLocalizableProperties() + { + localization = CurrentLanguage.SciMagDetailsTab; + NotifyPropertyChanged(nameof(Pages)); + NotifyPropertyChanged(nameof(FileSize)); + NotifyPropertyChanged(nameof(AddedDateTime)); + } + } +} diff --git a/LibgenDesktop/ViewModels/DownloadManagerTabViewModel.cs b/LibgenDesktop/ViewModels/DownloadManagerTabViewModel.cs deleted file mode 100644 index 5571228..0000000 --- a/LibgenDesktop/ViewModels/DownloadManagerTabViewModel.cs +++ /dev/null @@ -1,13 +0,0 @@ -using LibgenDesktop.Infrastructure; -using LibgenDesktop.Models; - -namespace LibgenDesktop.ViewModels -{ - internal class DownloadManagerTabViewModel : TabViewModel - { - public DownloadManagerTabViewModel(MainModel mainModel, IWindowContext parentWindowContext) - : base(mainModel, parentWindowContext, "Загрузки") - { - } - } -} diff --git a/LibgenDesktop/ViewModels/ErrorWindowViewModel.cs b/LibgenDesktop/ViewModels/ErrorWindowViewModel.cs deleted file mode 100644 index ff1377f..0000000 --- a/LibgenDesktop/ViewModels/ErrorWindowViewModel.cs +++ /dev/null @@ -1,23 +0,0 @@ -using System.Windows; -using LibgenDesktop.Infrastructure; - -namespace LibgenDesktop.ViewModels -{ - internal class ErrorWindowViewModel : LibgenWindowViewModel - { - public ErrorWindowViewModel(string error) - { - Error = error; - CopyErrorCommand = new Command(CopyErrorToClipboard); - } - - public string Error { get; } - - public Command CopyErrorCommand { get; } - - private void CopyErrorToClipboard() - { - WindowManager.SetClipboardText(Error); - } - } -} diff --git a/LibgenDesktop/ViewModels/EventArguments/OpenFictionDetailsEventArgs.cs b/LibgenDesktop/ViewModels/EventArguments/OpenFictionDetailsEventArgs.cs new file mode 100644 index 0000000..a5d7f28 --- /dev/null +++ b/LibgenDesktop/ViewModels/EventArguments/OpenFictionDetailsEventArgs.cs @@ -0,0 +1,15 @@ +using System; +using LibgenDesktop.Models.Entities; + +namespace LibgenDesktop.ViewModels.EventArguments +{ + internal class OpenFictionDetailsEventArgs : EventArgs + { + public OpenFictionDetailsEventArgs(FictionBook fictionBook) + { + FictionBook = fictionBook; + } + + public FictionBook FictionBook { get; } + } +} diff --git a/LibgenDesktop/ViewModels/EventArguments/OpenNonFictionDetailsEventArgs.cs b/LibgenDesktop/ViewModels/EventArguments/OpenNonFictionDetailsEventArgs.cs new file mode 100644 index 0000000..5f16747 --- /dev/null +++ b/LibgenDesktop/ViewModels/EventArguments/OpenNonFictionDetailsEventArgs.cs @@ -0,0 +1,15 @@ +using System; +using LibgenDesktop.Models.Entities; + +namespace LibgenDesktop.ViewModels.EventArguments +{ + internal class OpenNonFictionDetailsEventArgs : EventArgs + { + public OpenNonFictionDetailsEventArgs(NonFictionBook nonFictionBook) + { + NonFictionBook = nonFictionBook; + } + + public NonFictionBook NonFictionBook { get; } + } +} diff --git a/LibgenDesktop/ViewModels/EventArguments/OpenSciMagDetailsEventArgs.cs b/LibgenDesktop/ViewModels/EventArguments/OpenSciMagDetailsEventArgs.cs new file mode 100644 index 0000000..1da21fb --- /dev/null +++ b/LibgenDesktop/ViewModels/EventArguments/OpenSciMagDetailsEventArgs.cs @@ -0,0 +1,15 @@ +using System; +using LibgenDesktop.Models.Entities; + +namespace LibgenDesktop.ViewModels.EventArguments +{ + internal class OpenSciMagDetailsEventArgs : EventArgs + { + public OpenSciMagDetailsEventArgs(SciMagArticle sciMagArticle) + { + SciMagArticle = sciMagArticle; + } + + public SciMagArticle SciMagArticle { get; } + } +} diff --git a/LibgenDesktop/ViewModels/EventArguments/SearchCompleteEventArgs.cs b/LibgenDesktop/ViewModels/EventArguments/SearchCompleteEventArgs.cs new file mode 100644 index 0000000..055e0d5 --- /dev/null +++ b/LibgenDesktop/ViewModels/EventArguments/SearchCompleteEventArgs.cs @@ -0,0 +1,18 @@ +using System; +using System.Collections.Generic; +using LibgenDesktop.Models.Entities; + +namespace LibgenDesktop.ViewModels.EventArguments +{ + internal class SearchCompleteEventArgs : EventArgs where T: LibgenObject + { + public SearchCompleteEventArgs(string searchQuery, List searchResult) + { + SearchQuery = searchQuery; + SearchResult = searchResult; + } + + public string SearchQuery { get; } + public List SearchResult { get; } + } +} diff --git a/LibgenDesktop/ViewModels/EventArguments/SelectDownloadEventArgs.cs b/LibgenDesktop/ViewModels/EventArguments/SelectDownloadEventArgs.cs new file mode 100644 index 0000000..c358420 --- /dev/null +++ b/LibgenDesktop/ViewModels/EventArguments/SelectDownloadEventArgs.cs @@ -0,0 +1,14 @@ +using System; + +namespace LibgenDesktop.ViewModels.EventArguments +{ + internal class SelectDownloadEventArgs : EventArgs + { + public SelectDownloadEventArgs(Guid downloadId) + { + DownloadId = downloadId; + } + + public Guid DownloadId { get; } + } +} diff --git a/LibgenDesktop/ViewModels/FictionDetailsTabViewModel.cs b/LibgenDesktop/ViewModels/FictionDetailsTabViewModel.cs deleted file mode 100644 index b5c683d..0000000 --- a/LibgenDesktop/ViewModels/FictionDetailsTabViewModel.cs +++ /dev/null @@ -1,235 +0,0 @@ -using System; -using System.Diagnostics; -using System.IO; -using System.Net; -using System.Windows.Media.Imaging; -using LibgenDesktop.Infrastructure; -using LibgenDesktop.Models; -using LibgenDesktop.Models.Entities; -using LibgenDesktop.Models.Utils; - -namespace LibgenDesktop.ViewModels -{ - internal class FictionDetailsTabViewModel : TabViewModel - { - private FictionBook book; - private bool isBookCoverNotificationVisible; - private string bookCoverNotification; - private bool bookCoverVisible; - private BitmapImage bookCover; - private string downloadButtonCaption; - private bool isDownloadButtonEnabled; - private string disabledDownloadButtonTooltip; - private string bookDownloadUrl; - - public FictionDetailsTabViewModel(MainModel mainModel, IWindowContext parentWindowContext, FictionBook book, bool isInModalWindow) - : base(mainModel, parentWindowContext, book.Title) - { - this.book = book; - IsInModalWindow = isInModalWindow; - DownloadBookCommand = new Command(DownloadBook); - CloseCommand = new Command(CloseTab); - Initialize(); - } - - public bool IsInModalWindow { get; } - - public FictionBook Book - { - get - { - return book; - } - private set - { - book = value; - NotifyPropertyChanged(); - } - } - - public bool IsBookCoverNotificationVisible - { - get - { - return isBookCoverNotificationVisible; - } - set - { - isBookCoverNotificationVisible = value; - NotifyPropertyChanged(); - } - } - - public string BookCoverNotification - { - get - { - return bookCoverNotification; - } - set - { - bookCoverNotification = value; - NotifyPropertyChanged(); - } - } - - public bool BookCoverVisible - { - get - { - return bookCoverVisible; - } - private set - { - bookCoverVisible = value; - NotifyPropertyChanged(); - } - } - - public BitmapImage BookCover - { - get - { - return bookCover; - } - private set - { - bookCover = value; - NotifyPropertyChanged(); - } - } - - public string DownloadButtonCaption - { - get - { - return downloadButtonCaption; - } - set - { - downloadButtonCaption = value; - NotifyPropertyChanged(); - } - } - - public bool IsDownloadButtonEnabled - { - get - { - return isDownloadButtonEnabled; - } - set - { - isDownloadButtonEnabled = value; - NotifyPropertyChanged(); - } - } - - public string DisabledDownloadButtonTooltip - { - get - { - return disabledDownloadButtonTooltip; - } - set - { - disabledDownloadButtonTooltip = value; - NotifyPropertyChanged(); - } - } - - public Command DownloadBookCommand { get; } - public Command CloseCommand { get; } - - public event EventHandler CloseTabRequested; - - private async void Initialize() - { - bool isInOfflineMode = MainModel.AppSettings.Network.OfflineMode; - string downloadMirrorName = MainModel.AppSettings.Mirrors.FictionBooksMirrorName; - string coverMirrorName = MainModel.AppSettings.Mirrors.FictionCoversMirrorName; - if (downloadMirrorName == null) - { - DownloadButtonCaption = "СКАЧАТЬ"; - IsDownloadButtonEnabled = false; - DisabledDownloadButtonTooltip = "Не выбрано зеркало для загрузки книг"; - bookDownloadUrl = null; - } - else - { - DownloadButtonCaption = "СКАЧАТЬ С " + downloadMirrorName.ToUpper(); - if (isInOfflineMode) - { - IsDownloadButtonEnabled = false; - DisabledDownloadButtonTooltip = "Включен автономный режим"; - bookDownloadUrl = null; - } - else - { - IsDownloadButtonEnabled = true; - bookDownloadUrl = UrlGenerator.GetFictionDownloadUrl(MainModel.Mirrors[downloadMirrorName], Book); - } - } - BookCoverVisible = false; - BookCover = null; - bool hasCover = Book.Cover == "1"; - if (!hasCover) - { - BookCoverNotification = "Обложка отсутствует"; - IsBookCoverNotificationVisible = true; - } - else - { - if (coverMirrorName == null) - { - BookCoverNotification = "Не выбрано зеркало\r\nдля загрузки обложек"; - IsBookCoverNotificationVisible = true; - } - else - { - if (isInOfflineMode) - { - BookCoverNotification = "Обложка не загружена, потому что\r\nвключен автономный режим"; - IsBookCoverNotificationVisible = true; - } - else - { - string bookCoverUrl = UrlGenerator.GetFictionCoverUrl(MainModel.Mirrors[coverMirrorName], Book); - BookCoverNotification = "Обложка загружается..."; - IsBookCoverNotificationVisible = true; - try - { - byte[] imageData = await MainModel.HttpClient.GetByteArrayAsync(bookCoverUrl); - BitmapImage bitmapImage = new BitmapImage(); - using (MemoryStream memoryStream = new MemoryStream(imageData)) - { - bitmapImage.BeginInit(); - bitmapImage.CacheOption = BitmapCacheOption.OnLoad; - bitmapImage.StreamSource = memoryStream; - bitmapImage.EndInit(); - bitmapImage.Freeze(); - } - BookCover = bitmapImage; - BookCoverVisible = true; - IsBookCoverNotificationVisible = false; - } - catch - { - BookCoverNotification = "Не удалось загрузить обложку"; - } - } - } - } - } - - private void DownloadBook() - { - Process.Start(bookDownloadUrl); - } - - private void CloseTab() - { - CloseTabRequested?.Invoke(this, EventArgs.Empty); - } - } -} diff --git a/LibgenDesktop/ViewModels/FictionDetailsWindowViewModel.cs b/LibgenDesktop/ViewModels/FictionDetailsWindowViewModel.cs deleted file mode 100644 index 494b617..0000000 --- a/LibgenDesktop/ViewModels/FictionDetailsWindowViewModel.cs +++ /dev/null @@ -1,71 +0,0 @@ -using System; -using LibgenDesktop.Infrastructure; -using LibgenDesktop.Models; -using LibgenDesktop.Models.Entities; -using LibgenDesktop.Models.Settings; - -namespace LibgenDesktop.ViewModels -{ - internal class FictionDetailsWindowViewModel : LibgenWindowViewModel - { - private readonly MainModel mainModel; - private readonly FictionBook book; - private readonly bool modalWindow; - private FictionDetailsTabViewModel tabViewModel; - - public FictionDetailsWindowViewModel(MainModel mainModel, FictionBook book, bool modalWindow) - { - this.mainModel = mainModel; - this.book = book; - this.modalWindow = modalWindow; - tabViewModel = null; - WindowTitle = book.Title; - WindowWidth = mainModel.AppSettings.Fiction.DetailsWindow.Width; - WindowHeight = mainModel.AppSettings.Fiction.DetailsWindow.Height; - WindowClosedCommand = new Command(WindowClosed); - } - - public string WindowTitle { get; private set; } - public int WindowWidth { get; set; } - public int WindowHeight { get; set; } - - public FictionDetailsTabViewModel TabViewModel - { - get - { - if (tabViewModel == null) - { - tabViewModel = new FictionDetailsTabViewModel(mainModel, CurrentWindowContext, book, modalWindow); - tabViewModel.CloseTabRequested += CloseTabRequested; - } - return tabViewModel; - } - } - - public Command WindowClosedCommand { get; } - - private void CloseTabRequested(object sender, EventArgs e) - { - TabViewModel.CloseTabRequested -= CloseTabRequested; - IWindowContext currentWindowContext = WindowManager.GetWindowContext(this); - if (modalWindow) - { - currentWindowContext.CloseDialog(false); - } - else - { - currentWindowContext.Close(); - } - } - - private void WindowClosed() - { - mainModel.AppSettings.Fiction.DetailsWindow = new AppSettings.FictionDetailsWindowSettings - { - Width = WindowWidth, - Height = WindowHeight - }; - mainModel.SaveSettings(); - } - } -} diff --git a/LibgenDesktop/ViewModels/LibgenWindowViewModel.cs b/LibgenDesktop/ViewModels/LibgenWindowViewModel.cs deleted file mode 100644 index 817ab1b..0000000 --- a/LibgenDesktop/ViewModels/LibgenWindowViewModel.cs +++ /dev/null @@ -1,15 +0,0 @@ -using LibgenDesktop.Infrastructure; - -namespace LibgenDesktop.ViewModels -{ - internal abstract class LibgenWindowViewModel : ViewModel - { - protected IWindowContext CurrentWindowContext - { - get - { - return WindowManager.GetWindowContext(this); - } - } - } -} diff --git a/LibgenDesktop/ViewModels/NonFictionDetailsTabViewModel.cs b/LibgenDesktop/ViewModels/NonFictionDetailsTabViewModel.cs deleted file mode 100644 index 1668a7b..0000000 --- a/LibgenDesktop/ViewModels/NonFictionDetailsTabViewModel.cs +++ /dev/null @@ -1,234 +0,0 @@ -using System; -using System.Diagnostics; -using System.IO; -using System.Windows.Media.Imaging; -using LibgenDesktop.Infrastructure; -using LibgenDesktop.Models; -using LibgenDesktop.Models.Entities; -using LibgenDesktop.Models.Utils; - -namespace LibgenDesktop.ViewModels -{ - internal class NonFictionDetailsTabViewModel : TabViewModel - { - private NonFictionBook book; - private bool isBookCoverNotificationVisible; - private string bookCoverNotification; - private bool bookCoverVisible; - private BitmapImage bookCover; - private string downloadButtonCaption; - private bool isDownloadButtonEnabled; - private string disabledDownloadButtonTooltip; - private string bookDownloadUrl; - - public NonFictionDetailsTabViewModel(MainModel mainModel, IWindowContext parentWindowContext, NonFictionBook book, bool isInModalWindow) - : base(mainModel, parentWindowContext, book.Title) - { - this.book = book; - IsInModalWindow = isInModalWindow; - DownloadBookCommand = new Command(DownloadBook); - CloseCommand = new Command(CloseTab); - Initialize(); - } - - public bool IsInModalWindow { get; } - - public NonFictionBook Book - { - get - { - return book; - } - private set - { - book = value; - NotifyPropertyChanged(); - } - } - - public bool IsBookCoverNotificationVisible - { - get - { - return isBookCoverNotificationVisible; - } - set - { - isBookCoverNotificationVisible = value; - NotifyPropertyChanged(); - } - } - - public string BookCoverNotification - { - get - { - return bookCoverNotification; - } - set - { - bookCoverNotification = value; - NotifyPropertyChanged(); - } - } - - public bool BookCoverVisible - { - get - { - return bookCoverVisible; - } - private set - { - bookCoverVisible = value; - NotifyPropertyChanged(); - } - } - - public BitmapImage BookCover - { - get - { - return bookCover; - } - private set - { - bookCover = value; - NotifyPropertyChanged(); - } - } - - public string DownloadButtonCaption - { - get - { - return downloadButtonCaption; - } - set - { - downloadButtonCaption = value; - NotifyPropertyChanged(); - } - } - - public bool IsDownloadButtonEnabled - { - get - { - return isDownloadButtonEnabled; - } - set - { - isDownloadButtonEnabled = value; - NotifyPropertyChanged(); - } - } - - public string DisabledDownloadButtonTooltip - { - get - { - return disabledDownloadButtonTooltip; - } - set - { - disabledDownloadButtonTooltip = value; - NotifyPropertyChanged(); - } - } - - public Command DownloadBookCommand { get; } - public Command CloseCommand { get; } - - public event EventHandler CloseTabRequested; - - private async void Initialize() - { - bool isInOfflineMode = MainModel.AppSettings.Network.OfflineMode; - string downloadMirrorName = MainModel.AppSettings.Mirrors.NonFictionBooksMirrorName; - string coverMirrorName = MainModel.AppSettings.Mirrors.NonFictionCoversMirrorName; - if (downloadMirrorName == null) - { - DownloadButtonCaption = "СКАЧАТЬ"; - IsDownloadButtonEnabled = false; - DisabledDownloadButtonTooltip = "Не выбрано зеркало для загрузки книг"; - bookDownloadUrl = null; - } - else - { - DownloadButtonCaption = "СКАЧАТЬ С " + downloadMirrorName.ToUpper(); - if (isInOfflineMode) - { - IsDownloadButtonEnabled = false; - DisabledDownloadButtonTooltip = "Включен автономный режим"; - bookDownloadUrl = null; - } - else - { - IsDownloadButtonEnabled = true; - bookDownloadUrl = UrlGenerator.GetNonFictionDownloadUrl(MainModel.Mirrors[downloadMirrorName], Book); - } - } - BookCoverVisible = false; - BookCover = null; - bool hasCover = !String.IsNullOrWhiteSpace(Book.CoverUrl); - if (!hasCover) - { - BookCoverNotification = "Обложка отсутствует"; - IsBookCoverNotificationVisible = true; - } - else - { - if (coverMirrorName == null) - { - BookCoverNotification = "Не выбрано зеркало\r\nдля загрузки обложек"; - IsBookCoverNotificationVisible = true; - } - else - { - if (isInOfflineMode) - { - BookCoverNotification = "Обложка не загружена, потому что\r\nвключен автономный режим"; - IsBookCoverNotificationVisible = true; - } - else - { - string bookCoverUrl = UrlGenerator.GetNonFictionCoverUrl(MainModel.Mirrors[coverMirrorName], Book); - BookCoverNotification = "Обложка загружается..."; - IsBookCoverNotificationVisible = true; - try - { - byte[] imageData = await MainModel.HttpClient.GetByteArrayAsync(bookCoverUrl); - BitmapImage bitmapImage = new BitmapImage(); - using (MemoryStream memoryStream = new MemoryStream(imageData)) - { - bitmapImage.BeginInit(); - bitmapImage.CacheOption = BitmapCacheOption.OnLoad; - bitmapImage.StreamSource = memoryStream; - bitmapImage.EndInit(); - bitmapImage.Freeze(); - } - BookCover = bitmapImage; - BookCoverVisible = true; - IsBookCoverNotificationVisible = false; - } - catch - { - BookCoverNotification = "Не удалось загрузить обложку"; - } - } - } - } - } - - private void DownloadBook() - { - Process.Start(bookDownloadUrl); - } - - private void CloseTab() - { - CloseTabRequested?.Invoke(this, EventArgs.Empty); - } - } -} diff --git a/LibgenDesktop/ViewModels/NonFictionDetailsWindowViewModel.cs b/LibgenDesktop/ViewModels/NonFictionDetailsWindowViewModel.cs deleted file mode 100644 index 65407ef..0000000 --- a/LibgenDesktop/ViewModels/NonFictionDetailsWindowViewModel.cs +++ /dev/null @@ -1,71 +0,0 @@ -using System; -using LibgenDesktop.Infrastructure; -using LibgenDesktop.Models; -using LibgenDesktop.Models.Entities; -using LibgenDesktop.Models.Settings; - -namespace LibgenDesktop.ViewModels -{ - internal class NonFictionDetailsWindowViewModel : LibgenWindowViewModel - { - private readonly MainModel mainModel; - private readonly NonFictionBook book; - private readonly bool modalWindow; - private NonFictionDetailsTabViewModel tabViewModel; - - public NonFictionDetailsWindowViewModel(MainModel mainModel, NonFictionBook book, bool modalWindow) - { - this.mainModel = mainModel; - this.book = book; - this.modalWindow = modalWindow; - tabViewModel = null; - WindowTitle = book.Title; - WindowWidth = mainModel.AppSettings.NonFiction.DetailsWindow.Width; - WindowHeight = mainModel.AppSettings.NonFiction.DetailsWindow.Height; - WindowClosedCommand = new Command(WindowClosed); - } - - public string WindowTitle { get; private set; } - public int WindowWidth { get; set; } - public int WindowHeight { get; set; } - - public NonFictionDetailsTabViewModel TabViewModel - { - get - { - if (tabViewModel == null) - { - tabViewModel = new NonFictionDetailsTabViewModel(mainModel, CurrentWindowContext, book, modalWindow); - tabViewModel.CloseTabRequested += CloseTabRequested; - } - return tabViewModel; - } - } - - public Command WindowClosedCommand { get; } - - private void CloseTabRequested(object sender, EventArgs e) - { - TabViewModel.CloseTabRequested -= CloseTabRequested; - IWindowContext currentWindowContext = WindowManager.GetWindowContext(this); - if (modalWindow) - { - currentWindowContext.CloseDialog(false); - } - else - { - currentWindowContext.Close(); - } - } - - private void WindowClosed() - { - mainModel.AppSettings.NonFiction.DetailsWindow = new AppSettings.NonFictionDetailsWindowSettings - { - Width = WindowWidth, - Height = WindowHeight - }; - mainModel.SaveSettings(); - } - } -} diff --git a/LibgenDesktop/ViewModels/ExportPanelViewModel.cs b/LibgenDesktop/ViewModels/Panels/ExportPanelViewModel.cs similarity index 85% rename from LibgenDesktop/ViewModels/ExportPanelViewModel.cs rename to LibgenDesktop/ViewModels/Panels/ExportPanelViewModel.cs index 627b529..8083ee6 100644 --- a/LibgenDesktop/ViewModels/ExportPanelViewModel.cs +++ b/LibgenDesktop/ViewModels/Panels/ExportPanelViewModel.cs @@ -8,19 +8,20 @@ using LibgenDesktop.Models; using LibgenDesktop.Models.Entities; using LibgenDesktop.Models.Export; +using LibgenDesktop.Models.Localization; +using LibgenDesktop.Models.Localization.Localizators; using LibgenDesktop.Models.ProgressArgs; using LibgenDesktop.Models.Settings; using LibgenDesktop.Models.Utils; -using LibgenDesktop.Views; using Environment = LibgenDesktop.Common.Environment; -namespace LibgenDesktop.ViewModels +namespace LibgenDesktop.ViewModels.Panels { - internal class ExportPanelViewModel : ViewModel + internal class ExportPanelViewModel : ContainerViewModel { - private readonly MainModel mainModel; private readonly LibgenObjectType libgenObjectType; private readonly IWindowContext parentWindowContext; + private ExportPanelLocalizator localization; private CancellationTokenSource cancellationTokenSource; private bool isSeparatorPanelVisible; private bool isXlsxSelected; @@ -34,11 +35,9 @@ internal class ExportPanelViewModel : ViewModel private bool isLimitPanelVisible; private bool isNoLimitSelected; private bool isLimitSelected; - private string limitString; private bool isExportButtonEnabled; private bool isProgressPanelVisible; private string progressStatus; - private string cancelExportButtonText; private bool isCancelExportButtonEnabled; private bool isCancelExportButtonVisible; private bool areExportResultButtonsVisible; @@ -46,10 +45,11 @@ internal class ExportPanelViewModel : ViewModel private ExportResult exportResult; public ExportPanelViewModel(MainModel mainModel, LibgenObjectType libgenObjectType, IWindowContext parentWindowContext) + : base(mainModel) { - this.mainModel = mainModel; this.libgenObjectType = libgenObjectType; this.parentWindowContext = parentWindowContext; + localization = mainModel.Localization.CurrentLanguage.ExportPanel; SelectPathCommand = new Command(SelectPath); ExportCommand = new Command(Export); CancelCommand = new Command(Cancel); @@ -59,6 +59,19 @@ public ExportPanelViewModel(MainModel mainModel, LibgenObjectType libgenObjectTy Initialize(); } + public ExportPanelLocalizator Localization + { + get + { + return localization; + } + set + { + localization = value; + NotifyPropertyChanged(); + } + } + public bool IsSettingsPanelVisible { get @@ -213,12 +226,7 @@ public string LimitString { get { - return limitString; - } - set - { - limitString = value; - NotifyPropertyChanged(); + return Localization.GetLimitString(MainModel.AppSettings.Search.MaximumResultCount); } } @@ -265,12 +273,7 @@ public string CancelExportButtonText { get { - return cancelExportButtonText; - } - set - { - cancelExportButtonText = value; - NotifyPropertyChanged(); + return IsCancelExportButtonEnabled ? Localization.Interrupt : Localization.Interrupting; } } @@ -284,6 +287,7 @@ public bool IsCancelExportButtonEnabled { isCancelExportButtonEnabled = value; NotifyPropertyChanged(); + NotifyPropertyChanged(nameof(CancelExportButtonText)); } } @@ -349,19 +353,26 @@ public void UpdateSearchQuery(string searchQuery) IsExportButtonEnabled = !String.IsNullOrWhiteSpace(searchQuery); } + public void UpdateLocalization(Language newLanguage) + { + Localization = newLanguage.ExportPanel; + NotifyPropertyChanged(nameof(LimitString)); + NotifyPropertyChanged(nameof(CancelExportButtonText)); + } + private void Initialize() { AppSettings.ExportPanelSettngs exportPanelSettngs; switch (libgenObjectType) { case LibgenObjectType.NON_FICTION_BOOK: - exportPanelSettngs = mainModel.AppSettings.NonFiction.ExportPanel; + exportPanelSettngs = MainModel.AppSettings.NonFiction.ExportPanel; break; case LibgenObjectType.FICTION_BOOK: - exportPanelSettngs = mainModel.AppSettings.Fiction.ExportPanel; + exportPanelSettngs = MainModel.AppSettings.Fiction.ExportPanel; break; case LibgenObjectType.SCIMAG_ARTICLE: - exportPanelSettngs = mainModel.AppSettings.SciMag.ExportPanel; + exportPanelSettngs = MainModel.AppSettings.SciMag.ExportPanel; break; default: throw new Exception($"Unknown object type: {libgenObjectType}."); @@ -376,23 +387,20 @@ private void Initialize() isTabSeparatorSelected = exportPanelSettngs.Separator == AppSettings.ExportPanelSettngs.CsvSeparator.TAB; filePathTemplate = (exportPanelSettngs.ExportDirectory ?? Environment.AppDataDirectory) + @"\"; isExportButtonEnabled = false; - if (mainModel.AppSettings.Search.LimitResults) + if (MainModel.AppSettings.Search.LimitResults) { isLimitPanelVisible = true; isLimitSelected = exportPanelSettngs.LimitSearchResults; isNoLimitSelected = !isLimitSelected; - limitString = $"только первые {mainModel.AppSettings.Search.MaximumResultCount.ToFormattedString()} результатов"; } else { isLimitPanelVisible = false; isLimitSelected = false; isNoLimitSelected = true; - limitString = String.Empty; } isProgressPanelVisible = false; progressStatus = String.Empty; - cancelExportButtonText = "ПРЕРВАТЬ"; isCancelExportButtonEnabled = true; isCancelExportButtonVisible = false; areExportResultButtonsVisible = false; @@ -419,7 +427,7 @@ private void SelectPath() } SaveFileDialogParameters saveFileDialogParameters = new SaveFileDialogParameters { - DialogTitle = "Экспортировать результаты поиска", + DialogTitle = Localization.BrowseDialogTitle, Filter = GetFileFilter(), OverwritePrompt = false, InitialDirectory = initialDirectory, @@ -455,16 +463,7 @@ private string GetDirectoryPath(bool correctPath) private string GenerateFileName(string searchQuery) { - string result = searchQuery.Trim(); - foreach (char invalidChar in Path.GetInvalidFileNameChars()) - { - result = result.Replace(invalidChar, '_'); - } - if (String.IsNullOrEmpty(result)) - { - result = "export"; - } - return result; + return FileUtils.RemoveInvalidFileNameCharacters(searchQuery, "export"); } private string GetFileNameWithoutExtension(bool correctFileName) @@ -538,17 +537,22 @@ private string GetFileFilter() StringBuilder resultBuilder = new StringBuilder(); if (IsXlsxSelected) { - resultBuilder.Append("Файлы Microsoft Excel (*.xlsx)|*.xlsx"); + resultBuilder.Append(Localization.ExcelFiles); + resultBuilder.Append(" (*.xlsx)|*.xlsx"); } else if (IsTabSeparatorSelected) { - resultBuilder.Append("Файлы TSV (*.tsv)|*.tsv"); + resultBuilder.Append(Localization.TsvFiles); + resultBuilder.Append(" (*.tsv)|*.tsv"); } else { - resultBuilder.Append("Файлы CSV (*.csv)|*.csv"); + resultBuilder.Append(Localization.CsvFiles); + resultBuilder.Append(" (*.csv)|*.csv"); } - resultBuilder.Append("|Все файлы (*.*)|*.*"); + resultBuilder.Append("|"); + resultBuilder.Append(Localization.AllFiles); + resultBuilder.Append(" (*.*)|*.*"); return resultBuilder.ToString(); } @@ -557,26 +561,27 @@ private async void Export() string directory = GetDirectoryPath(correctPath: false); if (directory == null) { - MessageBoxWindow.ShowMessage("Ошибка", "Указанный путь файла для экспорта некорректен.", parentWindowContext); + ShowMessage(Localization.ErrorWarningTitle, Localization.InvalidExportPath, parentWindowContext); return; } if (!Directory.Exists(directory)) { - MessageBoxWindow.ShowMessage("Ошибка", $"Директория {directory} не существует.", parentWindowContext); + ShowMessage(Localization.ErrorWarningTitle, Localization.GetDirectoryNotFoundString(directory), parentWindowContext); return; } string fileNameWithoutExtension = GetFileNameWithoutExtension(correctFileName: false); if (fileNameWithoutExtension == null) { - MessageBoxWindow.ShowMessage("Ошибка", "Указанное имя файла для экспорта некорректно.", parentWindowContext); + ShowMessage(Localization.ErrorWarningTitle, Localization.InvalidExportFileName, parentWindowContext); return; } string fileExtension = GetFileExtension(); - if (File.Exists(FilePathTemplate) && !MessageBoxWindow.ShowPrompt("Перезаписать файл?", $"Файл {FilePathTemplate} существует. Вы действительно хотите перезаписать его?", parentWindowContext)) + if (File.Exists(FilePathTemplate) && + !ShowPrompt(Localization.OverwritePromptTitle, Localization.GetOverwritePromptTextString(FilePathTemplate), parentWindowContext)) { return; } - int? searchResultLimit = isNoLimitSelected ? (int?)null : mainModel.AppSettings.Search.MaximumResultCount; + int? searchResultLimit = isNoLimitSelected ? (int?)null : MainModel.AppSettings.Search.MaximumResultCount; Progress exportProgressHandler = new Progress(HandleExportProgress); cancellationTokenSource = new CancellationTokenSource(); CancellationToken cancellationToken = cancellationTokenSource.Token; @@ -638,7 +643,6 @@ private async void Export() IsSettingsPanelVisible = false; IsProgressPanelVisible = true; AreExportResultButtonsVisible = false; - CancelExportButtonText = "ПРЕРВАТЬ"; IsCancelExportButtonEnabled = true; IsCancelExportButtonVisible = true; SaveSettings(); @@ -646,7 +650,7 @@ private async void Export() exportResult = null; try { - exportResult = await exportTask(mainModel); + exportResult = await exportTask(MainModel); } catch (Exception exception) { @@ -666,18 +670,16 @@ private async void Export() string status = null; if (exportResult.IsExportCancelled || exportResult.IsRowsPerFileLimitReached) { - status = "Экспорт прерван."; + status = Localization.ExportInterrupted; } UpdateProgressStatus(status, exportResult.ItemsExported, exportResult.FilesCreated); if (exportResult.IsRowsPerFileLimitReached) { - MessageBoxWindow.ShowMessage("Предел количества строк", - "Достигнут предел количества строк, допустимый для записи в файл Microsoft Excel. Дальнейший экспорт невозможен. " + - @"Для экспорта большего числа строк включите опцию ""Делить на несколько файлов"" в настройках программы.", parentWindowContext); + ShowMessage(Localization.RowLimitWarningTitle, Localization.RowLimitWarningText, parentWindowContext); } AreExportResultButtonsVisible = true; IsShowResultButtonVisible = exportResult.ItemsExported > 0 && exportResult.FirstFilePath != null; - if (IsShowResultButtonVisible && mainModel.AppSettings.Export.OpenResultsAfterExport && !exportResult.IsExportCancelled && + if (IsShowResultButtonVisible && MainModel.AppSettings.Export.OpenResultsAfterExport && !exportResult.IsExportCancelled && !exportResult.IsRowsPerFileLimitReached) { ShowResult(); @@ -686,7 +688,7 @@ private async void Export() } else { - ProgressStatus = "Экспорт завершился с ошибкой."; + ProgressStatus = Localization.ExportError; AreExportResultButtonsVisible = true; IsShowResultButtonVisible = false; } @@ -694,7 +696,7 @@ private async void Export() private void HandleExportProgress(ExportProgress exportProgress) { - string status = exportProgress.IsWriterDisposing ? "Сохранение файла." : null; + string status = exportProgress.IsWriterDisposing ? Localization.SavingFile : null; UpdateProgressStatus(status, exportProgress.ItemsExported, exportProgress.FilesCreated); } @@ -706,14 +708,14 @@ private void UpdateProgressStatus(string status, int itemsExported, int filesCre statusBuilder.Append(status); statusBuilder.Append(" "); } - statusBuilder.Append("Экспортировано строк: "); - statusBuilder.Append(itemsExported); if (filesCreated > 1) { - statusBuilder.Append(", файлов создано: "); - statusBuilder.Append(filesCreated); + statusBuilder.Append(Localization.GetRowCountMultipleFilesString(itemsExported, filesCreated)); + } + else + { + statusBuilder.Append(Localization.GetRowCountSingleFileString(itemsExported)); } - statusBuilder.Append("."); ProgressStatus = statusBuilder.ToString(); } @@ -740,16 +742,16 @@ private void SaveSettings() switch (libgenObjectType) { case LibgenObjectType.NON_FICTION_BOOK: - mainModel.AppSettings.NonFiction.ExportPanel = exportPanelSettngs; + MainModel.AppSettings.NonFiction.ExportPanel = exportPanelSettngs; break; case LibgenObjectType.FICTION_BOOK: - mainModel.AppSettings.Fiction.ExportPanel = exportPanelSettngs; + MainModel.AppSettings.Fiction.ExportPanel = exportPanelSettngs; break; case LibgenObjectType.SCIMAG_ARTICLE: - mainModel.AppSettings.SciMag.ExportPanel = exportPanelSettngs; + MainModel.AppSettings.SciMag.ExportPanel = exportPanelSettngs; break; } - mainModel.SaveSettings(); + MainModel.SaveSettings(); } private void Cancel() @@ -760,7 +762,6 @@ private void Cancel() private void CancelExport() { IsCancelExportButtonEnabled = false; - CancelExportButtonText = "ПРЕРЫВАЕТСЯ..."; cancellationTokenSource.Cancel(); } diff --git a/LibgenDesktop/ViewModels/ImportLogPanelViewModel.cs b/LibgenDesktop/ViewModels/Panels/ImportLogPanelViewModel.cs similarity index 92% rename from LibgenDesktop/ViewModels/ImportLogPanelViewModel.cs rename to LibgenDesktop/ViewModels/Panels/ImportLogPanelViewModel.cs index c10e81e..cc89ee5 100644 --- a/LibgenDesktop/ViewModels/ImportLogPanelViewModel.cs +++ b/LibgenDesktop/ViewModels/Panels/ImportLogPanelViewModel.cs @@ -2,7 +2,7 @@ using System.Collections.ObjectModel; using System.Linq; -namespace LibgenDesktop.ViewModels +namespace LibgenDesktop.ViewModels.Panels { internal class ImportLogPanelViewModel : ViewModel { @@ -13,10 +13,10 @@ internal class ImportLogItemViewModel : ViewModel private string headerText; private ObservableCollection logLines; - public ImportLogItemViewModel(int? stepIndex, string headerText) + public ImportLogItemViewModel(string stepIndex, string headerText) { - this.stepIndex = stepIndex.HasValue ? $"Шаг {stepIndex.Value}" : String.Empty; - isStepIndexVisible = stepIndex.HasValue; + this.stepIndex = stepIndex; + isStepIndexVisible = !String.IsNullOrEmpty(stepIndex); this.headerText = headerText; logLines = new ObservableCollection(); } @@ -161,7 +161,7 @@ public bool IsErrorLogLineVisible } } - public void AddLogItem(int? stepIndex, string headerText, string logLine = null) + public void AddLogItem(string stepIndex, string headerText, string logLine = null) { ImportLogItemViewModel importLogItemViewModel = new ImportLogItemViewModel(stepIndex, headerText); if (logLine != null) diff --git a/LibgenDesktop/ViewModels/SciMagDetailsTabViewModel.cs b/LibgenDesktop/ViewModels/SciMagDetailsTabViewModel.cs deleted file mode 100644 index 8292c4d..0000000 --- a/LibgenDesktop/ViewModels/SciMagDetailsTabViewModel.cs +++ /dev/null @@ -1,125 +0,0 @@ -using System; -using System.Diagnostics; -using LibgenDesktop.Infrastructure; -using LibgenDesktop.Models; -using LibgenDesktop.Models.Entities; -using LibgenDesktop.Models.Utils; - -namespace LibgenDesktop.ViewModels -{ - internal class SciMagDetailsTabViewModel : TabViewModel - { - private SciMagArticle article; - private string downloadButtonCaption; - private bool isDownloadButtonEnabled; - private string disabledDownloadButtonTooltip; - private string articleDownloadUrl; - - public SciMagDetailsTabViewModel(MainModel mainModel, IWindowContext parentWindowContext, SciMagArticle article, bool isInModalWindow) - : base(mainModel, parentWindowContext, article.Title) - { - this.article = article; - IsInModalWindow = isInModalWindow; - DownloadArticleCommand = new Command(DownloadArticle); - CloseCommand = new Command(CloseTab); - Initialize(); - } - - public bool IsInModalWindow { get; } - - public SciMagArticle Article - { - get - { - return article; - } - private set - { - article = value; - NotifyPropertyChanged(); - } - } - - public string DownloadButtonCaption - { - get - { - return downloadButtonCaption; - } - set - { - downloadButtonCaption = value; - NotifyPropertyChanged(); - } - } - - public bool IsDownloadButtonEnabled - { - get - { - return isDownloadButtonEnabled; - } - set - { - isDownloadButtonEnabled = value; - NotifyPropertyChanged(); - } - } - - public string DisabledDownloadButtonTooltip - { - get - { - return disabledDownloadButtonTooltip; - } - set - { - disabledDownloadButtonTooltip = value; - NotifyPropertyChanged(); - } - } - - public Command DownloadArticleCommand { get; } - public Command CloseCommand { get; } - - public event EventHandler CloseTabRequested; - - private void Initialize() - { - bool isInOfflineMode = MainModel.AppSettings.Network.OfflineMode; - string downloadMirrorName = MainModel.AppSettings.Mirrors.ArticlesMirrorMirrorName; - if (downloadMirrorName == null) - { - DownloadButtonCaption = "СКАЧАТЬ"; - IsDownloadButtonEnabled = false; - DisabledDownloadButtonTooltip = "Не выбрано зеркало для загрузки статей"; - articleDownloadUrl = null; - } - else - { - DownloadButtonCaption = "СКАЧАТЬ С " + downloadMirrorName.ToUpper(); - if (isInOfflineMode) - { - IsDownloadButtonEnabled = false; - DisabledDownloadButtonTooltip = "Включен автономный режим"; - articleDownloadUrl = null; - } - else - { - IsDownloadButtonEnabled = true; - articleDownloadUrl = UrlGenerator.GetSciMagDownloadUrl(MainModel.Mirrors[downloadMirrorName], Article); - } - } - } - - private void DownloadArticle() - { - Process.Start(articleDownloadUrl); - } - - private void CloseTab() - { - CloseTabRequested?.Invoke(this, EventArgs.Empty); - } - } -} diff --git a/LibgenDesktop/ViewModels/SciMagDetailsWindowViewModel.cs b/LibgenDesktop/ViewModels/SciMagDetailsWindowViewModel.cs deleted file mode 100644 index 1b13660..0000000 --- a/LibgenDesktop/ViewModels/SciMagDetailsWindowViewModel.cs +++ /dev/null @@ -1,71 +0,0 @@ -using System; -using LibgenDesktop.Infrastructure; -using LibgenDesktop.Models; -using LibgenDesktop.Models.Entities; -using LibgenDesktop.Models.Settings; - -namespace LibgenDesktop.ViewModels -{ - internal class SciMagDetailsWindowViewModel : LibgenWindowViewModel - { - private readonly MainModel mainModel; - private readonly SciMagArticle article; - private readonly bool modalWindow; - private SciMagDetailsTabViewModel tabViewModel; - - public SciMagDetailsWindowViewModel(MainModel mainModel, SciMagArticle article, bool modalWindow) - { - this.mainModel = mainModel; - this.article = article; - this.modalWindow = modalWindow; - tabViewModel = null; - WindowTitle = article.Title; - WindowWidth = mainModel.AppSettings.SciMag.DetailsWindow.Width; - WindowHeight = mainModel.AppSettings.SciMag.DetailsWindow.Height; - WindowClosedCommand = new Command(WindowClosed); - } - - public string WindowTitle { get; private set; } - public int WindowWidth { get; set; } - public int WindowHeight { get; set; } - - public SciMagDetailsTabViewModel TabViewModel - { - get - { - if (tabViewModel == null) - { - tabViewModel = new SciMagDetailsTabViewModel(mainModel, CurrentWindowContext, article, modalWindow); - tabViewModel.CloseTabRequested += CloseTabRequested; - } - return tabViewModel; - } - } - - public Command WindowClosedCommand { get; } - - private void CloseTabRequested(object sender, EventArgs e) - { - TabViewModel.CloseTabRequested -= CloseTabRequested; - IWindowContext currentWindowContext = WindowManager.GetWindowContext(this); - if (modalWindow) - { - currentWindowContext.CloseDialog(false); - } - else - { - currentWindowContext.Close(); - } - } - - private void WindowClosed() - { - mainModel.AppSettings.SciMag.DetailsWindow = new AppSettings.SciMagDetailsWindowSettings - { - Width = WindowWidth, - Height = WindowHeight - }; - mainModel.SaveSettings(); - } - } -} diff --git a/LibgenDesktop/ViewModels/SearchResultItems/FictionSearchResultItemViewModel.cs b/LibgenDesktop/ViewModels/SearchResultItems/FictionSearchResultItemViewModel.cs new file mode 100644 index 0000000..86e3038 --- /dev/null +++ b/LibgenDesktop/ViewModels/SearchResultItems/FictionSearchResultItemViewModel.cs @@ -0,0 +1,29 @@ +using System; +using LibgenDesktop.Models.Entities; +using LibgenDesktop.Models.Localization; + +namespace LibgenDesktop.ViewModels.SearchResultItems +{ + internal class FictionSearchResultItemViewModel : SearchResultItemViewModel + { + public FictionSearchResultItemViewModel(FictionBook book, LanguageFormatter formatter) + : base(book, formatter) + { + } + + public FictionBook Book => LibgenObject; + public string Title => Book.Title; + public string Authors => Book.Authors; + public string Series => Book.Series; + public string Year => Book.Year != "0" ? Book.Year : String.Empty; + public string Publisher => Book.Publisher; + public string Format => Book.Format; + public string FileSize => Formatter.FileSizeToString(Book.SizeInBytes, false); + public long SizeInBytes => Book.SizeInBytes; + + protected override void UpdateLocalizableProperties() + { + NotifyPropertyChanged(nameof(FileSize)); + } + } +} diff --git a/LibgenDesktop/ViewModels/SearchResultItems/NonFictionSearchResultItemViewModel.cs b/LibgenDesktop/ViewModels/SearchResultItems/NonFictionSearchResultItemViewModel.cs new file mode 100644 index 0000000..d97c258 --- /dev/null +++ b/LibgenDesktop/ViewModels/SearchResultItems/NonFictionSearchResultItemViewModel.cs @@ -0,0 +1,29 @@ +using LibgenDesktop.Models.Entities; +using LibgenDesktop.Models.Localization; + +namespace LibgenDesktop.ViewModels.SearchResultItems +{ + internal class NonFictionSearchResultItemViewModel : SearchResultItemViewModel + { + public NonFictionSearchResultItemViewModel(NonFictionBook book, LanguageFormatter formatter) + : base(book, formatter) + { + } + + public NonFictionBook Book => LibgenObject; + public string Title => Book.Title; + public string Authors => Book.Authors; + public string Series => Book.Series; + public string Year => Book.Year; + public string Publisher => Book.Publisher; + public string Format => Book.Format; + public string FileSize => Formatter.FileSizeToString(Book.SizeInBytes, false); + public long SizeInBytes => Book.SizeInBytes; + public bool Ocr => Book.Searchable == "1"; + + protected override void UpdateLocalizableProperties() + { + NotifyPropertyChanged(nameof(FileSize)); + } + } +} diff --git a/LibgenDesktop/ViewModels/SearchResultItems/SciMagSearchResultItemViewModel.cs b/LibgenDesktop/ViewModels/SearchResultItems/SciMagSearchResultItemViewModel.cs new file mode 100644 index 0000000..cda2687 --- /dev/null +++ b/LibgenDesktop/ViewModels/SearchResultItems/SciMagSearchResultItemViewModel.cs @@ -0,0 +1,27 @@ +using LibgenDesktop.Models.Entities; +using LibgenDesktop.Models.Localization; + +namespace LibgenDesktop.ViewModels.SearchResultItems +{ + internal class SciMagSearchResultItemViewModel : SearchResultItemViewModel + { + public SciMagSearchResultItemViewModel(SciMagArticle article, LanguageFormatter formatter) + : base(article, formatter) + { + } + + public SciMagArticle Article => LibgenObject; + public string Title => Article.Title; + public string Authors => Article.Authors; + public string Journal => Article.Journal; + public string Year => Article.Year; + public string FileSize => Formatter.FileSizeToString(Article.SizeInBytes, false); + public long SizeInBytes => Article.SizeInBytes; + public string Doi => Article.DoiString; + + protected override void UpdateLocalizableProperties() + { + NotifyPropertyChanged(nameof(FileSize)); + } + } +} diff --git a/LibgenDesktop/ViewModels/SearchResultItems/SearchResultItemViewModel.cs b/LibgenDesktop/ViewModels/SearchResultItems/SearchResultItemViewModel.cs new file mode 100644 index 0000000..faba3d3 --- /dev/null +++ b/LibgenDesktop/ViewModels/SearchResultItems/SearchResultItemViewModel.cs @@ -0,0 +1,25 @@ +using LibgenDesktop.Models.Entities; +using LibgenDesktop.Models.Localization; + +namespace LibgenDesktop.ViewModels.SearchResultItems +{ + internal abstract class SearchResultItemViewModel : ViewModel where T: LibgenObject + { + public SearchResultItemViewModel(T libgenObject, LanguageFormatter formatter) + { + LibgenObject = libgenObject; + Formatter = formatter; + } + + protected T LibgenObject { get; } + protected LanguageFormatter Formatter { get; private set; } + + public void UpdateLocalization(LanguageFormatter newFormatter) + { + Formatter = newFormatter; + UpdateLocalizableProperties(); + } + + protected abstract void UpdateLocalizableProperties(); + } +} diff --git a/LibgenDesktop/ViewModels/TabViewModel.cs b/LibgenDesktop/ViewModels/TabViewModel.cs deleted file mode 100644 index 33807bc..0000000 --- a/LibgenDesktop/ViewModels/TabViewModel.cs +++ /dev/null @@ -1,36 +0,0 @@ -using LibgenDesktop.Infrastructure; -using LibgenDesktop.Models; - -namespace LibgenDesktop.ViewModels -{ - internal abstract class TabViewModel : ViewModel - { - private string title; - - protected TabViewModel(MainModel mainModel, IWindowContext parentWindowContext, string title) - { - MainModel = mainModel; - ParentWindowContext = parentWindowContext; - this.title = title; - Events = new EventProvider(); - } - - public string Title - { - get - { - return title; - } - set - { - title = value; - NotifyPropertyChanged(); - } - } - - public EventProvider Events { get; } - - protected MainModel MainModel { get; } - protected IWindowContext ParentWindowContext { get; } - } -} diff --git a/LibgenDesktop/ViewModels/Tabs/DetailsTabViewModel.cs b/LibgenDesktop/ViewModels/Tabs/DetailsTabViewModel.cs new file mode 100644 index 0000000..ecf89a8 --- /dev/null +++ b/LibgenDesktop/ViewModels/Tabs/DetailsTabViewModel.cs @@ -0,0 +1,528 @@ +using System; +using System.Diagnostics; +using System.IO; +using System.Threading.Tasks; +using System.Windows.Media.Imaging; +using LibgenDesktop.Common; +using LibgenDesktop.Infrastructure; +using LibgenDesktop.Models; +using LibgenDesktop.Models.Download; +using LibgenDesktop.Models.Entities; +using LibgenDesktop.Models.Localization; +using LibgenDesktop.Models.Localization.Localizators; +using LibgenDesktop.Models.Settings; +using LibgenDesktop.ViewModels.EventArguments; + +namespace LibgenDesktop.ViewModels.Tabs +{ + internal abstract class DetailsTabViewModel : TabViewModel where T: LibgenObject + { + private enum DownloadButtonAction + { + START_DOWNLOAD = 1, + SELECT_DOWNLOAD, + OPEN_FILE + } + + private enum DownloadButtonCaptionOption + { + DOWNLOAD = 1, + QUEUED, + DOWNLOADING, + STOPPED, + ERROR, + OPEN + } + + private enum DownloadActionTextOption + { + DOWNLOAD = 1, + DOWNLOAD_FROM_MIRROR + } + + private enum DownloadButtonTooltipOption + { + NO_TOOLTIP = 1, + NO_DOWNLOAD_MIRROR, + OFFLINE_MODE_IS_ON + } + + private enum CoverNotificationOption + { + NO_NOTIFICATION = 1, + COVER_IS_LOADING, + NO_COVER, + NO_COVER_MIRROR, + NO_COVER_DUE_TO_OFFLINE_MODE, + COVER_LOADING_ERROR + } + + private readonly string downloadMirrorName; + private readonly string coverMirrorName; + private CommonDetailsTabLocalizator localization; + private DownloadButtonCaptionOption downloadButtonCaptionOption; + private DownloadActionTextOption downloadActionTextOption; + private DownloadButtonTooltipOption downloadButtonTooltipOption; + private CoverNotificationOption coverNotificationOption; + private string downloadButtonCaption; + private double downloadProgress; + private bool isDownloadButtonEnabled; + private string disabledDownloadButtonTooltip; + private bool isCoverNotificationVisible; + private string coverNotification; + private bool coverVisible; + private BitmapImage cover; + private DownloadButtonAction downloadButtonAction; + private string downloadUrl; + private Guid? downloadId; + private string downloadedFilePath; + + protected DetailsTabViewModel(MainModel mainModel, IWindowContext parentWindowContext, T libgenObject, string libgenObjectTitle, bool isInModalWindow, + string downloadMirrorName, string coverMirrorName) + : base(mainModel, parentWindowContext, libgenObjectTitle) + { + localization = mainModel.Localization.CurrentLanguage.CommonDetailsTab; + LibgenObject = libgenObject; + IsInModalWindow = isInModalWindow; + this.downloadMirrorName = downloadMirrorName; + this.coverMirrorName = coverMirrorName; + DownloadCommand = new Command(DownloadButtonClick); + Initialize(); + mainModel.Localization.LanguageChanged += LocalizationLanguageChanged; + } + + public bool IsInModalWindow { get; } + + public string DownloadButtonCaption + { + get + { + return downloadButtonCaption; + } + set + { + downloadButtonCaption = value; + NotifyPropertyChanged(); + } + } + + public double DownloadProgress + { + get + { + return downloadProgress; + } + set + { + downloadProgress = value; + NotifyPropertyChanged(); + } + } + + public bool IsDownloadButtonEnabled + { + get + { + return isDownloadButtonEnabled; + } + set + { + isDownloadButtonEnabled = value; + NotifyPropertyChanged(); + } + } + + public string DisabledDownloadButtonTooltip + { + get + { + return disabledDownloadButtonTooltip; + } + set + { + disabledDownloadButtonTooltip = value; + NotifyPropertyChanged(); + } + } + + public bool IsCoverNotificationVisible + { + get + { + return isCoverNotificationVisible; + } + set + { + isCoverNotificationVisible = value; + NotifyPropertyChanged(); + } + } + + public string CoverNotification + { + get + { + return coverNotification; + } + set + { + coverNotification = value; + NotifyPropertyChanged(); + } + } + + public bool CoverVisible + { + get + { + return coverVisible; + } + private set + { + coverVisible = value; + NotifyPropertyChanged(); + } + } + + public BitmapImage Cover + { + get + { + return cover; + } + private set + { + cover = value; + NotifyPropertyChanged(); + } + } + + public Command DownloadCommand { get; } + + protected abstract string FileNameWithoutExtension { get; } + protected abstract string FileExtension { get; } + protected abstract string Md5Hash { get; } + protected abstract bool HasCover { get; } + + protected T LibgenObject { get; } + protected bool IsInOfflineMode => MainModel.AppSettings.Network.OfflineMode; + + public event EventHandler SelectDownloadRequested; + + public override void HandleTabClosing() + { + MainModel.Downloader.DownloaderEvent -= DownloaderEvent; + MainModel.Localization.LanguageChanged -= LocalizationLanguageChanged; + } + + protected abstract string GenerateDownloadUrl(Mirrors.MirrorConfiguration mirrorConfiguration); + protected abstract string GenerateCoverUrl(Mirrors.MirrorConfiguration mirrorConfiguration); + protected abstract string GetDownloadTransformations(Mirrors.MirrorConfiguration mirrorConfiguration); + protected abstract void UpdateLocalization(Language newLanguage); + + private async void Initialize() + { + downloadProgress = 0; + downloadButtonAction = DownloadButtonAction.START_DOWNLOAD; + downloadId = null; + downloadedFilePath = null; + InitializeLibgenObject(); + UpdateDownloadStatus(MainModel.Downloader.GetDownloadItemByDownloadPageUrl(downloadUrl)); + MainModel.Downloader.DownloaderEvent += DownloaderEvent; + await InitializeCoverAsync(); + } + + private void InitializeLibgenObject() + { + downloadButtonCaptionOption = DownloadButtonCaptionOption.DOWNLOAD; + if (downloadMirrorName == null) + { + downloadActionTextOption = DownloadActionTextOption.DOWNLOAD; + IsDownloadButtonEnabled = false; + downloadButtonTooltipOption = DownloadButtonTooltipOption.NO_DOWNLOAD_MIRROR; + downloadUrl = null; + } + else + { + downloadActionTextOption = DownloadActionTextOption.DOWNLOAD_FROM_MIRROR; + if (IsInOfflineMode) + { + IsDownloadButtonEnabled = false; + downloadButtonTooltipOption = DownloadButtonTooltipOption.OFFLINE_MODE_IS_ON; + downloadUrl = null; + } + else + { + IsDownloadButtonEnabled = true; + downloadButtonTooltipOption = DownloadButtonTooltipOption.NO_TOOLTIP; + downloadUrl = GenerateDownloadUrl(MainModel.Mirrors[downloadMirrorName]); + } + } + UpdateDownloadButtonCaption(); + UpdateDownloadButtonTooltip(); + } + + private async Task InitializeCoverAsync() + { + CoverVisible = false; + Cover = null; + if (!HasCover) + { + coverNotificationOption = CoverNotificationOption.NO_COVER; + UpdateCoverNotification(); + IsCoverNotificationVisible = true; + } + else + { + if (coverMirrorName == null) + { + coverNotificationOption = CoverNotificationOption.NO_COVER_MIRROR; + UpdateCoverNotification(); + IsCoverNotificationVisible = true; + } + else + { + if (IsInOfflineMode) + { + coverNotificationOption = CoverNotificationOption.NO_COVER_DUE_TO_OFFLINE_MODE; + UpdateCoverNotification(); + IsCoverNotificationVisible = true; + } + else + { + string coverUrl = GenerateCoverUrl(MainModel.Mirrors[coverMirrorName]); + coverNotificationOption = CoverNotificationOption.COVER_IS_LOADING; + UpdateCoverNotification(); + IsCoverNotificationVisible = true; + try + { + Cover = await LoadCoverAsync(coverUrl); + CoverVisible = true; + coverNotificationOption = CoverNotificationOption.NO_NOTIFICATION; + UpdateCoverNotification(); + IsCoverNotificationVisible = false; + } + catch (Exception exception) + { + Logger.Exception(exception); + coverNotificationOption = CoverNotificationOption.COVER_LOADING_ERROR; + UpdateCoverNotification(); + } + } + } + } + } + + private async Task LoadCoverAsync(string coverUrl) + { + byte[] imageData = await MainModel.HttpClient.GetByteArrayAsync(coverUrl); + BitmapImage result = new BitmapImage(); + using (MemoryStream memoryStream = new MemoryStream(imageData)) + { + result.BeginInit(); + result.CacheOption = BitmapCacheOption.OnLoad; + result.StreamSource = memoryStream; + result.EndInit(); + result.Freeze(); + } + return result; + } + + private void DownloadButtonClick() + { + switch (downloadButtonAction) + { + case DownloadButtonAction.START_DOWNLOAD: + if (MainModel.AppSettings.Download.UseDownloadManager) + { + MainModel.Downloader.EnqueueDownloadItem(downloadUrl, FileNameWithoutExtension, FileExtension.ToLower(), Md5Hash, + GetDownloadTransformations(MainModel.Mirrors[downloadMirrorName])); + } + else + { + Process.Start(downloadUrl); + } + break; + case DownloadButtonAction.SELECT_DOWNLOAD: + SelectDownloadRequested?.Invoke(this, new SelectDownloadEventArgs(downloadId.Value)); + break; + case DownloadButtonAction.OPEN_FILE: + if (File.Exists(downloadedFilePath)) + { + Process.Start(downloadedFilePath); + } + else + { + ShowMessage(localization.ErrorMessageTitle, localization.GetFileNotFoundErrorText(downloadedFilePath)); + } + break; + } + } + + private void DownloaderEvent(object sender, EventArgs e) + { + DownloadItem downloadItem; + switch (e) + { + case DownloadItemAddedEventArgs downloadItemAddedEvent: + downloadItem = downloadItemAddedEvent.AddedDownloadItem; + break; + case DownloadItemChangedEventArgs downloadItemChangedEvent: + downloadItem = downloadItemChangedEvent.ChangedDownloadItem; + break; + case DownloadItemRemovedEventArgs downloadItemRemovedEvent: + downloadItem = downloadItemRemovedEvent.RemovedDownloadItem; + break; + default: + downloadItem = null; + break; + } + UpdateDownloadStatus(downloadItem); + } + + private void UpdateDownloadStatus(DownloadItem downloadItem) + { + if (downloadItem != null && downloadItem.DownloadPageUrl == downloadUrl) + { + ExecuteInUiThread(() => + { + switch (downloadItem.Status) + { + case DownloadItemStatus.QUEUED: + downloadButtonCaptionOption = DownloadButtonCaptionOption.QUEUED; + break; + case DownloadItemStatus.DOWNLOADING: + case DownloadItemStatus.RETRY_DELAY: + downloadButtonCaptionOption = DownloadButtonCaptionOption.DOWNLOADING; + break; + case DownloadItemStatus.STOPPED: + downloadButtonCaptionOption = DownloadButtonCaptionOption.STOPPED; + break; + case DownloadItemStatus.ERROR: + downloadButtonCaptionOption = DownloadButtonCaptionOption.ERROR; + break; + case DownloadItemStatus.COMPLETED: + downloadButtonCaptionOption = DownloadButtonCaptionOption.OPEN; + break; + case DownloadItemStatus.REMOVED: + downloadButtonCaptionOption = DownloadButtonCaptionOption.DOWNLOAD; + break; + } + UpdateDownloadButtonCaption(); + switch (downloadItem.Status) + { + case DownloadItemStatus.QUEUED: + case DownloadItemStatus.DOWNLOADING: + case DownloadItemStatus.RETRY_DELAY: + case DownloadItemStatus.STOPPED: + case DownloadItemStatus.ERROR: + downloadId = downloadItem.Id; + downloadButtonAction = DownloadButtonAction.SELECT_DOWNLOAD; + break; + case DownloadItemStatus.COMPLETED: + downloadedFilePath = Path.Combine(downloadItem.DownloadDirectory, downloadItem.FileName); + downloadButtonAction = DownloadButtonAction.OPEN_FILE; + break; + case DownloadItemStatus.REMOVED: + downloadId = null; + downloadedFilePath = null; + downloadButtonAction = DownloadButtonAction.START_DOWNLOAD; + break; + } + if (downloadItem.Status != DownloadItemStatus.REMOVED && downloadItem.DownloadedFileSize.HasValue && + downloadItem.TotalFileSize.HasValue && downloadItem.DownloadedFileSize.Value < downloadItem.TotalFileSize.Value) + { + DownloadProgress = (double)downloadItem.DownloadedFileSize.Value * 100 / downloadItem.TotalFileSize.Value; + } + else + { + DownloadProgress = 0; + } + }); + } + } + + private void UpdateDownloadButtonCaption() + { + switch (downloadButtonCaptionOption) + { + case DownloadButtonCaptionOption.DOWNLOAD: + switch (downloadActionTextOption) + { + case DownloadActionTextOption.DOWNLOAD: + DownloadButtonCaption = localization.Download; + break; + case DownloadActionTextOption.DOWNLOAD_FROM_MIRROR: + DownloadButtonCaption = localization.GetDownloadFromMirrorText(downloadMirrorName.ToUpper()); + break; + } + break; + case DownloadButtonCaptionOption.QUEUED: + DownloadButtonCaption = localization.Queued; + break; + case DownloadButtonCaptionOption.DOWNLOADING: + DownloadButtonCaption = localization.Downloading; + break; + case DownloadButtonCaptionOption.STOPPED: + DownloadButtonCaption = localization.Stopped; + break; + case DownloadButtonCaptionOption.ERROR: + DownloadButtonCaption = localization.Error; + break; + case DownloadButtonCaptionOption.OPEN: + DownloadButtonCaption = localization.Open; + break; + } + } + + private void UpdateDownloadButtonTooltip() + { + switch (downloadButtonTooltipOption) + { + case DownloadButtonTooltipOption.NO_TOOLTIP: + DisabledDownloadButtonTooltip = null; + break; + case DownloadButtonTooltipOption.NO_DOWNLOAD_MIRROR: + DisabledDownloadButtonTooltip = localization.NoDownloadMirrorTooltip; + break; + case DownloadButtonTooltipOption.OFFLINE_MODE_IS_ON: + DisabledDownloadButtonTooltip = localization.OfflineModeIsOnTooltip; + break; + } + } + + private void UpdateCoverNotification() + { + switch (coverNotificationOption) + { + case CoverNotificationOption.NO_NOTIFICATION: + CoverNotification = null; + break; + case CoverNotificationOption.COVER_IS_LOADING: + CoverNotification = localization.CoverIsLoading; + break; + case CoverNotificationOption.NO_COVER: + CoverNotification = localization.NoCover; + break; + case CoverNotificationOption.NO_COVER_MIRROR: + CoverNotification = localization.NoCoverMirror; + break; + case CoverNotificationOption.NO_COVER_DUE_TO_OFFLINE_MODE: + CoverNotification = localization.NoCoverDueToOfflineMode; + break; + case CoverNotificationOption.COVER_LOADING_ERROR: + CoverNotification = localization.CoverLoadingError; + break; + } + } + + private void LocalizationLanguageChanged(object sender, EventArgs e) + { + localization = MainModel.Localization.CurrentLanguage.CommonDetailsTab; + UpdateDownloadButtonCaption(); + UpdateDownloadButtonTooltip(); + UpdateCoverNotification(); + UpdateLocalization(MainModel.Localization.CurrentLanguage); + } + } +} diff --git a/LibgenDesktop/ViewModels/Tabs/DownloadManagerTabViewModel.cs b/LibgenDesktop/ViewModels/Tabs/DownloadManagerTabViewModel.cs new file mode 100644 index 0000000..1ee36e1 --- /dev/null +++ b/LibgenDesktop/ViewModels/Tabs/DownloadManagerTabViewModel.cs @@ -0,0 +1,691 @@ +using System; +using System.Collections.Generic; +using System.Collections.ObjectModel; +using System.Diagnostics; +using System.IO; +using System.Linq; +using System.Text; +using LibgenDesktop.Infrastructure; +using LibgenDesktop.Models; +using LibgenDesktop.Models.Download; +using LibgenDesktop.Models.Localization.Localizators; + +namespace LibgenDesktop.ViewModels.Tabs +{ + internal class DownloadManagerTabViewModel : TabViewModel + { + internal class DownloadItemViewModel : ViewModel + { + private string name; + private DownloadItemStatus status; + private string progressText; + private double progressValue; + private bool isSelected; + private ObservableCollection logs; + + public DownloadItemViewModel(Guid id, string name, DownloadItemStatus status, string progressText, double progressValue, + string downloadDirectory, IEnumerable logs) + { + Id = id; + this.name = name; + this.status = status; + this.progressText = progressText; + this.progressValue = progressValue; + isSelected = false; + DownloadDirectory = downloadDirectory; + this.logs = new ObservableCollection(logs); + } + + public Guid Id { get; } + + public string Name + { + get + { + return name; + } + set + { + name = value; + NotifyPropertyChanged(); + } + } + + public DownloadItemStatus Status + { + get + { + return status; + } + set + { + status = value; + NotifyPropertyChanged(); + } + } + + public string ProgressText + { + get + { + return progressText; + } + set + { + progressText = value; + NotifyPropertyChanged(); + } + } + + public double ProgressValue + { + get + { + return progressValue; + } + set + { + progressValue = value; + NotifyPropertyChanged(); + } + } + + public bool IsSelected + { + get + { + return isSelected; + } + set + { + isSelected = value; + NotifyPropertyChanged(); + } + } + + public ObservableCollection Logs + { + get + { + return logs; + } + set + { + logs = value; + NotifyPropertyChanged(); + } + } + + public string DownloadDirectory { get; } + } + + internal class DownloadItemLogLineViewModel : ViewModel + { + private DownloadItemLogLineType type; + private string timeStamp; + private string text; + + public DownloadItemLogLineViewModel(DownloadItemLogLineType type, string timeStamp, string text) + { + this.type = type; + this.timeStamp = timeStamp; + this.text = text; + } + + public DownloadItemLogLineType Type + { + get + { + return type; + } + set + { + type = value; + NotifyPropertyChanged(); + } + } + + public string TimeStamp + { + get + { + return timeStamp; + } + set + { + timeStamp = value; + NotifyPropertyChanged(); + } + } + + public string Text + { + get + { + return text; + } + set + { + text = value; + NotifyPropertyChanged(); + } + } + } + + private readonly Dictionary downloadDictionary; + private DownloadManagerLocalizator localization; + private bool isStartButtonEnabled; + private bool isStopButtonEnabled; + private bool isRemoveButtonEnabled; + private bool isStartAllButtonEnabled; + private bool isStopAllButtonEnabled; + private bool isRemoveAllCompletedButtonEnabled; + private int logPanelHeight; + private bool showDebugDownloadLogs; + private ObservableCollection selectedDownloadLogs; + + public DownloadManagerTabViewModel(MainModel mainModel, IWindowContext parentWindowContext) + : base(mainModel, parentWindowContext, mainModel.Localization.CurrentLanguage.DownloadManager.TabTitle) + { + localization = mainModel.Localization.CurrentLanguage.DownloadManager; + Downloads = new ObservableCollection(mainModel.Downloader.GetDownloadQueueSnapshot().Select(ToDownloadItemViewModel)); + downloadDictionary = Downloads.ToDictionary(downloadItem => downloadItem.Id); + SelectionChangedCommand = new Command(SelectionChangedHandler); + DownloaderListBoxDoubleClickCommand = new Command(DownloaderListBoxDoubleClick); + StartSelectedDownloadsCommand = new Command(StartSelectedDownloads); + StopSelectedDownloadsCommand = new Command(StopSelectedDownloads); + RemoveSelectedDownloadsCommand = new Command(RemoveSelectedDownloads); + StartAllDownloadsCommand = new Command(StartAllDownloads); + StopAllDownloadsCommand = new Command(StopAllDownloads); + RemoveAllCompletedDownloadsCommand = new Command(RemoveAllCompletedDownloads); + CopyDownloadLogCommand = new Command(CopyDownloadLog); + Initialize(); + mainModel.Downloader.DownloaderEvent += DownloaderEvent; + mainModel.Localization.LanguageChanged += LocalizationLanguageChanged; + } + + public DownloadManagerLocalizator Localization + { + get + { + return localization; + } + set + { + localization = value; + NotifyPropertyChanged(); + } + } + + public bool IsStartButtonEnabled + { + get + { + return isStartButtonEnabled; + } + set + { + isStartButtonEnabled = value; + NotifyPropertyChanged(); + } + } + + public bool IsStopButtonEnabled + { + get + { + return isStopButtonEnabled; + } + set + { + isStopButtonEnabled = value; + NotifyPropertyChanged(); + } + } + + public bool IsRemoveButtonEnabled + { + get + { + return isRemoveButtonEnabled; + } + set + { + isRemoveButtonEnabled = value; + NotifyPropertyChanged(); + } + } + + public bool IsStartAllButtonEnabled + { + get + { + return isStartAllButtonEnabled; + } + set + { + isStartAllButtonEnabled = value; + NotifyPropertyChanged(); + } + } + + public bool IsStopAllButtonEnabled + { + get + { + return isStopAllButtonEnabled; + } + set + { + isStopAllButtonEnabled = value; + NotifyPropertyChanged(); + } + } + + public bool IsRemoveAllCompletedButtonEnabled + { + get + { + return isRemoveAllCompletedButtonEnabled; + } + set + { + isRemoveAllCompletedButtonEnabled = value; + NotifyPropertyChanged(); + } + } + + public int LogPanelHeight + { + get + { + return logPanelHeight; + } + set + { + logPanelHeight = value; + NotifyPropertyChanged(); + } + } + + public bool ShowDebugDownloadLogs + { + get + { + return showDebugDownloadLogs; + } + set + { + showDebugDownloadLogs = value; + NotifyPropertyChanged(); + } + } + + public ObservableCollection SelectedDownloadLogs + { + get + { + return selectedDownloadLogs; + } + set + { + selectedDownloadLogs = value; + NotifyPropertyChanged(); + } + } + + public ObservableCollection Downloads { get; } + + public Command SelectionChangedCommand { get; } + public Command DownloaderListBoxDoubleClickCommand { get; } + public Command StartSelectedDownloadsCommand { get; } + public Command StopSelectedDownloadsCommand { get; } + public Command RemoveSelectedDownloadsCommand { get; } + public Command StartAllDownloadsCommand { get; } + public Command StopAllDownloadsCommand { get; } + public Command RemoveAllCompletedDownloadsCommand { get; } + public Command CopyDownloadLogCommand { get; } + + private IEnumerable SelectedDownloads + { + get + { + return Downloads.Where(downloadItem => downloadItem.IsSelected); + } + } + + public void SelectDownload(Guid downloadId) + { + ExecuteInUiThread(() => + { + foreach (DownloadItemViewModel downloadItemViewModel in Downloads) + { + downloadItemViewModel.IsSelected = downloadItemViewModel.Id == downloadId; + } + Events.RaiseEvent(ViewModelEvent.RegisteredEventId.SCROLL_TO_SELECTION); + }); + } + + public override void HandleTabClosing() + { + MainModel.Downloader.DownloaderEvent -= DownloaderEvent; + MainModel.Localization.LanguageChanged -= LocalizationLanguageChanged; + } + + private void Initialize() + { + isStartButtonEnabled = false; + isStopButtonEnabled = false; + isRemoveButtonEnabled = false; + isStartAllButtonEnabled = false; + isStopAllButtonEnabled = false; + isRemoveAllCompletedButtonEnabled = false; + logPanelHeight = MainModel.AppSettings.DownloadManagerTab.LogPanelHeight; + showDebugDownloadLogs = MainModel.AppSettings.DownloadManagerTab.ShowDebugLogs; + selectedDownloadLogs = null; + UpdateNonSelectionButtonStates(); + } + + private void DownloaderEvent(object sender, EventArgs e) + { + switch (e) + { + case DownloadItemAddedEventArgs downloadItemAddedEvent: + DownloadItemViewModel newDownloadItemViewModel = ToDownloadItemViewModel(downloadItemAddedEvent.AddedDownloadItem); + downloadDictionary[newDownloadItemViewModel.Id] = newDownloadItemViewModel; + ExecuteInUiThread(() => + { + Downloads.Add(newDownloadItemViewModel); + UpdateNonSelectionButtonStates(); + }); + break; + case DownloadItemChangedEventArgs downloadItemChangedEvent: + DownloadItem changedDownloadItem = downloadItemChangedEvent.ChangedDownloadItem; + DownloadItemViewModel changedDownloadItemViewModel = downloadDictionary[changedDownloadItem.Id]; + bool statusChanged = changedDownloadItemViewModel.Status != changedDownloadItem.Status; + ExecuteInUiThread(() => + { + changedDownloadItemViewModel.Name = changedDownloadItem.FileName; + if (statusChanged) + { + changedDownloadItemViewModel.Status = changedDownloadItem.Status; + } + changedDownloadItemViewModel.ProgressText = GetDownloadProgressText(changedDownloadItem); + changedDownloadItemViewModel.ProgressValue = GetDownloadProgressValue(changedDownloadItem); + if (statusChanged) + { + if (changedDownloadItemViewModel.IsSelected) + { + UpdateSelectionButtonStates(); + } + UpdateNonSelectionButtonStates(); + } + }); + break; + case DownloadItemRemovedEventArgs downloadItemRemovedEvent: + DownloadItem removedDownloadItem = downloadItemRemovedEvent.RemovedDownloadItem; + DownloadItemViewModel removedDownloadItemViewModel = downloadDictionary[removedDownloadItem.Id]; + ExecuteInUiThread(() => + { + bool isSelected = removedDownloadItemViewModel.IsSelected; + Downloads.Remove(removedDownloadItemViewModel); + if (isSelected) + { + UpdateSelectionButtonStates(); + } + UpdateNonSelectionButtonStates(); + }); + break; + case DownloadItemLogLineEventArgs downloadItemLogLineEvent: + DownloadItemViewModel downloadItemViewModel = downloadDictionary[downloadItemLogLineEvent.DownloadItemId]; + if (downloadItemLogLineEvent.LineIndex >= downloadItemViewModel.Logs.Count) + { + ExecuteInUiThread(() => downloadItemViewModel.Logs.Add(ToDownloadItemLogLineViewModel(downloadItemLogLineEvent.LogLine))); + } + break; + } + } + + private void SelectionChangedHandler() + { + UpdateSelectionButtonStates(); + UpdateSelectedDownloadLogs(); + } + + private void UpdateSelectionButtonStates() + { + bool enableStartButton = false; + bool enableStopButton = false; + bool enableRemoveButton = false; + foreach (DownloadItemViewModel downloadItemViewModel in SelectedDownloads) + { + enableRemoveButton = true; + if (CanBeStarted(downloadItemViewModel.Status)) + { + enableStartButton = true; + } + else if (CanBeStopped(downloadItemViewModel.Status)) + { + enableStopButton = true; + } + if (enableStartButton && enableStopButton) + { + break; + } + } + IsStartButtonEnabled = enableStartButton; + IsStopButtonEnabled = enableStopButton; + IsRemoveButtonEnabled = enableRemoveButton; + } + + private void UpdateSelectedDownloadLogs() + { + List selectedDownloads = SelectedDownloads.Take(2).ToList(); + if (selectedDownloads.Count == 1) + { + SelectedDownloadLogs = selectedDownloads.First().Logs; + } + else + { + SelectedDownloadLogs = null; + } + } + + private void UpdateNonSelectionButtonStates() + { + bool enableStartAllButton = false; + bool enableStopAllButton = false; + bool enableRemoveAllCompletedButton = false; + foreach (DownloadItemViewModel downloadItemViewModel in Downloads) + { + if (CanBeStarted(downloadItemViewModel.Status)) + { + enableStartAllButton = true; + } + else if (CanBeStopped(downloadItemViewModel.Status)) + { + enableStopAllButton = true; + } + else if (IsCompleted(downloadItemViewModel.Status)) + { + enableRemoveAllCompletedButton = true; + } + if (enableStartAllButton && enableStopAllButton && enableRemoveAllCompletedButton) + { + break; + } + } + IsStartAllButtonEnabled = enableStartAllButton; + IsStopAllButtonEnabled = enableStopAllButton; + IsRemoveAllCompletedButtonEnabled = enableRemoveAllCompletedButton; + } + + private void DownloaderListBoxDoubleClick() + { + List selectedDownloads = SelectedDownloads.Take(2).ToList(); + if (selectedDownloads.Count == 1) + { + DownloadItemViewModel selectedDownload = selectedDownloads.First(); + if (selectedDownload.Status == DownloadItemStatus.COMPLETED) + { + string downloadedFilePath = Path.Combine(selectedDownload.DownloadDirectory, selectedDownload.Name); + if (File.Exists(downloadedFilePath)) + { + Process.Start(downloadedFilePath); + } + else + { + ShowMessage(Localization.FileNotFoundErrorTitle, Localization.GetFileNotFoundErrorText(downloadedFilePath)); + } + } + } + } + + private void StartSelectedDownloads() + { + MainModel.Downloader.StartDownloads(SelectedDownloads.Where(downloadItem => CanBeStarted(downloadItem.Status)). + Select(downloadItem => downloadItem.Id)); + } + + private void StopSelectedDownloads() + { + MainModel.Downloader.StopDownloads(SelectedDownloads.Where(downloadItem => CanBeStopped(downloadItem.Status)). + Select(downloadItem => downloadItem.Id)); + } + + private void RemoveSelectedDownloads() + { + MainModel.Downloader.RemoveDownloads(SelectedDownloads.Select(downloadItem => downloadItem.Id)); + } + + private void StartAllDownloads() + { + MainModel.Downloader.StartDownloads(Downloads.Where(downloadItem => CanBeStarted(downloadItem.Status)).Select(downloadItem => downloadItem.Id)); + } + + private void StopAllDownloads() + { + MainModel.Downloader.StopDownloads(Downloads.Where(downloadItem => CanBeStopped(downloadItem.Status)).Select(downloadItem => downloadItem.Id)); + } + + private void RemoveAllCompletedDownloads() + { + MainModel.Downloader.RemoveDownloads(Downloads.Where(downloadItem => IsCompleted(downloadItem.Status)).Select(downloadItem => downloadItem.Id)); + } + + private void CopyDownloadLog() + { + if (SelectedDownloadLogs != null) + { + StringBuilder clipboardTextBuilder = new StringBuilder(); + foreach (DownloadItemLogLineViewModel logLine in SelectedDownloadLogs) + { + if (logLine.Type != DownloadItemLogLineType.DEBUG || ShowDebugDownloadLogs) + { + clipboardTextBuilder.Append("["); + clipboardTextBuilder.Append(logLine.TimeStamp); + clipboardTextBuilder.Append("] "); + clipboardTextBuilder.AppendLine(logLine.Text); + } + } + WindowManager.SetClipboardText(clipboardTextBuilder.ToString()); + } + } + + private DownloadItemViewModel ToDownloadItemViewModel(DownloadItem downloadItem) + { + return new DownloadItemViewModel(downloadItem.Id, downloadItem.FileName, downloadItem.Status, + GetDownloadProgressText(downloadItem), GetDownloadProgressValue(downloadItem), downloadItem.DownloadDirectory, + downloadItem.Logs.Select(downloadItemLogLine => ToDownloadItemLogLineViewModel(downloadItemLogLine))); + } + + private string GetDownloadProgressText(DownloadItem downloadItem) + { + if (downloadItem.DownloadedFileSize.HasValue) + { + if (downloadItem.TotalFileSize.HasValue) + { + int percentage = (int)Math.Truncate(GetDownloadProgressValue(downloadItem)); + return Localization.GetDownloadProgressKnownFileSize(downloadItem.DownloadedFileSize.Value, downloadItem.TotalFileSize.Value, percentage); + } + else if (downloadItem.Status == DownloadItemStatus.COMPLETED) + { + return Localization.GetDownloadProgressKnownFileSize(downloadItem.DownloadedFileSize.Value, downloadItem.DownloadedFileSize.Value, 100); + } + else + { + return Localization.GetDownloadProgressUnknownFileSize(downloadItem.DownloadedFileSize.Value); + } + } + else + { + switch (downloadItem.Status) + { + case DownloadItemStatus.QUEUED: + return Localization.QueuedStatus; + case DownloadItemStatus.DOWNLOADING: + return Localization.DownloadingStatus; + case DownloadItemStatus.STOPPED: + return Localization.StoppedStatus; + case DownloadItemStatus.RETRY_DELAY: + return Localization.RetryDelayStatus; + case DownloadItemStatus.ERROR: + return Localization.ErrorStatus; + default: + throw new Exception($"Unexpected download item status: {downloadItem.Status}."); + } + } + } + + private double GetDownloadProgressValue(DownloadItem downloadItem) + { + if (downloadItem.DownloadedFileSize.HasValue && downloadItem.TotalFileSize.HasValue) + { + return (double)downloadItem.DownloadedFileSize.Value * 100 / downloadItem.TotalFileSize.Value; + } + else if (downloadItem.Status == DownloadItemStatus.COMPLETED) + { + return 100; + } + else + { + return 0; + } + } + + private DownloadItemLogLineViewModel ToDownloadItemLogLineViewModel(DownloadItemLogLine downloadItemLogLine) + { + return new DownloadItemLogLineViewModel(downloadItemLogLine.Type, + MainModel.Localization.CurrentLanguage.Formatter.ToFormattedTimeString(downloadItemLogLine.TimeStamp), downloadItemLogLine.Text); + } + + private bool CanBeStarted(DownloadItemStatus downloadItemStatus) + { + return downloadItemStatus == DownloadItemStatus.STOPPED || downloadItemStatus == DownloadItemStatus.ERROR; + } + + private bool CanBeStopped(DownloadItemStatus downloadItemStatus) + { + return downloadItemStatus == DownloadItemStatus.QUEUED || downloadItemStatus == DownloadItemStatus.DOWNLOADING || + downloadItemStatus == DownloadItemStatus.RETRY_DELAY; + } + + private bool IsCompleted(DownloadItemStatus downloadItemStatus) + { + return downloadItemStatus == DownloadItemStatus.COMPLETED; + } + + private void LocalizationLanguageChanged(object sender, EventArgs e) + { + Localization = MainModel.Localization.CurrentLanguage.DownloadManager; + Title = Localization.TabTitle; + foreach (DownloadItem downloadItem in MainModel.Downloader.GetDownloadQueueSnapshot()) + { + if (downloadDictionary.TryGetValue(downloadItem.Id, out DownloadItemViewModel downloadItemViewModel)) + { + downloadItemViewModel.ProgressText = GetDownloadProgressText(downloadItem); + } + } + } + } +} diff --git a/LibgenDesktop/ViewModels/Tabs/FictionDetailsTabViewModel.cs b/LibgenDesktop/ViewModels/Tabs/FictionDetailsTabViewModel.cs new file mode 100644 index 0000000..dcebf3c --- /dev/null +++ b/LibgenDesktop/ViewModels/Tabs/FictionDetailsTabViewModel.cs @@ -0,0 +1,78 @@ +using LibgenDesktop.Infrastructure; +using LibgenDesktop.Models; +using LibgenDesktop.Models.Entities; +using LibgenDesktop.Models.Localization; +using LibgenDesktop.Models.Localization.Localizators; +using LibgenDesktop.Models.Settings; +using LibgenDesktop.Models.Utils; +using LibgenDesktop.ViewModels.DetailsItems; + +namespace LibgenDesktop.ViewModels.Tabs +{ + internal class FictionDetailsTabViewModel : DetailsTabViewModel + { + private FictionDetailsTabLocalizator localization; + private FictionDetailsItemViewModel detailsItem; + + public FictionDetailsTabViewModel(MainModel mainModel, IWindowContext parentWindowContext, FictionBook book, bool isInModalWindow) + : base(mainModel, parentWindowContext, book, book.Title, isInModalWindow, mainModel.AppSettings.Mirrors.FictionBooksMirrorName, + mainModel.AppSettings.Mirrors.FictionCoversMirrorName) + { + localization = mainModel.Localization.CurrentLanguage.FictionDetailsTab; + } + + public FictionDetailsTabLocalizator Localization + { + get + { + return localization; + } + set + { + localization = value; + NotifyPropertyChanged(); + } + } + + public FictionDetailsItemViewModel DetailsItem + { + get + { + if (detailsItem == null) + { + detailsItem = new FictionDetailsItemViewModel(LibgenObject, MainModel.Localization.CurrentLanguage); + } + return detailsItem; + } + } + + protected override string FileNameWithoutExtension => $"{DetailsItem.Authors} - {DetailsItem.Title}"; + + protected override string FileExtension => DetailsItem.Format; + + protected override string Md5Hash => DetailsItem.Md5Hash; + + protected override bool HasCover => DetailsItem.Book.Cover == "1"; + + protected override string GenerateDownloadUrl(Mirrors.MirrorConfiguration mirrorConfiguration) + { + return UrlGenerator.GetFictionDownloadUrl(mirrorConfiguration, DetailsItem.Book); + } + + protected override string GenerateCoverUrl(Mirrors.MirrorConfiguration mirrorConfiguration) + { + return UrlGenerator.GetFictionCoverUrl(mirrorConfiguration, DetailsItem.Book); + } + + protected override string GetDownloadTransformations(Mirrors.MirrorConfiguration mirrorConfiguration) + { + return mirrorConfiguration.FictionDownloadTransformations; + } + + protected override void UpdateLocalization(Language newLanguage) + { + Localization = MainModel.Localization.CurrentLanguage.FictionDetailsTab; + DetailsItem.UpdateLocalization(newLanguage); + } + } +} diff --git a/LibgenDesktop/ViewModels/FictionSearchResultsTabViewModel.cs b/LibgenDesktop/ViewModels/Tabs/FictionSearchResultsTabViewModel.cs similarity index 76% rename from LibgenDesktop/ViewModels/FictionSearchResultsTabViewModel.cs rename to LibgenDesktop/ViewModels/Tabs/FictionSearchResultsTabViewModel.cs index 10cc5db..c0a8ab4 100644 --- a/LibgenDesktop/ViewModels/FictionSearchResultsTabViewModel.cs +++ b/LibgenDesktop/ViewModels/Tabs/FictionSearchResultsTabViewModel.cs @@ -1,29 +1,26 @@ using System; +using System.Collections.Generic; using System.Collections.ObjectModel; +using System.Linq; using System.Threading; using LibgenDesktop.Infrastructure; using LibgenDesktop.Models; using LibgenDesktop.Models.Entities; +using LibgenDesktop.Models.Localization; +using LibgenDesktop.Models.Localization.Localizators; using LibgenDesktop.Models.ProgressArgs; -using LibgenDesktop.Models.Utils; +using LibgenDesktop.ViewModels.EventArguments; +using LibgenDesktop.ViewModels.Panels; +using LibgenDesktop.ViewModels.SearchResultItems; using static LibgenDesktop.Models.Settings.AppSettings; -namespace LibgenDesktop.ViewModels +namespace LibgenDesktop.ViewModels.Tabs { internal class FictionSearchResultsTabViewModel : SearchResultsTabViewModel { - internal class OpenFictionDetailsEventArgs : EventArgs - { - public OpenFictionDetailsEventArgs(FictionBook fictionBook) - { - FictionBook = fictionBook; - } - - public FictionBook FictionBook { get; } - } - private readonly FictionColumnSettings columnSettings; - private ObservableCollection books; + private ObservableCollection books; + private FictionSearchResultsTabLocalizator localization; private string searchQuery; private string bookCount; private bool isBookGridVisible; @@ -33,19 +30,35 @@ public OpenFictionDetailsEventArgs(FictionBook fictionBook) private bool isExportPanelVisible; public FictionSearchResultsTabViewModel(MainModel mainModel, IWindowContext parentWindowContext, string searchQuery, - ObservableCollection searchResults) + List searchResults) : base(mainModel, parentWindowContext, searchQuery) { columnSettings = mainModel.AppSettings.Fiction.Columns; this.searchQuery = searchQuery; - books = searchResults; + LanguageFormatter formatter = MainModel.Localization.CurrentLanguage.Formatter; + books = new ObservableCollection(searchResults.Select(book => + new FictionSearchResultItemViewModel(book, formatter))); ExportPanelViewModel = new ExportPanelViewModel(mainModel, LibgenObjectType.FICTION_BOOK, parentWindowContext); ExportPanelViewModel.ClosePanel += CloseExportPanel; - OpenDetailsCommand = new Command(param => OpenDetails(param as FictionBook)); + OpenDetailsCommand = new Command(param => OpenDetails((param as FictionSearchResultItemViewModel)?.Book)); SearchCommand = new Command(Search); ExportCommand = new Command(ShowExportPanel); BookDataGridEnterKeyCommand = new Command(BookDataGridEnterKeyPressed); Initialize(); + mainModel.Localization.LanguageChanged += LocalizationLanguageChanged; + } + + public FictionSearchResultsTabLocalizator Localization + { + get + { + return localization; + } + set + { + localization = value; + NotifyPropertyChanged(); + } } public string SearchQuery @@ -65,7 +78,7 @@ public string SearchQuery } } - public ObservableCollection Books + public ObservableCollection Books { get { @@ -242,7 +255,7 @@ public bool IsExportPanelVisible public ExportPanelViewModel ExportPanelViewModel { get; } - public FictionBook SelectedBook { get; set; } + public FictionSearchResultItemViewModel SelectedBook { get; set; } public Command OpenDetailsCommand { get; } public Command SearchCommand { get; } @@ -262,8 +275,14 @@ public override void ShowExportPanel() } } + public override void HandleTabClosing() + { + MainModel.Localization.LanguageChanged -= LocalizationLanguageChanged; + } + private void Initialize() { + localization = MainModel.Localization.CurrentLanguage.FictionSearchResultsTab; isBookGridVisible = true; isStatusBarVisible = true; isSearchProgressPanelVisible = false; @@ -274,12 +293,12 @@ private void Initialize() private void UpdateBookCount() { - BookCount = $"Найдено книг: {Books.Count.ToFormattedString()}"; + BookCount = Localization.GetStatusBarText(Books.Count); } private void BookDataGridEnterKeyPressed() { - OpenDetails(SelectedBook); + OpenDetails(SelectedBook.Book); } private void OpenDetails(FictionBook book) @@ -298,7 +317,7 @@ private async void Search() UpdateSearchProgressStatus(0); Progress searchProgressHandler = new Progress(HandleSearchProgress); CancellationToken cancellationToken = new CancellationToken(); - ObservableCollection result = new ObservableCollection(); + List result = new List(); try { result = await MainModel.SearchFictionAsync(SearchQuery, searchProgressHandler, cancellationToken); @@ -307,7 +326,9 @@ private async void Search() { ShowErrorWindow(exception, ParentWindowContext); } - Books = result; + LanguageFormatter formatter = MainModel.Localization.CurrentLanguage.Formatter; + Books = new ObservableCollection(result.Select(book => + new FictionSearchResultItemViewModel(book, formatter))); UpdateBookCount(); IsSearchProgressPanelVisible = false; IsBookGridVisible = true; @@ -322,7 +343,7 @@ private void HandleSearchProgress(SearchProgress searchProgress) private void UpdateSearchProgressStatus(int booksFound) { - SearchProgressStatus = $"Найдено книг: {booksFound.ToFormattedString()}"; + SearchProgressStatus = Localization.GetSearchProgressText(booksFound); } private void CloseExportPanel(object sender, EventArgs e) @@ -331,5 +352,18 @@ private void CloseExportPanel(object sender, EventArgs e) IsBookGridVisible = true; IsStatusBarVisible = true; } + + private void LocalizationLanguageChanged(object sender, EventArgs e) + { + Language newLanguage = MainModel.Localization.CurrentLanguage; + Localization = newLanguage.FictionSearchResultsTab; + UpdateBookCount(); + LanguageFormatter newFormatter = newLanguage.Formatter; + foreach (FictionSearchResultItemViewModel book in Books) + { + book.UpdateLocalization(newFormatter); + } + ExportPanelViewModel.UpdateLocalization(newLanguage); + } } } diff --git a/LibgenDesktop/ViewModels/Tabs/NonFictionDetailsTabViewModel.cs b/LibgenDesktop/ViewModels/Tabs/NonFictionDetailsTabViewModel.cs new file mode 100644 index 0000000..a20485e --- /dev/null +++ b/LibgenDesktop/ViewModels/Tabs/NonFictionDetailsTabViewModel.cs @@ -0,0 +1,76 @@ +using System; +using LibgenDesktop.Infrastructure; +using LibgenDesktop.Models; +using LibgenDesktop.Models.Entities; +using LibgenDesktop.Models.Localization; +using LibgenDesktop.Models.Localization.Localizators; +using LibgenDesktop.Models.Settings; +using LibgenDesktop.Models.Utils; +using LibgenDesktop.ViewModels.DetailsItems; + +namespace LibgenDesktop.ViewModels.Tabs +{ + internal class NonFictionDetailsTabViewModel : DetailsTabViewModel + { + private NonFictionDetailsTabLocalizator localization; + private NonFictionDetailsItemViewModel detailsItem; + + public NonFictionDetailsTabViewModel(MainModel mainModel, IWindowContext parentWindowContext, NonFictionBook book, bool isInModalWindow) + : base(mainModel, parentWindowContext, book, book.Title, isInModalWindow, mainModel.AppSettings.Mirrors.NonFictionBooksMirrorName, + mainModel.AppSettings.Mirrors.NonFictionCoversMirrorName) + { + localization = mainModel.Localization.CurrentLanguage.NonFictionDetailsTab; + } + + public NonFictionDetailsTabLocalizator Localization + { + get + { + return localization; + } + set + { + localization = value; + NotifyPropertyChanged(); + } + } + + public NonFictionDetailsItemViewModel DetailsItem + { + get + { + if (detailsItem == null) + { + detailsItem = new NonFictionDetailsItemViewModel(LibgenObject, MainModel.Localization.CurrentLanguage); + } + return detailsItem; + } + } + + protected override string FileNameWithoutExtension => $"{DetailsItem.Authors} - {DetailsItem.Title}"; + protected override string FileExtension => DetailsItem.Format; + protected override string Md5Hash => DetailsItem.Md5Hash; + protected override bool HasCover => !String.IsNullOrWhiteSpace(DetailsItem.Book.CoverUrl); + + protected override string GenerateDownloadUrl(Mirrors.MirrorConfiguration mirrorConfiguration) + { + return UrlGenerator.GetNonFictionDownloadUrl(mirrorConfiguration, DetailsItem.Book); + } + + protected override string GenerateCoverUrl(Mirrors.MirrorConfiguration mirrorConfiguration) + { + return UrlGenerator.GetNonFictionCoverUrl(mirrorConfiguration, DetailsItem.Book); + } + + protected override string GetDownloadTransformations(Mirrors.MirrorConfiguration mirrorConfiguration) + { + return mirrorConfiguration.NonFictionDownloadTransformations; + } + + protected override void UpdateLocalization(Language newLanguage) + { + Localization = MainModel.Localization.CurrentLanguage.NonFictionDetailsTab; + DetailsItem.UpdateLocalization(newLanguage); + } + } +} diff --git a/LibgenDesktop/ViewModels/NonFictionSearchResultsTabViewModel.cs b/LibgenDesktop/ViewModels/Tabs/NonFictionSearchResultsTabViewModel.cs similarity index 76% rename from LibgenDesktop/ViewModels/NonFictionSearchResultsTabViewModel.cs rename to LibgenDesktop/ViewModels/Tabs/NonFictionSearchResultsTabViewModel.cs index 64ec444..7e20783 100644 --- a/LibgenDesktop/ViewModels/NonFictionSearchResultsTabViewModel.cs +++ b/LibgenDesktop/ViewModels/Tabs/NonFictionSearchResultsTabViewModel.cs @@ -1,29 +1,26 @@ using System; +using System.Collections.Generic; using System.Collections.ObjectModel; +using System.Linq; using System.Threading; using LibgenDesktop.Infrastructure; using LibgenDesktop.Models; using LibgenDesktop.Models.Entities; +using LibgenDesktop.Models.Localization; +using LibgenDesktop.Models.Localization.Localizators; using LibgenDesktop.Models.ProgressArgs; -using LibgenDesktop.Models.Utils; +using LibgenDesktop.ViewModels.EventArguments; +using LibgenDesktop.ViewModels.Panels; +using LibgenDesktop.ViewModels.SearchResultItems; using static LibgenDesktop.Models.Settings.AppSettings; -namespace LibgenDesktop.ViewModels +namespace LibgenDesktop.ViewModels.Tabs { internal class NonFictionSearchResultsTabViewModel : SearchResultsTabViewModel { - internal class OpenNonFictionDetailsEventArgs : EventArgs - { - public OpenNonFictionDetailsEventArgs(NonFictionBook nonFictionBook) - { - NonFictionBook = nonFictionBook; - } - - public NonFictionBook NonFictionBook { get; } - } - private readonly NonFictionColumnSettings columnSettings; - private ObservableCollection books; + private ObservableCollection books; + private NonFictionSearchResultsTabLocalizator localization; private string searchQuery; private string bookCount; private bool isBookGridVisible; @@ -33,19 +30,35 @@ public OpenNonFictionDetailsEventArgs(NonFictionBook nonFictionBook) private bool isExportPanelVisible; public NonFictionSearchResultsTabViewModel(MainModel mainModel, IWindowContext parentWindowContext, string searchQuery, - ObservableCollection searchResults) + List searchResults) : base(mainModel, parentWindowContext, searchQuery) { columnSettings = mainModel.AppSettings.NonFiction.Columns; this.searchQuery = searchQuery; - books = searchResults; + LanguageFormatter formatter = MainModel.Localization.CurrentLanguage.Formatter; + books = new ObservableCollection(searchResults.Select(book => + new NonFictionSearchResultItemViewModel(book, formatter))); ExportPanelViewModel = new ExportPanelViewModel(mainModel, LibgenObjectType.NON_FICTION_BOOK, parentWindowContext); ExportPanelViewModel.ClosePanel += CloseExportPanel; - OpenDetailsCommand = new Command(param => OpenDetails(param as NonFictionBook)); + OpenDetailsCommand = new Command(param => OpenDetails((param as NonFictionSearchResultItemViewModel)?.Book)); SearchCommand = new Command(Search); ExportCommand = new Command(ShowExportPanel); BookDataGridEnterKeyCommand = new Command(BookDataGridEnterKeyPressed); Initialize(); + mainModel.Localization.LanguageChanged += LocalizationLanguageChanged; + } + + public NonFictionSearchResultsTabLocalizator Localization + { + get + { + return localization; + } + set + { + localization = value; + NotifyPropertyChanged(); + } } public string SearchQuery @@ -65,7 +78,7 @@ public string SearchQuery } } - public ObservableCollection Books + public ObservableCollection Books { get { @@ -254,7 +267,7 @@ public bool IsExportPanelVisible public ExportPanelViewModel ExportPanelViewModel { get; } - public NonFictionBook SelectedBook { get; set; } + public NonFictionSearchResultItemViewModel SelectedBook { get; set; } public Command OpenDetailsCommand { get; } public Command SearchCommand { get; } @@ -274,8 +287,14 @@ public override void ShowExportPanel() } } + public override void HandleTabClosing() + { + MainModel.Localization.LanguageChanged -= LocalizationLanguageChanged; + } + private void Initialize() { + localization = MainModel.Localization.CurrentLanguage.NonFictionSearchResultsTab; isBookGridVisible = true; isStatusBarVisible = true; isSearchProgressPanelVisible = false; @@ -286,12 +305,12 @@ private void Initialize() private void UpdateBookCount() { - BookCount = $"Найдено книг: {Books.Count.ToFormattedString()}"; + BookCount = Localization.GetStatusBarText(Books.Count); } private void BookDataGridEnterKeyPressed() { - OpenDetails(SelectedBook); + OpenDetails(SelectedBook.Book); } private void OpenDetails(NonFictionBook book) @@ -310,7 +329,7 @@ private async void Search() UpdateSearchProgressStatus(0); Progress searchProgressHandler = new Progress(HandleSearchProgress); CancellationToken cancellationToken = new CancellationToken(); - ObservableCollection result = new ObservableCollection(); + List result = new List(); try { result = await MainModel.SearchNonFictionAsync(SearchQuery, searchProgressHandler, cancellationToken); @@ -319,7 +338,9 @@ private async void Search() { ShowErrorWindow(exception, ParentWindowContext); } - Books = result; + LanguageFormatter formatter = MainModel.Localization.CurrentLanguage.Formatter; + Books = new ObservableCollection(result.Select(book => + new NonFictionSearchResultItemViewModel(book, formatter))); UpdateBookCount(); IsSearchProgressPanelVisible = false; IsBookGridVisible = true; @@ -334,7 +355,7 @@ private void HandleSearchProgress(SearchProgress searchProgress) private void UpdateSearchProgressStatus(int booksFound) { - SearchProgressStatus = $"Найдено книг: {booksFound.ToFormattedString()}"; + SearchProgressStatus = Localization.GetSearchProgressText(booksFound); } private void CloseExportPanel(object sender, EventArgs e) @@ -343,5 +364,18 @@ private void CloseExportPanel(object sender, EventArgs e) IsBookGridVisible = true; IsStatusBarVisible = true; } + + private void LocalizationLanguageChanged(object sender, EventArgs e) + { + Language newLanguage = MainModel.Localization.CurrentLanguage; + Localization = newLanguage.NonFictionSearchResultsTab; + UpdateBookCount(); + LanguageFormatter newFormatter = newLanguage.Formatter; + foreach (NonFictionSearchResultItemViewModel book in Books) + { + book.UpdateLocalization(newFormatter); + } + ExportPanelViewModel.UpdateLocalization(newLanguage); + } } } diff --git a/LibgenDesktop/ViewModels/Tabs/SciMagDetailsTabViewModel.cs b/LibgenDesktop/ViewModels/Tabs/SciMagDetailsTabViewModel.cs new file mode 100644 index 0000000..4d06104 --- /dev/null +++ b/LibgenDesktop/ViewModels/Tabs/SciMagDetailsTabViewModel.cs @@ -0,0 +1,77 @@ +using LibgenDesktop.Infrastructure; +using LibgenDesktop.Models; +using LibgenDesktop.Models.Entities; +using LibgenDesktop.Models.Localization; +using LibgenDesktop.Models.Localization.Localizators; +using LibgenDesktop.Models.Settings; +using LibgenDesktop.Models.Utils; +using LibgenDesktop.ViewModels.DetailsItems; + +namespace LibgenDesktop.ViewModels.Tabs +{ + internal class SciMagDetailsTabViewModel : DetailsTabViewModel + { + private SciMagDetailsTabLocalizator localization; + private SciMagDetailsItemViewModel detailsItem; + + public SciMagDetailsTabViewModel(MainModel mainModel, IWindowContext parentWindowContext, SciMagArticle article, bool isInModalWindow) + : base(mainModel, parentWindowContext, article, article.Title, isInModalWindow, mainModel.AppSettings.Mirrors.ArticlesMirrorMirrorName, null) + { + localization = mainModel.Localization.CurrentLanguage.SciMagDetailsTab; + } + + public SciMagDetailsTabLocalizator Localization + { + get + { + return localization; + } + set + { + localization = value; + NotifyPropertyChanged(); + } + } + + public SciMagDetailsItemViewModel DetailsItem + { + get + { + if (detailsItem == null) + { + detailsItem = new SciMagDetailsItemViewModel(LibgenObject, MainModel.Localization.CurrentLanguage); + } + return detailsItem; + } + } + + protected override string FileNameWithoutExtension => $"{DetailsItem.Authors} - {DetailsItem.Title}"; + + protected override string FileExtension => "pdf"; + + protected override string Md5Hash => DetailsItem.Md5Hash; + + protected override bool HasCover => false; + + protected override string GenerateDownloadUrl(Mirrors.MirrorConfiguration mirrorConfiguration) + { + return UrlGenerator.GetSciMagDownloadUrl(mirrorConfiguration, DetailsItem.Article); + } + + protected override string GenerateCoverUrl(Mirrors.MirrorConfiguration mirrorConfiguration) + { + return null; + } + + protected override string GetDownloadTransformations(Mirrors.MirrorConfiguration mirrorConfiguration) + { + return mirrorConfiguration.SciMagDownloadTransformations; + } + + protected override void UpdateLocalization(Language newLanguage) + { + Localization = MainModel.Localization.CurrentLanguage.SciMagDetailsTab; + DetailsItem.UpdateLocalization(newLanguage); + } + } +} diff --git a/LibgenDesktop/ViewModels/SciMagSearchResultsTabViewModel.cs b/LibgenDesktop/ViewModels/Tabs/SciMagSearchResultsTabViewModel.cs similarity index 75% rename from LibgenDesktop/ViewModels/SciMagSearchResultsTabViewModel.cs rename to LibgenDesktop/ViewModels/Tabs/SciMagSearchResultsTabViewModel.cs index 5e5abed..fe5ed83 100644 --- a/LibgenDesktop/ViewModels/SciMagSearchResultsTabViewModel.cs +++ b/LibgenDesktop/ViewModels/Tabs/SciMagSearchResultsTabViewModel.cs @@ -1,29 +1,27 @@ using System; +using System.Collections.Generic; using System.Collections.ObjectModel; +using System.Linq; using System.Threading; using LibgenDesktop.Infrastructure; using LibgenDesktop.Models; using LibgenDesktop.Models.Entities; +using LibgenDesktop.Models.Localization; +using LibgenDesktop.Models.Localization.Localizators; using LibgenDesktop.Models.ProgressArgs; using LibgenDesktop.Models.Utils; +using LibgenDesktop.ViewModels.EventArguments; +using LibgenDesktop.ViewModels.Panels; +using LibgenDesktop.ViewModels.SearchResultItems; using static LibgenDesktop.Models.Settings.AppSettings; -namespace LibgenDesktop.ViewModels +namespace LibgenDesktop.ViewModels.Tabs { internal class SciMagSearchResultsTabViewModel : SearchResultsTabViewModel { - internal class OpenSciMagDetailsEventArgs : EventArgs - { - public OpenSciMagDetailsEventArgs(SciMagArticle sciMagArticle) - { - SciMagArticle = sciMagArticle; - } - - public SciMagArticle SciMagArticle { get; } - } - private readonly SciMagColumnSettings columnSettings; - private ObservableCollection articles; + private ObservableCollection articles; + private SciMagSearchResultsTabLocalizator localization; private string searchQuery; private string articleCount; private bool isArticleGridVisible; @@ -33,19 +31,35 @@ public OpenSciMagDetailsEventArgs(SciMagArticle sciMagArticle) private bool isExportPanelVisible; public SciMagSearchResultsTabViewModel(MainModel mainModel, IWindowContext parentWindowContext, string searchQuery, - ObservableCollection searchResults) + List searchResults) : base(mainModel, parentWindowContext, searchQuery) { columnSettings = mainModel.AppSettings.SciMag.Columns; this.searchQuery = searchQuery; - articles = searchResults; + LanguageFormatter formatter = MainModel.Localization.CurrentLanguage.Formatter; + articles = new ObservableCollection(searchResults.Select(article => + new SciMagSearchResultItemViewModel(article, formatter))); ExportPanelViewModel = new ExportPanelViewModel(mainModel, LibgenObjectType.SCIMAG_ARTICLE, parentWindowContext); ExportPanelViewModel.ClosePanel += CloseExportPanel; - OpenDetailsCommand = new Command(param => OpenDetails(param as SciMagArticle)); + OpenDetailsCommand = new Command(param => OpenDetails((param as SciMagSearchResultItemViewModel)?.Article)); SearchCommand = new Command(Search); ExportCommand = new Command(ShowExportPanel); ArticleDataGridEnterKeyCommand = new Command(ArticleDataGridEnterKeyPressed); Initialize(); + mainModel.Localization.LanguageChanged += LocalizationLanguageChanged; + } + + public SciMagSearchResultsTabLocalizator Localization + { + get + { + return localization; + } + set + { + localization = value; + NotifyPropertyChanged(); + } } public string SearchQuery @@ -65,7 +79,7 @@ public string SearchQuery } } - public ObservableCollection Articles + public ObservableCollection Articles { get { @@ -230,7 +244,7 @@ public bool IsExportPanelVisible public ExportPanelViewModel ExportPanelViewModel { get; } - public SciMagArticle SelectedArticle { get; set; } + public SciMagSearchResultItemViewModel SelectedArticle { get; set; } public Command OpenDetailsCommand { get; } public Command SearchCommand { get; } @@ -250,8 +264,14 @@ public override void ShowExportPanel() } } + public override void HandleTabClosing() + { + MainModel.Localization.LanguageChanged -= LocalizationLanguageChanged; + } + private void Initialize() { + localization = MainModel.Localization.CurrentLanguage.SciMagSearchResultsTab; isArticleGridVisible = true; isStatusBarVisible = true; isSearchProgressPanelVisible = false; @@ -262,12 +282,12 @@ private void Initialize() private void UpdateArticleCount() { - ArticleCount = $"Найдено статей: {Articles.Count.ToFormattedString()}"; + ArticleCount = Localization.GetStatusBarText(Articles.Count); } private void ArticleDataGridEnterKeyPressed() { - OpenDetails(SelectedArticle); + OpenDetails(SelectedArticle.Article); } private void OpenDetails(SciMagArticle article) @@ -286,7 +306,7 @@ private async void Search() UpdateSearchProgressStatus(0); Progress searchProgressHandler = new Progress(HandleSearchProgress); CancellationToken cancellationToken = new CancellationToken(); - ObservableCollection result = new ObservableCollection(); + List result = new List(); try { result = await MainModel.SearchSciMagAsync(SearchQuery, searchProgressHandler, cancellationToken); @@ -295,7 +315,9 @@ private async void Search() { ShowErrorWindow(exception, ParentWindowContext); } - Articles = result; + LanguageFormatter formatter = MainModel.Localization.CurrentLanguage.Formatter; + Articles = new ObservableCollection(result.Select(article => + new SciMagSearchResultItemViewModel(article, formatter))); UpdateArticleCount(); IsSearchProgressPanelVisible = false; IsArticleGridVisible = true; @@ -310,7 +332,7 @@ private void HandleSearchProgress(SearchProgress searchProgress) private void UpdateSearchProgressStatus(int articlesFound) { - SearchProgressStatus = $"Найдено статей: {articlesFound.ToFormattedString()}"; + SearchProgressStatus = Localization.GetSearchProgressText(articlesFound); } private void CloseExportPanel(object sender, EventArgs e) @@ -319,5 +341,18 @@ private void CloseExportPanel(object sender, EventArgs e) IsArticleGridVisible = true; IsStatusBarVisible = true; } + + private void LocalizationLanguageChanged(object sender, EventArgs e) + { + Language newLanguage = MainModel.Localization.CurrentLanguage; + Localization = newLanguage.SciMagSearchResultsTab; + UpdateArticleCount(); + LanguageFormatter newFormatter = newLanguage.Formatter; + foreach (SciMagSearchResultItemViewModel article in Articles) + { + article.UpdateLocalization(newFormatter); + } + ExportPanelViewModel.UpdateLocalization(newLanguage); + } } } diff --git a/LibgenDesktop/ViewModels/SearchResultsTabViewModel.cs b/LibgenDesktop/ViewModels/Tabs/SearchResultsTabViewModel.cs similarity index 90% rename from LibgenDesktop/ViewModels/SearchResultsTabViewModel.cs rename to LibgenDesktop/ViewModels/Tabs/SearchResultsTabViewModel.cs index 7eae2fa..f557e42 100644 --- a/LibgenDesktop/ViewModels/SearchResultsTabViewModel.cs +++ b/LibgenDesktop/ViewModels/Tabs/SearchResultsTabViewModel.cs @@ -1,7 +1,7 @@ using LibgenDesktop.Infrastructure; using LibgenDesktop.Models; -namespace LibgenDesktop.ViewModels +namespace LibgenDesktop.ViewModels.Tabs { internal abstract class SearchResultsTabViewModel : TabViewModel { diff --git a/LibgenDesktop/ViewModels/SearchTabViewModel.cs b/LibgenDesktop/ViewModels/Tabs/SearchTabViewModel.cs similarity index 85% rename from LibgenDesktop/ViewModels/SearchTabViewModel.cs rename to LibgenDesktop/ViewModels/Tabs/SearchTabViewModel.cs index bb3afc3..e95ce1b 100644 --- a/LibgenDesktop/ViewModels/SearchTabViewModel.cs +++ b/LibgenDesktop/ViewModels/Tabs/SearchTabViewModel.cs @@ -1,29 +1,19 @@ using System; -using System.Collections.ObjectModel; +using System.Collections.Generic; using System.Threading; using System.Threading.Tasks; using LibgenDesktop.Infrastructure; using LibgenDesktop.Models; using LibgenDesktop.Models.Entities; +using LibgenDesktop.Models.Localization.Localizators; using LibgenDesktop.Models.ProgressArgs; -using LibgenDesktop.Models.Utils; +using LibgenDesktop.ViewModels.EventArguments; -namespace LibgenDesktop.ViewModels +namespace LibgenDesktop.ViewModels.Tabs { internal class SearchTabViewModel : TabViewModel { - internal class SearchCompleteEventArgs : EventArgs - { - public SearchCompleteEventArgs(string searchQuery, ObservableCollection searchResult) - { - SearchQuery = searchQuery; - SearchResult = searchResult; - } - - public string SearchQuery { get; } - public ObservableCollection SearchResult { get; } - } - + private SearchTabLocalizator localization; private bool isSearchBlockVisible; private bool isSearchParamsPanelVisible; private string searchQuery; @@ -40,11 +30,25 @@ public SearchCompleteEventArgs(string searchQuery, ObservableCollection searc private bool isImportBlockVisible; public SearchTabViewModel(MainModel mainModel, IWindowContext parentWindowContext) - : base(mainModel, parentWindowContext, "Поиск") + : base(mainModel, parentWindowContext, mainModel.Localization.CurrentLanguage.SearchTab.TabTitle) { ImportCommand = new Command(Import); SearchCommand = new Command(Search); Initialize(); + mainModel.Localization.LanguageChanged += LocalizationLanguageChanged; + } + + public SearchTabLocalizator Localization + { + get + { + return localization; + } + set + { + localization = value; + NotifyPropertyChanged(); + } } public bool IsSearchBlockVisible @@ -299,8 +303,14 @@ public void SetFocus() Events.RaiseEvent(ViewModelEvent.RegisteredEventId.FOCUS_SEARCH_TEXT_BOX); } + public override void HandleTabClosing() + { + MainModel.Localization.LanguageChanged -= LocalizationLanguageChanged; + } + private void Initialize() { + localization = MainModel.Localization.CurrentLanguage.SearchTab; isSearchBlockVisible = false; isSearchParamsPanelVisible = false; isLibrarySelectorVisible = false; @@ -324,15 +334,15 @@ private void UpdateSearchBoxHint() { if (IsNonFictionLibrarySelected) { - SearchBoxHint = "Поиск по наименованию, авторам, серии, издателю и ISBN без дефисов"; + SearchBoxHint = Localization.NonFictionSearchBoxTooltip; } else if (IsFictionLibrarySelected) { - SearchBoxHint = "Поиск по наименованию, авторам, серии, издателю и ISBN с дефисами"; + SearchBoxHint = Localization.FictionSearchBoxTooltip; } else if (IsSciMagLibrarySelected) { - SearchBoxHint = "Поиск по наименованию, авторам, журналу, DOI, Pubmed ID и ISSN (p/e)"; + SearchBoxHint = Localization.SciMagSearchBoxTooltip; } } @@ -358,12 +368,13 @@ private void Search() } } - private async void SearchItemsAsync(Func, CancellationToken, Task>> searchFunction, + private async void SearchItemsAsync(Func, CancellationToken, Task>> searchFunction, string searchQuery, EventHandler> searchCompleteEventHandler) + where T: LibgenObject { Progress searchProgressHandler = new Progress(HandleSearchProgress); CancellationToken cancellationToken = new CancellationToken(); - ObservableCollection result = null; + List result = null; bool error = false; try { @@ -394,15 +405,25 @@ private void HandleSearchProgress(SearchProgress searchProgress) private void UpdateSearchProgressStatus(int itemsFound) { - string itemsFoundString = itemsFound.ToFormattedString(); - if (IsNonFictionLibrarySelected || IsFictionLibrarySelected) + if (IsNonFictionLibrarySelected) { - SearchProgressStatus = $"Найдено книг: {itemsFoundString}"; + SearchProgressStatus = Localization.GetNonFictionSearchProgressText(itemsFound); + } + else if (IsFictionLibrarySelected) + { + SearchProgressStatus = Localization.GetFictionSearchProgressText(itemsFound); } else if (IsSciMagLibrarySelected) { - SearchProgressStatus = $"Найдено статей: {itemsFoundString}"; + SearchProgressStatus = Localization.GetSciMagSearchProgressText(itemsFound); } } + + private void LocalizationLanguageChanged(object sender, EventArgs e) + { + Localization = MainModel.Localization.CurrentLanguage.SearchTab; + Title = Localization.TabTitle; + UpdateSearchBoxHint(); + } } } diff --git a/LibgenDesktop/ViewModels/Tabs/TabViewModel.cs b/LibgenDesktop/ViewModels/Tabs/TabViewModel.cs new file mode 100644 index 0000000..7f1e7e7 --- /dev/null +++ b/LibgenDesktop/ViewModels/Tabs/TabViewModel.cs @@ -0,0 +1,67 @@ +using System; +using System.Threading; +using LibgenDesktop.Infrastructure; +using LibgenDesktop.Models; + +namespace LibgenDesktop.ViewModels.Tabs +{ + internal abstract class TabViewModel : ContainerViewModel + { + private readonly SynchronizationContext synchronizationContext; + private string title; + + protected TabViewModel(MainModel mainModel, IWindowContext parentWindowContext, string title) + : base(mainModel) + { + synchronizationContext = SynchronizationContext.Current; + ParentWindowContext = parentWindowContext; + this.title = title; + RequestCloseCommand = new Command(RequestCloseTab); + Events = new EventProvider(); + } + + public string Title + { + get + { + return title; + } + set + { + title = value; + NotifyPropertyChanged(); + } + } + + public Command RequestCloseCommand { get; } + public EventProvider Events { get; } + + public event EventHandler CloseTabRequested; + + protected IWindowContext ParentWindowContext { get; } + + public virtual void HandleTabClosing() + { + } + + protected void ShowMessage(string title, string text) + { + ShowMessage(title, text, ParentWindowContext); + } + + protected bool ShowPrompt(string title, string text) + { + return ShowPrompt(title, text, ParentWindowContext); + } + + protected void ExecuteInUiThread(Action action) + { + synchronizationContext.Post(state => action(), null); + } + + private void RequestCloseTab() + { + CloseTabRequested?.Invoke(this, EventArgs.Empty); + } + } +} diff --git a/LibgenDesktop/ViewModels/ViewModel.cs b/LibgenDesktop/ViewModels/ViewModel.cs index f8d51ca..075d71e 100644 --- a/LibgenDesktop/ViewModels/ViewModel.cs +++ b/LibgenDesktop/ViewModels/ViewModel.cs @@ -1,8 +1,5 @@ -using System; -using System.ComponentModel; +using System.ComponentModel; using System.Runtime.CompilerServices; -using LibgenDesktop.Common; -using LibgenDesktop.Infrastructure; namespace LibgenDesktop.ViewModels { @@ -14,13 +11,5 @@ protected void NotifyPropertyChanged([CallerMemberName]string propertyName = "") { PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName)); } - - protected void ShowErrorWindow(Exception exception, IWindowContext parentWindowContext) - { - Logger.Exception(exception); - ErrorWindowViewModel errorWindowViewModel = new ErrorWindowViewModel(exception.ToString()); - IWindowContext errorWindowContext = WindowManager.CreateWindow(RegisteredWindows.WindowKey.ERROR_WINDOW, errorWindowViewModel, parentWindowContext); - errorWindowContext.ShowDialog(); - } } } diff --git a/LibgenDesktop/ViewModels/ApplicationUpdateWindowViewModel.cs b/LibgenDesktop/ViewModels/Windows/ApplicationUpdateWindowViewModel.cs similarity index 89% rename from LibgenDesktop/ViewModels/ApplicationUpdateWindowViewModel.cs rename to LibgenDesktop/ViewModels/Windows/ApplicationUpdateWindowViewModel.cs index 2fbe8a6..ba87b68 100644 --- a/LibgenDesktop/ViewModels/ApplicationUpdateWindowViewModel.cs +++ b/LibgenDesktop/ViewModels/Windows/ApplicationUpdateWindowViewModel.cs @@ -4,16 +4,15 @@ using System.Threading; using LibgenDesktop.Infrastructure; using LibgenDesktop.Models; +using LibgenDesktop.Models.Localization.Localizators; using LibgenDesktop.Models.ProgressArgs; using LibgenDesktop.Models.Update; -using LibgenDesktop.Views; using Environment = LibgenDesktop.Common.Environment; -namespace LibgenDesktop.ViewModels +namespace LibgenDesktop.ViewModels.Windows { internal class ApplicationUpdateWindowViewModel : LibgenWindowViewModel { - private readonly MainModel mainModel; private readonly Updater.UpdateCheckResult updateCheckResult; private readonly CancellationTokenSource cancellationTokenSource; private string newVersionText; @@ -29,10 +28,11 @@ internal class ApplicationUpdateWindowViewModel : LibgenWindowViewModel private string interruptButtonText; public ApplicationUpdateWindowViewModel(MainModel mainModel, Updater.UpdateCheckResult updateCheckResult) + : base(mainModel) { - this.mainModel = mainModel; this.updateCheckResult = updateCheckResult; cancellationTokenSource = new CancellationTokenSource(); + Localization = mainModel.Localization.CurrentLanguage.ApplicationUpdate; WindowClosingCommand = new FuncCommand(WindowClosing); SkipVersionCommand = new Command(SkipVersion); DownloadCommand = new Command(DownloadAsync); @@ -42,6 +42,8 @@ public ApplicationUpdateWindowViewModel(MainModel mainModel, Updater.UpdateCheck Initialize(); } + public ApplicationUpdateLocalizator Localization { get; } + public string NewVersionText { get @@ -183,13 +185,13 @@ public bool IsCloseButtonVisible private void Initialize() { - newVersionText = $"Новая версия: {updateCheckResult.NewReleaseName} от {updateCheckResult.PublishedAt:dd.MM.yyyy}"; + newVersionText = Localization.GetNewVersionString(updateCheckResult.NewReleaseName, updateCheckResult.PublishedAt); isSkipButtonVisible = true; downloadProgress = 0; - downloadButtonText = Environment.IsInPortableMode ? "СКАЧАТЬ" : "СКАЧАТЬ И УСТАНОВИТЬ"; + downloadButtonText = Environment.IsInPortableMode ? Localization.Download : Localization.DownloadAndInstall; isDownloadButtonVisible = true; isCancelButtonVisible = true; - interruptButtonText = "ПРЕРВАТЬ"; + interruptButtonText = Localization.Interrupt; isInterruptButtonEnabled = true; isInterruptButtonVisible = false; isCloseButtonVisible = false; @@ -198,9 +200,9 @@ private void Initialize() private void SkipVersion() { - mainModel.AppSettings.LastUpdate.IgnoreReleaseName = updateCheckResult.NewReleaseName; - mainModel.SaveSettings(); - mainModel.LastApplicationUpdateCheckResult = null; + MainModel.AppSettings.LastUpdate.IgnoreReleaseName = updateCheckResult.NewReleaseName; + MainModel.SaveSettings(); + MainModel.LastApplicationUpdateCheckResult = null; CurrentWindowContext.CloseDialog(true); } @@ -221,7 +223,7 @@ private async void DownloadAsync() } downloadFilePath = Path.Combine(downloadDirectory, updateCheckResult.FileName); Progress downloadProgressHandler = new Progress(HandleDownloadProgress); - result = await mainModel.DownloadFileAsync(updateCheckResult.DownloadUrl, downloadFilePath, downloadProgressHandler, + result = await MainModel.DownloadFileAsync(updateCheckResult.DownloadUrl, downloadFilePath, downloadProgressHandler, cancellationTokenSource.Token); } catch (Exception exception) @@ -233,7 +235,7 @@ private async void DownloadAsync() { if (result == MainModel.DownloadFileResult.INCOMPLETE) { - MessageBoxWindow.ShowMessage("Ошибка", "Файл обновления не был загружен полностью.", CurrentWindowContext); + ShowMessage(Localization.Error, Localization.IncompleteDownload); } error = true; } @@ -271,7 +273,7 @@ private void Cancel() private void InterruptDownload() { IsInterruptButtonEnabled = false; - InterruptButtonText = "ПРЕРЫВАЕТСЯ..."; + InterruptButtonText = Localization.Interrupting; cancellationTokenSource.Cancel(); } @@ -284,7 +286,7 @@ private bool WindowClosing() { if (IsInterruptButtonVisible) { - if (MessageBoxWindow.ShowPrompt("Прервать загрузку?", "Прервать загрузку обновления?", CurrentWindowContext)) + if (ShowPrompt(Localization.InterruptPromptTitle, Localization.InterruptPromptText)) { cancellationTokenSource.Cancel(); return true; diff --git a/LibgenDesktop/ViewModels/CreateDatabaseWindowViewModel.cs b/LibgenDesktop/ViewModels/Windows/CreateDatabaseWindowViewModel.cs similarity index 70% rename from LibgenDesktop/ViewModels/CreateDatabaseWindowViewModel.cs rename to LibgenDesktop/ViewModels/Windows/CreateDatabaseWindowViewModel.cs index 8ab60aa..3071b88 100644 --- a/LibgenDesktop/ViewModels/CreateDatabaseWindowViewModel.cs +++ b/LibgenDesktop/ViewModels/Windows/CreateDatabaseWindowViewModel.cs @@ -1,13 +1,14 @@ using System; using System.IO; using System.Linq; +using System.Text; using LibgenDesktop.Common; using LibgenDesktop.Infrastructure; using LibgenDesktop.Models; -using LibgenDesktop.Views; +using LibgenDesktop.Models.Localization.Localizators; using Environment = LibgenDesktop.Common.Environment; -namespace LibgenDesktop.ViewModels +namespace LibgenDesktop.ViewModels.Windows { internal class CreateDatabaseWindowViewModel : LibgenWindowViewModel { @@ -18,20 +19,22 @@ internal enum EventType FIRST_RUN } - private readonly MainModel mainModel; private EventType eventType; private string header; private bool isCreateDatabaseSelected; private bool isOpenDatabaseSelected; public CreateDatabaseWindowViewModel(MainModel mainModel) + : base(mainModel) { - this.mainModel = mainModel; + Localization = mainModel.Localization.CurrentLanguage.DatabaseWindow; OkButtonCommand = new Command(OkButtonClick); CancelButtonCommand = new Command(CancelButtonClick); Initialize(); } + public DatabaseWindowLocalizator Localization { get; } + public string Header { get @@ -83,44 +86,49 @@ private void Initialize() private void UpdateHeaderAndEventType(string databaseFileName = null) { - switch (mainModel.LocalDatabaseStatus) + switch (MainModel.LocalDatabaseStatus) { case MainModel.DatabaseStatus.NOT_SET: eventType = EventType.FIRST_RUN; - header = "Добро пожаловать в Libgen Desktop"; + header = Localization.FirstRunMessage; break; case MainModel.DatabaseStatus.NOT_FOUND: eventType = EventType.DATABASE_NOT_FOUND; - header = $"База данных {databaseFileName ?? GetDatabaseFileName()} не найдена"; + header = Localization.GetDatabaseNotFoundText(databaseFileName ?? GetDatabaseFileName()); break; case MainModel.DatabaseStatus.CORRUPTED: eventType = EventType.DATABASE_CORRUPTED; - header = $"База данных {databaseFileName ?? GetDatabaseFileName()} повреждена"; + header = Localization.GetDatabaseCorruptedText(databaseFileName ?? GetDatabaseFileName()); break; default: - throw new Exception($"Unexpected database status: {mainModel.LocalDatabaseStatus}."); + throw new Exception($"Unexpected database status: {MainModel.LocalDatabaseStatus}."); } - header += ". Выберите действие:"; + header += " " + Localization.ChooseOption + ":"; } private string GetDatabaseFileName() { - return Path.GetFileName(mainModel.AppSettings.DatabaseFileName); + return Path.GetFileName(MainModel.AppSettings.DatabaseFileName); } private void OkButtonClick() { + StringBuilder filterBuilder = new StringBuilder(); + filterBuilder.Append(Localization.Databases); + filterBuilder.Append(" (*.db)|*.db|"); + filterBuilder.Append(Localization.AllFiles); + filterBuilder.Append(" (*.*)|*.*"); if (IsCreateDatabaseSelected) { SaveFileDialogParameters saveFileDialogParameters = new SaveFileDialogParameters { - DialogTitle = "Сохранение новой базы данных", - Filter = "Базы данных (*.db)|*.db|Все файлы (*.*)|*.*", + DialogTitle = Localization.BrowseNewDatabaseDialogTitle, + Filter = filterBuilder.ToString(), OverwritePrompt = true }; if (eventType == EventType.DATABASE_CORRUPTED) { - string databaseFilePath = mainModel.GetDatabaseFullPath(mainModel.AppSettings.DatabaseFileName); + string databaseFilePath = MainModel.GetDatabaseFullPath(MainModel.AppSettings.DatabaseFileName); saveFileDialogParameters.InitialDirectory = Path.GetDirectoryName(databaseFilePath); saveFileDialogParameters.InitialFileName = Path.GetFileName(databaseFilePath); } @@ -132,15 +140,15 @@ private void OkButtonClick() SaveFileDialogResult saveFileDialogResult = WindowManager.ShowSaveFileDialog(saveFileDialogParameters); if (saveFileDialogResult.DialogResult) { - if (mainModel.CreateDatabase(saveFileDialogResult.SelectedFilePath)) + if (MainModel.CreateDatabase(saveFileDialogResult.SelectedFilePath)) { - mainModel.AppSettings.DatabaseFileName = mainModel.GetDatabaseNormalizedPath(saveFileDialogResult.SelectedFilePath); - mainModel.SaveSettings(); + MainModel.AppSettings.DatabaseFileName = MainModel.GetDatabaseNormalizedPath(saveFileDialogResult.SelectedFilePath); + MainModel.SaveSettings(); CurrentWindowContext.CloseDialog(true); } else { - MessageBoxWindow.ShowMessage("Ошибка", "Не удалось создать базу данных.", CurrentWindowContext); + ShowMessage(Localization.Error, Localization.CannotCreateDatabase); } } } @@ -148,18 +156,18 @@ private void OkButtonClick() { OpenFileDialogParameters openFileDialogParameters = new OpenFileDialogParameters { - DialogTitle = "Выбор базы данных", - Filter = "Базы данных (*.db)|*.db|Все файлы (*.*)|*.*", + DialogTitle = Localization.BrowseExistingDatabaseDialogTitle, + Filter = filterBuilder.ToString(), Multiselect = false }; OpenFileDialogResult openFileDialogResult = WindowManager.ShowOpenFileDialog(openFileDialogParameters); if (openFileDialogResult.DialogResult) { string databaseFilePath = openFileDialogResult.SelectedFilePaths.First(); - if (mainModel.OpenDatabase(databaseFilePath)) + if (MainModel.OpenDatabase(databaseFilePath)) { - mainModel.AppSettings.DatabaseFileName = mainModel.GetDatabaseNormalizedPath(databaseFilePath); - mainModel.SaveSettings(); + MainModel.AppSettings.DatabaseFileName = MainModel.GetDatabaseNormalizedPath(databaseFilePath); + MainModel.SaveSettings(); CurrentWindowContext.CloseDialog(true); } else diff --git a/LibgenDesktop/ViewModels/Windows/DetailsWindowViewModel.cs b/LibgenDesktop/ViewModels/Windows/DetailsWindowViewModel.cs new file mode 100644 index 0000000..35c067f --- /dev/null +++ b/LibgenDesktop/ViewModels/Windows/DetailsWindowViewModel.cs @@ -0,0 +1,83 @@ +using System; +using LibgenDesktop.Infrastructure; +using LibgenDesktop.Models; +using LibgenDesktop.Models.Entities; +using LibgenDesktop.ViewModels.EventArguments; +using LibgenDesktop.ViewModels.Tabs; + +namespace LibgenDesktop.ViewModels.Windows +{ + internal abstract class DetailsWindowViewModel : LibgenWindowViewModel where T : LibgenObject + { + private readonly T libgenObject; + private readonly bool modalWindow; + private DetailsTabViewModel tabViewModel; + + public DetailsWindowViewModel(MainModel mainModel, T libgenObject, bool modalWindow) + : base(mainModel) + { + this.libgenObject = libgenObject; + this.modalWindow = modalWindow; + tabViewModel = null; + WindowClosedCommand = new Command(WindowClosedHandler); + } + + public string WindowTitle { get; protected set; } + public int WindowWidth { get; set; } + public int WindowHeight { get; set; } + + public DetailsTabViewModel TabViewModel + { + get + { + if (tabViewModel == null) + { + tabViewModel = CreateDetailsTabViewModel(MainModel, CurrentWindowContext, libgenObject, modalWindow); + tabViewModel.SelectDownloadRequested += SelectDownloadRequestedHandler; + tabViewModel.CloseTabRequested += CloseTabRequested; + } + return tabViewModel; + } + } + + public Command WindowClosedCommand { get; } + + public event EventHandler SelectDownloadRequested; + public event EventHandler WindowClosed; + + protected abstract DetailsTabViewModel CreateDetailsTabViewModel(MainModel mainModel, IWindowContext currentWindowContext, T libgenObject, + bool modalWindow); + + protected virtual void OnWindowClosing() + { + } + + private void CloseTabRequested(object sender, EventArgs e) + { + if (modalWindow) + { + CurrentWindowContext.CloseDialog(false); + } + else + { + CurrentWindowContext.Close(); + } + } + + private void SelectDownloadRequestedHandler(object sender, SelectDownloadEventArgs e) + { + SelectDownloadRequested?.Invoke(this, e); + } + + private void WindowClosedHandler() + { + if (tabViewModel != null) + { + tabViewModel.CloseTabRequested -= CloseTabRequested; + tabViewModel.SelectDownloadRequested -= SelectDownloadRequestedHandler; + } + OnWindowClosing(); + WindowClosed?.Invoke(this, EventArgs.Empty); + } + } +} diff --git a/LibgenDesktop/ViewModels/Windows/ErrorWindowViewModel.cs b/LibgenDesktop/ViewModels/Windows/ErrorWindowViewModel.cs new file mode 100644 index 0000000..6081cac --- /dev/null +++ b/LibgenDesktop/ViewModels/Windows/ErrorWindowViewModel.cs @@ -0,0 +1,68 @@ +using System; +using LibgenDesktop.Infrastructure; +using LibgenDesktop.Models.Localization; +using LibgenDesktop.Models.Localization.Localizators; + +namespace LibgenDesktop.ViewModels.Windows +{ + internal class ErrorWindowViewModel : ViewModel + { + public ErrorWindowViewModel(string error, Language currentLanguage) + { + Error = error; + if (currentLanguage != null) + { + try + { + ErrorWindowLocalizator localization = currentLanguage.ErrorWindow; + if (localization != null) + { + WindowTitle = !String.IsNullOrWhiteSpace(localization.WindowTitle) ? localization.WindowTitle : DefaultWindowTitle; + UnexpectedError = !String.IsNullOrWhiteSpace(localization.UnexpectedError) ? localization.UnexpectedError : DefaultUnexpectedError; + Copy = !String.IsNullOrWhiteSpace(localization.Copy) ? localization.Copy : DefaultCopy; + Close = !String.IsNullOrWhiteSpace(localization.Close) ? localization.Close : DefaultClose; + } + else + { + PopulateDefaultMessages(); + } + } + catch + { + PopulateDefaultMessages(); + } + } + else + { + PopulateDefaultMessages(); + } + CopyErrorCommand = new Command(CopyErrorToClipboard); + } + + public string Error { get; } + public string WindowTitle { get; private set; } + public string UnexpectedError { get; private set; } + public string Copy { get; private set; } + public string Close { get; private set; } + + private string DefaultWindowTitle => "Error"; + private string DefaultUnexpectedError => "An unexpected error has occurred:"; + private string DefaultCopy => "COPY TO CLIPBOARD"; + private string DefaultClose => "CLOSE"; + + public Command CopyErrorCommand { get; } + + private void CopyErrorToClipboard() + { + WindowManager.SetClipboardText(Error); + } + + private void PopulateDefaultMessages() + { + WindowTitle = DefaultWindowTitle; + UnexpectedError = DefaultUnexpectedError; + Copy = DefaultCopy; + Close = DefaultClose; + } + } +} diff --git a/LibgenDesktop/ViewModels/Windows/FictionDetailsWindowViewModel.cs b/LibgenDesktop/ViewModels/Windows/FictionDetailsWindowViewModel.cs new file mode 100644 index 0000000..504b7c0 --- /dev/null +++ b/LibgenDesktop/ViewModels/Windows/FictionDetailsWindowViewModel.cs @@ -0,0 +1,35 @@ +using LibgenDesktop.Infrastructure; +using LibgenDesktop.Models; +using LibgenDesktop.Models.Entities; +using LibgenDesktop.Models.Settings; +using LibgenDesktop.ViewModels.Tabs; + +namespace LibgenDesktop.ViewModels.Windows +{ + internal class FictionDetailsWindowViewModel : DetailsWindowViewModel + { + public FictionDetailsWindowViewModel(MainModel mainModel, FictionBook book, bool modalWindow) + : base(mainModel, book, modalWindow) + { + WindowTitle = book.Title; + WindowWidth = mainModel.AppSettings.Fiction.DetailsWindow.Width; + WindowHeight = mainModel.AppSettings.Fiction.DetailsWindow.Height; + } + + protected override DetailsTabViewModel CreateDetailsTabViewModel(MainModel mainModel, IWindowContext currentWindowContext, + FictionBook libgenObject, bool modalWindow) + { + return new FictionDetailsTabViewModel(mainModel, CurrentWindowContext, libgenObject, modalWindow); + } + + protected override void OnWindowClosing() + { + MainModel.AppSettings.Fiction.DetailsWindow = new AppSettings.FictionDetailsWindowSettings + { + Width = WindowWidth, + Height = WindowHeight + }; + MainModel.SaveSettings(); + } + } +} diff --git a/LibgenDesktop/ViewModels/ImportWindowViewModel.cs b/LibgenDesktop/ViewModels/Windows/ImportWindowViewModel.cs similarity index 73% rename from LibgenDesktop/ViewModels/ImportWindowViewModel.cs rename to LibgenDesktop/ViewModels/Windows/ImportWindowViewModel.cs index d60ac02..980c77d 100644 --- a/LibgenDesktop/ViewModels/ImportWindowViewModel.cs +++ b/LibgenDesktop/ViewModels/Windows/ImportWindowViewModel.cs @@ -1,13 +1,13 @@ using System; -using System.Text; using System.Threading; using LibgenDesktop.Infrastructure; using LibgenDesktop.Models; +using LibgenDesktop.Models.Localization.Localizators; using LibgenDesktop.Models.ProgressArgs; using LibgenDesktop.Models.SqlDump; -using LibgenDesktop.Models.Utils; +using LibgenDesktop.ViewModels.Panels; -namespace LibgenDesktop.ViewModels +namespace LibgenDesktop.ViewModels.Windows { internal class ImportWindowViewModel : LibgenWindowViewModel { @@ -19,7 +19,6 @@ private enum Step IMPORTING_DATA } - private readonly MainModel mainModel; private readonly string dumpFilePath; private readonly CancellationTokenSource cancellationTokenSource; private readonly Timer elapsedTimer; @@ -39,10 +38,11 @@ private enum Step private TimeSpan lastElapsedTime; public ImportWindowViewModel(MainModel mainModel, string dumpFilePath) + : base(mainModel) { - this.mainModel = mainModel; this.dumpFilePath = dumpFilePath; cancellationTokenSource = new CancellationTokenSource(); + Localization = mainModel.Localization.CurrentLanguage.Import; elapsedTimer = new Timer(state => UpdateElapsedTime()); Logs = new ImportLogPanelViewModel(); CancelCommand = new Command(Cancel); @@ -51,6 +51,7 @@ public ImportWindowViewModel(MainModel mainModel, string dumpFilePath) Initialize(); } + public ImportLocalizator Localization { get; } public ImportLogPanelViewModel Logs { get; } public bool IsInProgress @@ -163,17 +164,17 @@ private void Initialize() currentStep = Step.SEARCHING_TABLE_DEFINITION; currentStepIndex = 1; totalSteps = 2; - UpdateStatus("Поиск данных для импорта"); + UpdateStatus(Localization.StatusDataLookup); startDateTime = DateTime.Now; lastElapsedTime = TimeSpan.Zero; elapsedTimer.Change(TimeSpan.Zero, TimeSpan.FromMilliseconds(100)); elapsed = GetElapsedString(lastElapsedTime); - cancelButtonText = "ПРЕРВАТЬ"; + cancelButtonText = Localization.Interrupt; isCancelButtonVisible = true; isCancelButtonEnabled = true; isCloseButtonVisible = false; lastScannedPercentage = 0; - Logs.AddLogItem(currentStepIndex, "Поиск данных для импорта в файле", "Идет сканирование файла..."); + Logs.AddLogItem(Localization.GetLogLineStep(currentStepIndex), Localization.LogLineDataLookup, Localization.LogLineScanning); Import(); } @@ -184,14 +185,14 @@ private async void Import() MainModel.ImportSqlDumpResult importResult; try { - importResult = await mainModel.ImportSqlDumpAsync(dumpFilePath, importProgressHandler, cancellationToken); + importResult = await MainModel.ImportSqlDumpAsync(dumpFilePath, importProgressHandler, cancellationToken); } catch (Exception exception) { elapsedTimer.Change(Timeout.InfiniteTimeSpan, Timeout.InfiniteTimeSpan); - Logs.ShowErrorLogLine("Импорт завершился с ошибками."); + Logs.ShowErrorLogLine(Localization.LogLineImportError); IsInProgress = false; - Status = "Импорт завершен с ошибками"; + Status = Localization.StatusImportError; IsCancelButtonVisible = false; IsCloseButtonVisible = true; ShowErrorWindow(exception, CurrentWindowContext); @@ -201,16 +202,16 @@ private async void Import() switch (importResult) { case MainModel.ImportSqlDumpResult.COMPLETED: - Logs.ShowResultLogLine("Импорт выполнен успешно."); - Status = "Импорт завершен"; + Logs.ShowResultLogLine(Localization.LogLineImportSuccessful); + Status = Localization.StatusImportComplete; break; case MainModel.ImportSqlDumpResult.CANCELLED: - Logs.ShowErrorLogLine("Импорт был прерван пользователем."); - Status = "Импорт прерван"; + Logs.ShowErrorLogLine(Localization.LogLineImportCancelled); + Status = Localization.StatusImportCancelled; break; case MainModel.ImportSqlDumpResult.DATA_NOT_FOUND: - Logs.ShowErrorLogLine("Не найдены данные для импорта."); - Status = "Данные не найдены"; + Logs.ShowErrorLogLine(Localization.LogLineDataNotFound); + Status = Localization.StatusDataNotFound; break; } IsInProgress = false; @@ -237,20 +238,18 @@ private void HandleImportProgress(object progress) break; case ImportTableDefinitionFoundProgress tableDefinitionFoundProgress: tableType = tableDefinitionFoundProgress.TableFound; - string tableFoundStatusString = "Найдена таблица с "; switch (tableDefinitionFoundProgress.TableFound) { case TableType.NON_FICTION: - tableFoundStatusString += "нехудожественными книгами"; + CurrentLogItem.LogLine = Localization.LogLineNonFictionTableFound; break; case TableType.FICTION: - tableFoundStatusString += "художественными книгами"; + CurrentLogItem.LogLine = Localization.LogLineFictionTableFound; break; case TableType.SCI_MAG: - tableFoundStatusString += "научными статьями"; + CurrentLogItem.LogLine = Localization.LogLineSciMagTableFound; break; } - CurrentLogItem.LogLine = tableFoundStatusString + "."; break; case ImportCreateIndexProgress createIndexProgress: if (currentStep != Step.CREATING_INDEXES) @@ -258,10 +257,10 @@ private void HandleImportProgress(object progress) currentStep = Step.CREATING_INDEXES; currentStepIndex++; totalSteps++; - UpdateStatus("Создание индексов"); - Logs.AddLogItem(currentStepIndex, "Создание недостающих индексов"); + UpdateStatus(Localization.StatusCreatingIndexes); + Logs.AddLogItem(Localization.GetLogLineStep(currentStepIndex), Localization.LogLineCreatingIndexes); } - CurrentLogItem.LogLines.Add($"Создается индекс для столбца {createIndexProgress.ColumnName}..."); + CurrentLogItem.LogLines.Add(Localization.GetLogLineCreatingIndexForColumn(createIndexProgress.ColumnName)); break; case ImportLoadLibgenIdsProgress importLoadLibgenIdsProgress: if (currentStep != Step.LOADING_EXISTING_IDS) @@ -269,18 +268,18 @@ private void HandleImportProgress(object progress) currentStep = Step.LOADING_EXISTING_IDS; currentStepIndex++; totalSteps++; - UpdateStatus("Загрузка идентификаторов"); - Logs.AddLogItem(currentStepIndex, "Загрузка идентификаторов существующих данных"); + UpdateStatus(Localization.StatusLoadingIds); + Logs.AddLogItem(Localization.GetLogLineStep(currentStepIndex), Localization.LogLineLoadingIds); } - CurrentLogItem.LogLines.Add($"Загрузка значений столбца LibgenId..."); + CurrentLogItem.LogLines.Add(Localization.GetLogLineLoadingColumnValues("LibgenId")); break; case ImportObjectsProgress importObjectsProgress: if (currentStep != Step.IMPORTING_DATA) { currentStep = Step.IMPORTING_DATA; currentStepIndex++; - UpdateStatus("Импорт данных"); - Logs.AddLogItem(currentStepIndex, "Импорт данных"); + UpdateStatus(Localization.StatusImportingData); + Logs.AddLogItem(Localization.GetLogLineStep(currentStepIndex), Localization.LogLineImportingData); } string logLine = GetImportedObjectCountLogLine(importObjectsProgress.ObjectsAdded, importObjectsProgress.ObjectsUpdated); CurrentLogItem.LogLine = logLine; @@ -307,7 +306,7 @@ private void UpdateElapsedTime() private void Cancel() { IsCancelButtonEnabled = false; - CancelButtonText = "ПРЕРЫВАЕТСЯ..."; + CancelButtonText = Localization.Interrupting; cancellationTokenSource.Cancel(); } @@ -323,55 +322,42 @@ private void WindowClosed() private void UpdateStatus(string statusDescription) { - Status = $"Шаг {currentStepIndex} из {totalSteps}. {statusDescription}..."; + Status = $"{Localization.GetStatusStep(currentStepIndex, totalSteps)} {statusDescription}..."; } private string GetScannedPercentageStatusString(decimal scannedPercentage) { - return $"Просканировано {scannedPercentage}% файла..."; + return Localization.GetLogLineScannedProgress(scannedPercentage); } private string GetImportedObjectCountLogLine(int addedObjectCount, int updatedObjectCount) { - string objectType; switch (tableType) { case TableType.NON_FICTION: case TableType.FICTION: - objectType = "книг"; - break; + return updatedObjectCount > 0 ? Localization.GetLogLineImportBooksProgressWithUpdate(addedObjectCount, updatedObjectCount) : + Localization.GetLogLineImportBooksProgressNoUpdate(addedObjectCount); case TableType.SCI_MAG: - objectType = "статей"; - break; + return updatedObjectCount > 0 ? Localization.GetLogLineImportArticlesProgressWithUpdate(addedObjectCount, updatedObjectCount) : + Localization.GetLogLineImportArticlesProgressNoUpdate(addedObjectCount); default: throw new Exception($"Unknown table type: {tableType}."); } - StringBuilder resultBuilder = new StringBuilder(); - resultBuilder.Append("Добавлено "); - resultBuilder.Append(objectType); - resultBuilder.Append(": "); - resultBuilder.Append(addedObjectCount.ToFormattedString()); - if (updatedObjectCount > 0) - { - resultBuilder.Append(", обновлено "); - resultBuilder.Append(objectType); - resultBuilder.Append(": "); - resultBuilder.Append(updatedObjectCount.ToFormattedString()); - } - resultBuilder.Append("."); - return resultBuilder.ToString(); } private string GetElapsedString(TimeSpan elapsedTime) { + string elapsed; if (elapsedTime.Hours > 0) { - return $"Прошло {Math.Truncate(elapsedTime.TotalHours)}:{elapsedTime:mm\\:ss}"; + elapsed = $"{Math.Truncate(elapsedTime.TotalHours)}:{elapsedTime:mm\\:ss}"; } else { - return $"Прошло {elapsedTime:mm\\:ss}"; + elapsed = $"{elapsedTime:mm\\:ss}"; } + return Localization.GetElapsedString(elapsed); } } } diff --git a/LibgenDesktop/ViewModels/Windows/LibgenWindowViewModel.cs b/LibgenDesktop/ViewModels/Windows/LibgenWindowViewModel.cs new file mode 100644 index 0000000..d01556b --- /dev/null +++ b/LibgenDesktop/ViewModels/Windows/LibgenWindowViewModel.cs @@ -0,0 +1,32 @@ +using System; +using LibgenDesktop.Infrastructure; +using LibgenDesktop.Models; + +namespace LibgenDesktop.ViewModels.Windows +{ + internal abstract class LibgenWindowViewModel : ContainerViewModel + { + public LibgenWindowViewModel(MainModel mainModel) + : base(mainModel) + { + } + + protected IWindowContext CurrentWindowContext + { + get + { + return WindowManager.GetWindowContext(this); + } + } + + protected void ShowMessage(string title, string text) + { + ShowMessage(title, text, CurrentWindowContext); + } + + protected bool ShowPrompt(string title, string text) + { + return ShowPrompt(title, text, CurrentWindowContext); + } + } +} diff --git a/LibgenDesktop/ViewModels/MainWindowViewModel.cs b/LibgenDesktop/ViewModels/Windows/MainWindowViewModel.cs similarity index 63% rename from LibgenDesktop/ViewModels/MainWindowViewModel.cs rename to LibgenDesktop/ViewModels/Windows/MainWindowViewModel.cs index e32e492..f3ccd2c 100644 --- a/LibgenDesktop/ViewModels/MainWindowViewModel.cs +++ b/LibgenDesktop/ViewModels/Windows/MainWindowViewModel.cs @@ -1,26 +1,33 @@ using System; using System.Collections.ObjectModel; using System.Linq; +using System.Text; using LibgenDesktop.Common; using LibgenDesktop.Infrastructure; using LibgenDesktop.Models; +using LibgenDesktop.Models.Download; using LibgenDesktop.Models.Entities; +using LibgenDesktop.Models.Localization.Localizators; using LibgenDesktop.Models.Settings; -using LibgenDesktop.Views; +using LibgenDesktop.ViewModels.EventArguments; +using LibgenDesktop.ViewModels.Tabs; using static LibgenDesktop.Models.Settings.AppSettings; -namespace LibgenDesktop.ViewModels +namespace LibgenDesktop.ViewModels.Windows { internal class MainWindowViewModel : LibgenWindowViewModel { - private readonly MainModel mainModel; + private MainWindowLocalizator localization; private SearchTabViewModel defaultSearchTabViewModel; private TabViewModel selectedTabViewModel; + private bool isDownloadManagerButtonHighlighted; + private bool isCompletedDownloadCounterVisible; + private int completedDownloadCount; private bool isApplicationUpdateAvailable; public MainWindowViewModel(MainModel mainModel) + : base(mainModel) { - this.mainModel = mainModel; defaultSearchTabViewModel = null; Events = new EventProvider(); NewTabCommand = new Command(NewTab); @@ -36,6 +43,8 @@ public MainWindowViewModel(MainModel mainModel) TabViewModels = new ObservableCollection(); Initialize(); mainModel.ApplicationUpdateCheckCompleted += ApplicationUpdateCheckCompleted; + mainModel.Localization.LanguageChanged += LocalizationLanguageChanged; + mainModel.Downloader.DownloaderEvent += DownloaderEvent; } public int WindowWidth { get; set; } @@ -46,13 +55,26 @@ public MainWindowViewModel(MainModel mainModel) public EventProvider Events { get; } public ObservableCollection TabViewModels { get; } + public MainWindowLocalizator Localization + { + get + { + return localization; + } + set + { + localization = value; + NotifyPropertyChanged(); + } + } + public SearchTabViewModel DefaultSearchTabViewModel { get { if (defaultSearchTabViewModel == null) { - defaultSearchTabViewModel = new SearchTabViewModel(mainModel, CurrentWindowContext); + defaultSearchTabViewModel = new SearchTabViewModel(MainModel, CurrentWindowContext); } return defaultSearchTabViewModel; } @@ -68,6 +90,7 @@ public TabViewModel SelectedTabViewModel { selectedTabViewModel = value; NotifyPropertyChanged(); + SelectedTabChanged(); } } @@ -95,6 +118,45 @@ public bool IsNewTabButtonVisible } } + public bool IsDownloadManagerButtonHighlighted + { + get + { + return isDownloadManagerButtonHighlighted; + } + set + { + isDownloadManagerButtonHighlighted = value; + NotifyPropertyChanged(); + } + } + + public bool IsCompletedDownloadCounterVisible + { + get + { + return isCompletedDownloadCounterVisible; + } + set + { + isCompletedDownloadCounterVisible = value; + NotifyPropertyChanged(); + } + } + + public int CompletedDownloadCount + { + get + { + return completedDownloadCount; + } + set + { + completedDownloadCount = value; + NotifyPropertyChanged(); + } + } + public bool IsApplicationUpdateAvailable { get @@ -119,9 +181,18 @@ public bool IsApplicationUpdateAvailable public Command SettingsCommand { get; } public Command WindowClosedCommand { get; } + private DownloadManagerTabViewModel DownloadManagerTabViewModel + { + get + { + return TabViewModels.OfType().FirstOrDefault(); + } + } + private void Initialize() { - AppSettings appSettings = mainModel.AppSettings; + localization = MainModel.Localization.CurrentLanguage.MainWindow; + AppSettings appSettings = MainModel.AppSettings; MainWindowSettings mainWindowSettings = appSettings.MainWindow; WindowWidth = mainWindowSettings.Width; WindowHeight = mainWindowSettings.Height; @@ -133,6 +204,9 @@ private void Initialize() DefaultSearchTabViewModel.FictionSearchComplete += SearchTabFictionSearchComplete; DefaultSearchTabViewModel.SciMagSearchComplete += SearchTabSciMagSearchComplete; selectedTabViewModel = null; + isDownloadManagerButtonHighlighted = false; + isCompletedDownloadCounterVisible = false; + completedDownloadCount = 0; isApplicationUpdateAvailable = false; } @@ -141,26 +215,36 @@ private void DefaultSearchTabViewModel_ImportRequested(object sender, EventArgs Import(); } - private void SearchTabNonFictionSearchComplete(object sender, SearchTabViewModel.SearchCompleteEventArgs e) + private void SelectedTabChanged() + { + if (SelectedTabViewModel is DownloadManagerTabViewModel) + { + IsDownloadManagerButtonHighlighted = false; + CompletedDownloadCount = 0; + IsCompletedDownloadCounterVisible = false; + } + } + + private void SearchTabNonFictionSearchComplete(object sender, SearchCompleteEventArgs e) { NonFictionSearchResultsTabViewModel nonFictionSearchResultsTabViewModel = - new NonFictionSearchResultsTabViewModel(mainModel, CurrentWindowContext, e.SearchQuery, e.SearchResult); + new NonFictionSearchResultsTabViewModel(MainModel, CurrentWindowContext, e.SearchQuery, e.SearchResult); nonFictionSearchResultsTabViewModel.OpenNonFictionDetailsRequested += OpenNonFictionDetailsRequested; ShowSearchResults(sender as SearchTabViewModel, nonFictionSearchResultsTabViewModel); } - private void SearchTabFictionSearchComplete(object sender, SearchTabViewModel.SearchCompleteEventArgs e) + private void SearchTabFictionSearchComplete(object sender, SearchCompleteEventArgs e) { FictionSearchResultsTabViewModel fictionSearchResultsTabViewModel = - new FictionSearchResultsTabViewModel(mainModel, CurrentWindowContext, e.SearchQuery, e.SearchResult); + new FictionSearchResultsTabViewModel(MainModel, CurrentWindowContext, e.SearchQuery, e.SearchResult); fictionSearchResultsTabViewModel.OpenFictionDetailsRequested += OpenFictionDetailsRequested; ShowSearchResults(sender as SearchTabViewModel, fictionSearchResultsTabViewModel); } - private void SearchTabSciMagSearchComplete(object sender, SearchTabViewModel.SearchCompleteEventArgs e) + private void SearchTabSciMagSearchComplete(object sender, SearchCompleteEventArgs e) { SciMagSearchResultsTabViewModel sciMagSearchResultsTabViewModel = - new SciMagSearchResultsTabViewModel(mainModel, CurrentWindowContext, e.SearchQuery, e.SearchResult); + new SciMagSearchResultsTabViewModel(MainModel, CurrentWindowContext, e.SearchQuery, e.SearchResult); sciMagSearchResultsTabViewModel.OpenSciMagDetailsRequested += OpenSciMagDetailsRequested; ShowSearchResults(sender as SearchTabViewModel, sciMagSearchResultsTabViewModel); } @@ -185,7 +269,7 @@ private void NewTab() { if (IsNewTabButtonVisible) { - SearchTabViewModel searchTabViewModel = new SearchTabViewModel(mainModel, CurrentWindowContext); + SearchTabViewModel searchTabViewModel = new SearchTabViewModel(MainModel, CurrentWindowContext); searchTabViewModel.NonFictionSearchComplete += SearchTabNonFictionSearchComplete; searchTabViewModel.FictionSearchComplete += SearchTabFictionSearchComplete; searchTabViewModel.SciMagSearchComplete += SearchTabSciMagSearchComplete; @@ -195,14 +279,15 @@ private void NewTab() } } - private void OpenNonFictionDetailsRequested(object sender, NonFictionSearchResultsTabViewModel.OpenNonFictionDetailsEventArgs e) + private void OpenNonFictionDetailsRequested(object sender, OpenNonFictionDetailsEventArgs e) { Logger.Debug($"Opening non-fiction book with ID = {e.NonFictionBook.Id}, Libgen ID = {e.NonFictionBook.LibgenId}."); - SearchSettings.DetailsMode openDetailsMode = mainModel.AppSettings.Search.OpenDetailsMode; + SearchSettings.DetailsMode openDetailsMode = MainModel.AppSettings.Search.OpenDetailsMode; if (openDetailsMode == SearchSettings.DetailsMode.NEW_TAB) { NonFictionDetailsTabViewModel nonFictionDetailsTabViewModel = - new NonFictionDetailsTabViewModel(mainModel, CurrentWindowContext, e.NonFictionBook, isInModalWindow: false); + new NonFictionDetailsTabViewModel(MainModel, CurrentWindowContext, e.NonFictionBook, isInModalWindow: false); + nonFictionDetailsTabViewModel.SelectDownloadRequested += SelectDownloadRequested; nonFictionDetailsTabViewModel.CloseTabRequested += NonFictionDetailsCloseTabRequested; TabViewModels.Add(nonFictionDetailsTabViewModel); SelectedTabViewModel = nonFictionDetailsTabViewModel; @@ -210,28 +295,32 @@ private void OpenNonFictionDetailsRequested(object sender, NonFictionSearchResul else { bool modalWindow = openDetailsMode == SearchSettings.DetailsMode.NEW_MODAL_WINDOW; - NonFictionDetailsWindowViewModel detailsWindowViewModel = new NonFictionDetailsWindowViewModel(mainModel, e.NonFictionBook, modalWindow); + NonFictionDetailsWindowViewModel detailsWindowViewModel = new NonFictionDetailsWindowViewModel(MainModel, e.NonFictionBook, modalWindow); + detailsWindowViewModel.SelectDownloadRequested += SelectDownloadRequested; IWindowContext detailsWindowContext = WindowManager.CreateWindow(RegisteredWindows.WindowKey.NON_FICTION_DETAILS_WINDOW, detailsWindowViewModel, CurrentWindowContext); - NonFictionDetailsWindowSettings detailsWindowSettings = mainModel.AppSettings.NonFiction.DetailsWindow; + NonFictionDetailsWindowSettings detailsWindowSettings = MainModel.AppSettings.NonFiction.DetailsWindow; if (modalWindow) { detailsWindowContext.ShowDialog(detailsWindowSettings.Width, detailsWindowSettings.Height); + detailsWindowViewModel.SelectDownloadRequested -= SelectDownloadRequested; } else { + detailsWindowViewModel.WindowClosed += NonFictionDetailsWindowClosed; detailsWindowContext.Show(detailsWindowSettings.Width, detailsWindowSettings.Height); } } } - private void OpenFictionDetailsRequested(object sender, FictionSearchResultsTabViewModel.OpenFictionDetailsEventArgs e) + private void OpenFictionDetailsRequested(object sender, OpenFictionDetailsEventArgs e) { Logger.Debug($"Opening fiction book with ID = {e.FictionBook.Id}, Libgen ID = {e.FictionBook.LibgenId}."); - SearchSettings.DetailsMode openDetailsMode = mainModel.AppSettings.Search.OpenDetailsMode; + SearchSettings.DetailsMode openDetailsMode = MainModel.AppSettings.Search.OpenDetailsMode; if (openDetailsMode == SearchSettings.DetailsMode.NEW_TAB) { FictionDetailsTabViewModel fictionDetailsTabViewModel - = new FictionDetailsTabViewModel(mainModel, CurrentWindowContext, e.FictionBook, isInModalWindow: false); + = new FictionDetailsTabViewModel(MainModel, CurrentWindowContext, e.FictionBook, isInModalWindow: false); + fictionDetailsTabViewModel.SelectDownloadRequested += SelectDownloadRequested; fictionDetailsTabViewModel.CloseTabRequested += FictionDetailsCloseTabRequested; TabViewModels.Add(fictionDetailsTabViewModel); SelectedTabViewModel = fictionDetailsTabViewModel; @@ -239,28 +328,32 @@ FictionDetailsTabViewModel fictionDetailsTabViewModel else { bool modalWindow = openDetailsMode == SearchSettings.DetailsMode.NEW_MODAL_WINDOW; - FictionDetailsWindowViewModel detailsWindowViewModel = new FictionDetailsWindowViewModel(mainModel, e.FictionBook, modalWindow); + FictionDetailsWindowViewModel detailsWindowViewModel = new FictionDetailsWindowViewModel(MainModel, e.FictionBook, modalWindow); + detailsWindowViewModel.SelectDownloadRequested += SelectDownloadRequested; IWindowContext detailsWindowContext = WindowManager.CreateWindow(RegisteredWindows.WindowKey.FICTION_DETAILS_WINDOW, detailsWindowViewModel, CurrentWindowContext); - FictionDetailsWindowSettings detailsWindowSettings = mainModel.AppSettings.Fiction.DetailsWindow; + FictionDetailsWindowSettings detailsWindowSettings = MainModel.AppSettings.Fiction.DetailsWindow; if (modalWindow) { detailsWindowContext.ShowDialog(detailsWindowSettings.Width, detailsWindowSettings.Height); + detailsWindowViewModel.SelectDownloadRequested -= SelectDownloadRequested; } else { + detailsWindowViewModel.WindowClosed += FictionDetailsWindowClosed; detailsWindowContext.Show(detailsWindowSettings.Width, detailsWindowSettings.Height); } } } - private void OpenSciMagDetailsRequested(object sender, SciMagSearchResultsTabViewModel.OpenSciMagDetailsEventArgs e) + private void OpenSciMagDetailsRequested(object sender, OpenSciMagDetailsEventArgs e) { Logger.Debug($"Opening article with ID = {e.SciMagArticle.Id}, Libgen ID = {e.SciMagArticle.LibgenId}."); - SearchSettings.DetailsMode openDetailsMode = mainModel.AppSettings.Search.OpenDetailsMode; + SearchSettings.DetailsMode openDetailsMode = MainModel.AppSettings.Search.OpenDetailsMode; if (openDetailsMode == SearchSettings.DetailsMode.NEW_TAB) { SciMagDetailsTabViewModel sciMagDetailsTabViewModel = - new SciMagDetailsTabViewModel(mainModel, CurrentWindowContext, e.SciMagArticle, isInModalWindow: false); + new SciMagDetailsTabViewModel(MainModel, CurrentWindowContext, e.SciMagArticle, isInModalWindow: false); + sciMagDetailsTabViewModel.SelectDownloadRequested += SelectDownloadRequested; sciMagDetailsTabViewModel.CloseTabRequested += SciMagDetailsCloseTabRequested; TabViewModels.Add(sciMagDetailsTabViewModel); SelectedTabViewModel = sciMagDetailsTabViewModel; @@ -268,20 +361,33 @@ private void OpenSciMagDetailsRequested(object sender, SciMagSearchResultsTabVie else { bool modalWindow = openDetailsMode == SearchSettings.DetailsMode.NEW_MODAL_WINDOW; - SciMagDetailsWindowViewModel detailsWindowViewModel = new SciMagDetailsWindowViewModel(mainModel, e.SciMagArticle, modalWindow); + SciMagDetailsWindowViewModel detailsWindowViewModel = new SciMagDetailsWindowViewModel(MainModel, e.SciMagArticle, modalWindow); + detailsWindowViewModel.SelectDownloadRequested += SelectDownloadRequested; IWindowContext detailsWindowContext = WindowManager.CreateWindow(RegisteredWindows.WindowKey.SCI_MAG_DETAILS_WINDOW, detailsWindowViewModel, CurrentWindowContext); - SciMagDetailsWindowSettings detailsWindowSettings = mainModel.AppSettings.SciMag.DetailsWindow; + SciMagDetailsWindowSettings detailsWindowSettings = MainModel.AppSettings.SciMag.DetailsWindow; if (modalWindow) { detailsWindowContext.ShowDialog(detailsWindowSettings.Width, detailsWindowSettings.Height); + detailsWindowViewModel.SelectDownloadRequested -= SelectDownloadRequested; } else { + detailsWindowViewModel.WindowClosed += SciMagDetailsWindowClosed; detailsWindowContext.Show(detailsWindowSettings.Width, detailsWindowSettings.Height); } } } + private void SelectDownloadRequested(object sender, SelectDownloadEventArgs e) + { + if (sender is LibgenWindowViewModel) + { + Events.RaiseEvent(ViewModelEvent.RegisteredEventId.BRING_TO_FRONT); + } + ShowDownloadManager(); + (SelectedTabViewModel as DownloadManagerTabViewModel).SelectDownload(e.DownloadId); + } + private void NonFictionDetailsCloseTabRequested(object sender, EventArgs e) { CloseTab(sender as NonFictionDetailsTabViewModel); @@ -297,6 +403,27 @@ private void SciMagDetailsCloseTabRequested(object sender, EventArgs e) CloseTab(sender as SciMagDetailsTabViewModel); } + private void NonFictionDetailsWindowClosed(object sender, EventArgs e) + { + NonFictionDetailsWindowViewModel nonFictionDetailsWindowViewModel = sender as NonFictionDetailsWindowViewModel; + nonFictionDetailsWindowViewModel.SelectDownloadRequested -= SelectDownloadRequested; + nonFictionDetailsWindowViewModel.WindowClosed -= NonFictionDetailsWindowClosed; + } + + private void FictionDetailsWindowClosed(object sender, EventArgs e) + { + FictionDetailsWindowViewModel fictionDetailsWindowViewModel = sender as FictionDetailsWindowViewModel; + fictionDetailsWindowViewModel.SelectDownloadRequested -= SelectDownloadRequested; + fictionDetailsWindowViewModel.WindowClosed -= NonFictionDetailsWindowClosed; + } + + private void SciMagDetailsWindowClosed(object sender, EventArgs e) + { + SciMagDetailsWindowViewModel sciMagDetailsWindowViewModel = sender as SciMagDetailsWindowViewModel; + sciMagDetailsWindowViewModel.SelectDownloadRequested -= SelectDownloadRequested; + sciMagDetailsWindowViewModel.WindowClosed -= NonFictionDetailsWindowClosed; + } + private void CloseCurrentTab() { if (TabViewModels.Any()) @@ -315,6 +442,7 @@ private void Export() private void CloseTab(TabViewModel tabViewModel) { + tabViewModel.HandleTabClosing(); switch (tabViewModel) { case SearchTabViewModel searchTabViewModel: @@ -325,13 +453,22 @@ private void CloseTab(TabViewModel tabViewModel) case NonFictionSearchResultsTabViewModel nonFictionSearchResultsTabViewModel: nonFictionSearchResultsTabViewModel.OpenNonFictionDetailsRequested -= OpenNonFictionDetailsRequested; break; + case FictionSearchResultsTabViewModel fictionSearchResultsTabViewModel: + fictionSearchResultsTabViewModel.OpenFictionDetailsRequested -= OpenFictionDetailsRequested; + break; + case SciMagSearchResultsTabViewModel sciMagSearchResultsTabViewModel: + sciMagSearchResultsTabViewModel.OpenSciMagDetailsRequested -= OpenSciMagDetailsRequested; + break; case NonFictionDetailsTabViewModel nonFictionDetailsTabViewModel: + nonFictionDetailsTabViewModel.SelectDownloadRequested -= SelectDownloadRequested; nonFictionDetailsTabViewModel.CloseTabRequested -= NonFictionDetailsCloseTabRequested; break; case FictionDetailsTabViewModel fictionDetailsTabViewModel: + fictionDetailsTabViewModel.SelectDownloadRequested -= SelectDownloadRequested; fictionDetailsTabViewModel.CloseTabRequested -= FictionDetailsCloseTabRequested; break; case SciMagDetailsTabViewModel sciMagDetailsTabViewModel: + sciMagDetailsTabViewModel.SelectDownloadRequested -= SelectDownloadRequested; sciMagDetailsTabViewModel.CloseTabRequested -= SciMagDetailsCloseTabRequested; break; } @@ -354,10 +491,10 @@ private void CloseTab(TabViewModel tabViewModel) private void ShowDownloadManager() { - DownloadManagerTabViewModel downloadManagerTabViewModel = TabViewModels.OfType().FirstOrDefault(); + DownloadManagerTabViewModel downloadManagerTabViewModel = DownloadManagerTabViewModel; if (downloadManagerTabViewModel == null) { - downloadManagerTabViewModel = new DownloadManagerTabViewModel(mainModel, CurrentWindowContext); + downloadManagerTabViewModel = new DownloadManagerTabViewModel(MainModel, CurrentWindowContext); TabViewModels.Add(downloadManagerTabViewModel); SelectedTabViewModel = downloadManagerTabViewModel; NotifyPropertyChanged(nameof(IsDefaultSearchTabVisible)); @@ -373,7 +510,7 @@ private void ShowDownloadManager() private void ShowApplicationUpdate() { ApplicationUpdateWindowViewModel applicationUpdateWindowViewModel = - new ApplicationUpdateWindowViewModel(mainModel, mainModel.LastApplicationUpdateCheckResult); + new ApplicationUpdateWindowViewModel(MainModel, MainModel.LastApplicationUpdateCheckResult); applicationUpdateWindowViewModel.ApplicationShutdownRequested += Shutdown; IWindowContext applicationUpdateWindowContext = WindowManager.CreateWindow(RegisteredWindows.WindowKey.APPLICATION_UPDATE_WINDOW, applicationUpdateWindowViewModel, CurrentWindowContext); @@ -386,16 +523,26 @@ private void ShowApplicationUpdate() private void Import() { + ImportLocalizator importLocalizator = MainModel.Localization.CurrentLanguage.Import; + StringBuilder filterBuilder = new StringBuilder(); + filterBuilder.Append(importLocalizator.AllSupportedFiles); + filterBuilder.Append("|*.sql;*zip;*.rar;*.gz;*.7z|"); + filterBuilder.Append(importLocalizator.SqlDumps); + filterBuilder.Append(" (*.sql)|*.sql|"); + filterBuilder.Append(importLocalizator.Archives); + filterBuilder.Append(" (*.zip, *.rar, *.gz, *.7z)|*zip;*.rar;*.gz;*.7z|"); + filterBuilder.Append(importLocalizator.AllFiles); + filterBuilder.Append(" (*.*)|*.*"); OpenFileDialogParameters selectSqlDumpFileDialogParameters = new OpenFileDialogParameters { - DialogTitle = "Выбор SQL-дампа", - Filter = "Все поддерживаемые файлы|*.sql;*zip;*.rar;*.gz;*.7z|SQL -дампы (*.sql)|*.sql|Архивы (*.zip, *.rar, *.gz, *.7z)|*zip;*.rar;*.gz;*.7z|Все файлы (*.*)|*.*", + DialogTitle = importLocalizator.BrowseImportFileDialogTitle, + Filter = filterBuilder.ToString(), Multiselect = false }; OpenFileDialogResult selectSqlDumpFileDialogResult = WindowManager.ShowOpenFileDialog(selectSqlDumpFileDialogParameters); if (selectSqlDumpFileDialogResult.DialogResult) { - ImportWindowViewModel importWindowViewModel = new ImportWindowViewModel(mainModel, selectSqlDumpFileDialogResult.SelectedFilePaths.First()); + ImportWindowViewModel importWindowViewModel = new ImportWindowViewModel(MainModel, selectSqlDumpFileDialogResult.SelectedFilePaths.First()); IWindowContext importWindowContext = WindowManager.CreateWindow(RegisteredWindows.WindowKey.IMPORT_WINDOW, importWindowViewModel, CurrentWindowContext); importWindowContext.ShowDialog(); if (IsDefaultSearchTabVisible) @@ -414,29 +561,30 @@ private void Import() public void Synchronize() { - if (mainModel.DatabaseMetadata.NonFictionFirstImportComplete != true) + SynchronizationLocalizator synchronizationLocalizator = MainModel.Localization.CurrentLanguage.Synchronization; + if (MainModel.DatabaseMetadata.NonFictionFirstImportComplete != true) { - MessageBoxWindow.ShowMessage("Ошибка", @"Перед синхронизацией списка нехудожественной литературы необходимо выполнить импорт из дампа базы данных (пункт ""Импорт"" в меню).", CurrentWindowContext); + ShowMessage(synchronizationLocalizator.ErrorMessageTitle, synchronizationLocalizator.ImportRequired); return; } - if (mainModel.AppSettings.Mirrors.NonFictionSynchronizationMirrorName == null) + if (MainModel.AppSettings.Mirrors.NonFictionSynchronizationMirrorName == null) { - MessageBoxWindow.ShowMessage("Ошибка", @"Не выбрано зеркало для синхронизации списка нехудожественной литературы.", CurrentWindowContext); + ShowMessage(synchronizationLocalizator.ErrorMessageTitle, synchronizationLocalizator.NoSynchronizationMirror); return; } - if (mainModel.AppSettings.Network.OfflineMode) + if (MainModel.AppSettings.Network.OfflineMode) { - if (MessageBoxWindow.ShowPrompt("Автономный режим", "Синхронизация невозможна при включенном автономном режиме. Выключить автономный режим?", CurrentWindowContext)) + if (ShowPrompt(synchronizationLocalizator.OfflineModePromptTitle, synchronizationLocalizator.OfflineModePromptText)) { - mainModel.AppSettings.Network.OfflineMode = false; - mainModel.SaveSettings(); + MainModel.AppSettings.Network.OfflineMode = false; + MainModel.SaveSettings(); } else { return; } } - SynchronizationWindowViewModel synchronizationWindowViewModel = new SynchronizationWindowViewModel(mainModel); + SynchronizationWindowViewModel synchronizationWindowViewModel = new SynchronizationWindowViewModel(MainModel); IWindowContext synchronizationWindowContext = WindowManager.CreateWindow(RegisteredWindows.WindowKey.SYNCHRONIZATION_WINDOW, synchronizationWindowViewModel, CurrentWindowContext); synchronizationWindowContext.ShowDialog(); if (IsDefaultSearchTabVisible) @@ -454,14 +602,41 @@ public void Synchronize() private void SettingsMenuItemClick() { - SettingsWindowViewModel settingsWindowViewModel = new SettingsWindowViewModel(mainModel); - IWindowContext settingsWindowContext = WindowManager.CreateWindow(RegisteredWindows.WindowKey.SETTINGS_WINDOW, settingsWindowViewModel, CurrentWindowContext); + SettingsWindowViewModel settingsWindowViewModel = new SettingsWindowViewModel(MainModel); + IWindowContext settingsWindowContext = WindowManager.CreateWindow(RegisteredWindows.WindowKey.SETTINGS_WINDOW, settingsWindowViewModel, + CurrentWindowContext); settingsWindowContext.ShowDialog(); } private void ApplicationUpdateCheckCompleted(object sender, EventArgs e) { - IsApplicationUpdateAvailable = mainModel.LastApplicationUpdateCheckResult != null; + IsApplicationUpdateAvailable = MainModel.LastApplicationUpdateCheckResult != null; + } + + private void LocalizationLanguageChanged(object sender, EventArgs e) + { + Localization = MainModel.Localization.CurrentLanguage.MainWindow; + } + + private void DownloaderEvent(object sender, EventArgs e) + { + if (!(SelectedTabViewModel is DownloadManagerTabViewModel)) + { + switch (e) + { + case DownloadItemAddedEventArgs downloadItemAddedEvent: + IsDownloadManagerButtonHighlighted = true; + break; + case DownloadItemChangedEventArgs downloadItemChangedEvent: + DownloadItem changedDownloadItem = downloadItemChangedEvent.ChangedDownloadItem; + if (changedDownloadItem.Status == DownloadItemStatus.COMPLETED) + { + CompletedDownloadCount++; + IsCompletedDownloadCounterVisible = true; + } + break; + } + } } private void Shutdown(object sender, EventArgs e) @@ -471,7 +646,16 @@ private void Shutdown(object sender, EventArgs e) private void WindowClosed() { - mainModel.AppSettings.MainWindow = new MainWindowSettings + DownloadManagerTabViewModel downloadManagerTabViewModel = DownloadManagerTabViewModel; + if (downloadManagerTabViewModel != null) + { + MainModel.AppSettings.DownloadManagerTab = new DownloadManagerTabSettings + { + LogPanelHeight = downloadManagerTabViewModel.LogPanelHeight, + ShowDebugLogs = downloadManagerTabViewModel.ShowDebugDownloadLogs + }; + } + MainModel.AppSettings.MainWindow = new MainWindowSettings { Width = WindowWidth, Height = WindowHeight, @@ -479,7 +663,7 @@ private void WindowClosed() Top = WindowTop, Maximized = IsWindowMaximized }; - mainModel.SaveSettings(); + MainModel.SaveSettings(); } } } diff --git a/LibgenDesktop/ViewModels/Windows/NonFictionDetailsWindowViewModel.cs b/LibgenDesktop/ViewModels/Windows/NonFictionDetailsWindowViewModel.cs new file mode 100644 index 0000000..d440c54 --- /dev/null +++ b/LibgenDesktop/ViewModels/Windows/NonFictionDetailsWindowViewModel.cs @@ -0,0 +1,35 @@ +using LibgenDesktop.Infrastructure; +using LibgenDesktop.Models; +using LibgenDesktop.Models.Entities; +using LibgenDesktop.Models.Settings; +using LibgenDesktop.ViewModels.Tabs; + +namespace LibgenDesktop.ViewModels.Windows +{ + internal class NonFictionDetailsWindowViewModel : DetailsWindowViewModel + { + public NonFictionDetailsWindowViewModel(MainModel mainModel, NonFictionBook book, bool modalWindow) + : base(mainModel, book, modalWindow) + { + WindowTitle = book.Title; + WindowWidth = mainModel.AppSettings.NonFiction.DetailsWindow.Width; + WindowHeight = mainModel.AppSettings.NonFiction.DetailsWindow.Height; + } + + protected override DetailsTabViewModel CreateDetailsTabViewModel(MainModel mainModel, IWindowContext currentWindowContext, + NonFictionBook libgenObject, bool modalWindow) + { + return new NonFictionDetailsTabViewModel(mainModel, currentWindowContext, libgenObject, modalWindow); + } + + protected override void OnWindowClosing() + { + MainModel.AppSettings.NonFiction.DetailsWindow = new AppSettings.NonFictionDetailsWindowSettings + { + Width = WindowWidth, + Height = WindowHeight + }; + MainModel.SaveSettings(); + } + } +} diff --git a/LibgenDesktop/ViewModels/Windows/SciMagDetailsWindowViewModel.cs b/LibgenDesktop/ViewModels/Windows/SciMagDetailsWindowViewModel.cs new file mode 100644 index 0000000..1ee3fec --- /dev/null +++ b/LibgenDesktop/ViewModels/Windows/SciMagDetailsWindowViewModel.cs @@ -0,0 +1,35 @@ +using LibgenDesktop.Infrastructure; +using LibgenDesktop.Models; +using LibgenDesktop.Models.Entities; +using LibgenDesktop.Models.Settings; +using LibgenDesktop.ViewModels.Tabs; + +namespace LibgenDesktop.ViewModels.Windows +{ + internal class SciMagDetailsWindowViewModel : DetailsWindowViewModel + { + public SciMagDetailsWindowViewModel(MainModel mainModel, SciMagArticle article, bool modalWindow) + : base(mainModel, article, modalWindow) + { + WindowTitle = article.Title; + WindowWidth = mainModel.AppSettings.SciMag.DetailsWindow.Width; + WindowHeight = mainModel.AppSettings.SciMag.DetailsWindow.Height; + } + + protected override DetailsTabViewModel CreateDetailsTabViewModel(MainModel mainModel, IWindowContext currentWindowContext, + SciMagArticle libgenObject, bool modalWindow) + { + return new SciMagDetailsTabViewModel(mainModel, CurrentWindowContext, libgenObject, modalWindow); + } + + protected override void OnWindowClosing() + { + MainModel.AppSettings.SciMag.DetailsWindow = new AppSettings.SciMagDetailsWindowSettings + { + Width = WindowWidth, + Height = WindowHeight + }; + MainModel.SaveSettings(); + } + } +} diff --git a/LibgenDesktop/ViewModels/SettingsWindowViewModel.cs b/LibgenDesktop/ViewModels/Windows/SettingsWindowViewModel.cs similarity index 68% rename from LibgenDesktop/ViewModels/SettingsWindowViewModel.cs rename to LibgenDesktop/ViewModels/Windows/SettingsWindowViewModel.cs index 8fb45ac..8c12ebe 100644 --- a/LibgenDesktop/ViewModels/SettingsWindowViewModel.cs +++ b/LibgenDesktop/ViewModels/Windows/SettingsWindowViewModel.cs @@ -3,30 +3,30 @@ using System.Collections.Generic; using System.Collections.ObjectModel; using System.ComponentModel; +using System.IO; using System.Linq; using LibgenDesktop.Infrastructure; using LibgenDesktop.Models; +using LibgenDesktop.Models.Localization; +using LibgenDesktop.Models.Localization.Localizators; using LibgenDesktop.Models.Settings; -using LibgenDesktop.Views; using static LibgenDesktop.Common.Constants; using static LibgenDesktop.Models.Settings.AppSettings; -namespace LibgenDesktop.ViewModels +namespace LibgenDesktop.ViewModels.Windows { internal class SettingsWindowViewModel : LibgenWindowViewModel, INotifyDataErrorInfo { - private const string NULL_MIRROR_NAME = "нет"; - - private readonly MainModel mainModel; private readonly Dictionary errors; private bool isGeneralTabSelected; private bool isNetworkTabSelected; + private bool isDownloadTabSelected; private bool isMirrorsTabSelected; private bool isSearchTabSelected; private bool isExportTabSelected; private bool isAdvancedTabSelected; - private Dictionary generalLanguagesList; - private KeyValuePair generalSelectedLanguage; + private Dictionary generalLanguagesList; + private KeyValuePair generalSelectedLanguage; private Dictionary generalUpdateCheckIntervalList; private KeyValuePair generalSelectedUpdateCheckInterval; private bool networkIsOfflineModeOn; @@ -35,6 +35,15 @@ internal class SettingsWindowViewModel : LibgenWindowViewModel, INotifyDataError private string networkProxyPort; private string networkProxyUserName; private string networkProxyPassword; + private bool downloadIsOpenInBrowserSelected; + private bool downloadIsUseDownloadManagerSelected; + private string downloadDirectory; + private ObservableCollection downloadTimeoutDefaultValues; + private string downloadTimeout; + private ObservableCollection downloadAttemptCountDefaultValues; + private string downloadAttemptCount; + private ObservableCollection downloadRetryDelayDefaultValues; + private string downloadRetryDelay; private ObservableCollection mirrorsNonFictionBooksMirrorList; private string mirrorsSelectedNonFictionBooksMirror; private ObservableCollection mirrorsNonFictionCoversMirrorList; @@ -62,15 +71,19 @@ internal class SettingsWindowViewModel : LibgenWindowViewModel, INotifyDataError private bool settingsChanged; public SettingsWindowViewModel(MainModel mainModel) + : base(mainModel) { - this.mainModel = mainModel; errors = new Dictionary(); + Localization = mainModel.Localization.CurrentLanguage.Settings; + DownloadSelectDirectoryCommand = new Command(DownloadSelectDirectory); OkCommand = new Command(OkButtonClick); CancelCommand = new Command(CancelButtonClick); WindowClosingCommand = new FuncCommand(WindowClosing); Initialize(); } + public SettingsWindowLocalizator Localization { get; } + public bool IsGeneralTabSelected { get @@ -97,6 +110,19 @@ public bool IsNetworkTabSelected } } + public bool IsDownloadTabSelected + { + get + { + return isDownloadTabSelected; + } + set + { + isDownloadTabSelected = value; + NotifyPropertyChanged(); + } + } + public bool IsMirrorsTabSelected { get @@ -149,7 +175,7 @@ public bool IsAdvancedTabSelected } } - public Dictionary GeneralLanguagesList + public Dictionary GeneralLanguagesList { get { @@ -162,7 +188,7 @@ public Dictionary GeneralLanguagesList } } - public KeyValuePair GeneralSelectedLanguage + public KeyValuePair GeneralSelectedLanguage { get { @@ -290,6 +316,135 @@ public string NetworkProxyPassword } } + public bool DownloadIsOpenInBrowserSelected + { + get + { + return downloadIsOpenInBrowserSelected; + } + set + { + downloadIsOpenInBrowserSelected = value; + NotifyPropertyChanged(); + settingsChanged = true; + Validate(); + } + } + + public bool DownloadIsUseDownloadManagerSelected + { + get + { + return downloadIsUseDownloadManagerSelected; + } + set + { + downloadIsUseDownloadManagerSelected = value; + NotifyPropertyChanged(); + settingsChanged = true; + Validate(); + } + } + + public string DownloadDirectory + { + get + { + return downloadDirectory; + } + set + { + downloadDirectory = value; + NotifyPropertyChanged(); + settingsChanged = true; + Validate(); + } + } + + public ObservableCollection DownloadTimeoutDefaultValues + { + get + { + return downloadTimeoutDefaultValues; + } + set + { + downloadTimeoutDefaultValues = value; + NotifyPropertyChanged(); + } + } + + public string DownloadTimeout + { + get + { + return downloadTimeout; + } + set + { + downloadTimeout = value; + NotifyPropertyChanged(); + settingsChanged = true; + Validate(); + } + } + + public ObservableCollection DownloadAttemptCountDefaultValues + { + get + { + return downloadAttemptCountDefaultValues; + } + set + { + downloadAttemptCountDefaultValues = value; + NotifyPropertyChanged(); + } + } + + public string DownloadAttemptCount + { + get + { + return downloadAttemptCount; + } + set + { + downloadAttemptCount = value; + NotifyPropertyChanged(); + settingsChanged = true; + Validate(); + } + } + + public ObservableCollection DownloadRetryDelayDefaultValues + { + get + { + return downloadRetryDelayDefaultValues; + } + set + { + downloadRetryDelayDefaultValues = value; + NotifyPropertyChanged(); + } + } + + public string DownloadRetryDelay + { + get + { + return downloadRetryDelay; + } + set + { + downloadRetryDelay = value; + NotifyPropertyChanged(); + settingsChanged = true; + Validate(); + } + } + public ObservableCollection MirrorsNonFictionBooksMirrorList { get @@ -594,6 +749,14 @@ public string ExportMaximumRowsPerFile } } + public string ExportExcelLimitNote + { + get + { + return Localization.GetExportExcelLimitNote(MAX_EXPORT_ROWS_PER_FILE + 1); + } + } + public bool AdvancedIsLoggingEnabled { get @@ -623,6 +786,7 @@ public bool IsOkButtonEnabled public bool HasErrors => errors.Any(); + public Command DownloadSelectDirectoryCommand { get; } public Command OkCommand { get; } public Command CancelCommand { get; } public FuncCommand WindowClosingCommand { get; } @@ -642,6 +806,51 @@ private int? NetworkProxyPortValue } } + private int? DownloadTimeoutValue + { + get + { + if (Int32.TryParse(DownloadTimeout, out int value)) + { + if (value >= MIN_DOWNLOAD_TIMEOUT && value <= MAX_DOWNLOAD_TIMEOUT) + { + return value; + } + } + return null; + } + } + + private int? DownloadAttemptCountValue + { + get + { + if (Int32.TryParse(DownloadAttemptCount, out int value)) + { + if (value >= 1 && value <= MAX_DOWNLOAD_ATTEMPT_COUNT) + { + return value; + } + } + return null; + } + } + + private int? DownloadRetryDelayValue + { + get + { + if (Int32.TryParse(DownloadRetryDelay, out int value)) + { + if (value >= 0 && value <= MAX_DOWNLOAD_RETRY_DELAY) + { + return value; + } + } + return null; + } + } + private int? SearchMaximumResultCountValue { get @@ -684,22 +893,23 @@ public IEnumerable GetErrors(string propertyName) private void Initialize() { - AppSettings appSettings = mainModel.AppSettings; + AppSettings appSettings = MainModel.AppSettings; settingsChanged = false; isGeneralTabSelected = true; isNetworkTabSelected = false; + isDownloadTabSelected = false; isMirrorsTabSelected = false; isSearchTabSelected = false; isExportTabSelected = false; isAdvancedTabSelected = false; - generalLanguagesList = mainModel.Languages; - generalSelectedLanguage = generalLanguagesList.First(language => language.Key == appSettings.General.Language); - generalUpdateCheckIntervalList = new Dictionary - { - { GeneralSettings.UpdateCheckInterval.NEVER, "никогда" }, - { GeneralSettings.UpdateCheckInterval.DAILY, "один раз в день" }, - { GeneralSettings.UpdateCheckInterval.WEEKLY, "один раз в неделю" }, - { GeneralSettings.UpdateCheckInterval.MONTHLY, "один раз в месяц" } + generalLanguagesList = MainModel.Localization.Languages.ToDictionary(language => language.DisplayName); + generalSelectedLanguage = generalLanguagesList.First(language => language.Value == MainModel.Localization.CurrentLanguage); + generalUpdateCheckIntervalList = new Dictionary + { + { GeneralSettings.UpdateCheckInterval.NEVER, Localization.GeneralUpdateCheckIntervalNever }, + { GeneralSettings.UpdateCheckInterval.DAILY, Localization.GeneralUpdateCheckIntervalDaily }, + { GeneralSettings.UpdateCheckInterval.WEEKLY, Localization.GeneralUpdateCheckIntervalWeekly }, + { GeneralSettings.UpdateCheckInterval.MONTHLY, Localization.GeneralUpdateCheckIntervalMonthly } }; generalSelectedUpdateCheckInterval = generalUpdateCheckIntervalList.First(interval => interval.Key == appSettings.General.UpdateCheck); networkIsOfflineModeOn = appSettings.Network.OfflineMode; @@ -708,6 +918,15 @@ private void Initialize() networkProxyPort = appSettings.Network.ProxyPort?.ToString() ?? String.Empty; networkProxyUserName = appSettings.Network.ProxyUserName; networkProxyPassword = appSettings.Network.ProxyPassword; + downloadIsOpenInBrowserSelected = !appSettings.Download.UseDownloadManager; + downloadIsUseDownloadManagerSelected = !downloadIsOpenInBrowserSelected; + downloadDirectory = appSettings.Download.DownloadDirectory; + downloadTimeoutDefaultValues = new ObservableCollection { "30", "60", "90", "120", "180", "300", "600", "1200" }; + downloadTimeout = appSettings.Download.Timeout.ToString(); + downloadAttemptCountDefaultValues = new ObservableCollection { "0", "1", "2", "3", "5", "10", "15", "20" }; + downloadAttemptCount = appSettings.Download.Attempts.ToString(); + downloadRetryDelayDefaultValues = new ObservableCollection { "60", "120", "180", "300", "600", "1200" }; + downloadRetryDelay = appSettings.Download.RetryDelay.ToString(); mirrorsNonFictionBooksMirrorList = BuildMirrorList(mirror => mirror.NonFictionDownloadUrl); mirrorsNonFictionCoversMirrorList = BuildMirrorList(mirror => mirror.NonFictionCoverUrl); mirrorsNonFictionSynchronizationMirrorList = BuildMirrorList(mirror => mirror.NonFictionSynchronizationUrl); @@ -748,21 +967,35 @@ private void Initialize() private ObservableCollection BuildMirrorList(Func mirrorProperty) { - return new ObservableCollection(new[] { NULL_MIRROR_NAME }. - Concat(mainModel.Mirrors.Where(mirror => !String.IsNullOrWhiteSpace(mirrorProperty(mirror.Value))).Select(mirror => mirror.Key))); + return new ObservableCollection(new[] { Localization.MirrorsNoMirror }. + Concat(MainModel.Mirrors.Where(mirror => !String.IsNullOrWhiteSpace(mirrorProperty(mirror.Value))).Select(mirror => mirror.Key))); } private void Validate() { bool networkProxyAddressValid = !NetworkUseProxy || !String.IsNullOrWhiteSpace(NetworkProxyAddress); bool networkProxyPortValid = !NetworkUseProxy || NetworkProxyPortValue.HasValue; + bool downloadDirectoryValid = DownloadIsOpenInBrowserSelected || + (!String.IsNullOrWhiteSpace(DownloadDirectory) && Directory.Exists(DownloadDirectory)); + bool downloadTimeoutValid = DownloadIsOpenInBrowserSelected || DownloadTimeoutValue.HasValue; + bool downloadAttemptCountValid = DownloadIsOpenInBrowserSelected || DownloadAttemptCountValue.HasValue; + bool downloadRetryDelayValid = DownloadIsOpenInBrowserSelected || DownloadRetryDelayValue.HasValue; bool searchMaximumResultCountValid = !SearchIsLimitResultsOn || SearchMaximumResultCountValue.HasValue; bool exportMaximumRowsPerFileValid = !ExportIsSplitIntoMultipleFilesEnabled || ExportMaximumRowsPerFileValue.HasValue; - UpdateValidationState(nameof(NetworkProxyAddress), networkProxyAddressValid, "Обязательное поле"); - UpdateValidationState(nameof(NetworkProxyPort), networkProxyPortValid, $"Числа от {MIN_PROXY_PORT} до {MAX_PROXY_PORT}"); - UpdateValidationState(nameof(SearchMaximumResultCount), searchMaximumResultCountValid, "Только положительные числа"); - UpdateValidationState(nameof(ExportMaximumRowsPerFile), exportMaximumRowsPerFileValid, $"Числа от 1 до {MAX_EXPORT_ROWS_PER_FILE}"); - IsOkButtonEnabled = networkProxyAddressValid && networkProxyPortValid && searchMaximumResultCountValid && exportMaximumRowsPerFileValid; + UpdateValidationState(nameof(NetworkProxyAddress), networkProxyAddressValid, Localization.NetworkProxyAddressRequired); + UpdateValidationState(nameof(NetworkProxyPort), networkProxyPortValid, Localization.GetNetworkProxyPortValidation(MIN_PROXY_PORT, MAX_PROXY_PORT)); + UpdateValidationState(nameof(DownloadDirectory), downloadDirectoryValid, Localization.DownloadDownloadDirectoryNotFound); + UpdateValidationState(nameof(DownloadTimeout), downloadTimeoutValid, + Localization.GetDownloadTimeoutValidation(MIN_DOWNLOAD_TIMEOUT, MAX_DOWNLOAD_TIMEOUT)); + UpdateValidationState(nameof(DownloadAttemptCount), downloadAttemptCountValid, + Localization.GetDownloadDownloadAttemptsValidation(1, MAX_DOWNLOAD_ATTEMPT_COUNT)); + UpdateValidationState(nameof(DownloadRetryDelay), downloadRetryDelayValid, + Localization.GetDownloadRetryDelayValidation(0, MAX_DOWNLOAD_RETRY_DELAY)); + UpdateValidationState(nameof(SearchMaximumResultCount), searchMaximumResultCountValid, Localization.SearchPositiveNumbersOnly); + UpdateValidationState(nameof(ExportMaximumRowsPerFile), exportMaximumRowsPerFileValid, + Localization.GetExportMaximumRowsPerFileValidation(1, MAX_EXPORT_ROWS_PER_FILE)); + IsOkButtonEnabled = networkProxyAddressValid && networkProxyPortValid && downloadDirectoryValid && downloadTimeoutValid && + downloadAttemptCountValid && downloadRetryDelayValid && searchMaximumResultCountValid && exportMaximumRowsPerFileValid; } private void UpdateValidationState(string propertyName, bool isValid, string errorText) @@ -779,23 +1012,45 @@ private void UpdateValidationState(string propertyName, bool isValid, string err } } + private void DownloadSelectDirectory() + { + SelectFolderDialogParameters selectFolderDialogParameters = new SelectFolderDialogParameters + { + DialogTitle = Localization.DownloadBrowseDirectoryDialogTitle, + InitialDirectory = DownloadDirectory + }; + SelectFolderDialogResult selectFolderDialogResult = WindowManager.ShowSelectFolderDialog(selectFolderDialogParameters); + if (selectFolderDialogResult.DialogResult) + { + DownloadDirectory = selectFolderDialogResult.SelectedFolderPath; + } + } + private void OkButtonClick() { - mainModel.AppSettings.General = new GeneralSettings + MainModel.AppSettings.General = new GeneralSettings { - Language = GeneralSelectedLanguage.Key, + Language = GeneralSelectedLanguage.Value.Name, UpdateCheck = GeneralSelectedUpdateCheckInterval.Key }; - mainModel.AppSettings.Network = new NetworkSettings + MainModel.AppSettings.Network = new NetworkSettings { OfflineMode = NetworkIsOfflineModeOn, UseProxy = NetworkUseProxy, ProxyAddress = NetworkProxyAddress, ProxyPort = NetworkProxyPortValue, - ProxyUserName = NetworkProxyUserName, - ProxyPassword = NetworkProxyPassword + ProxyUserName = NetworkProxyUserName ?? String.Empty, + ProxyPassword = NetworkProxyPassword ?? String.Empty + }; + MainModel.AppSettings.Download = new DownloadSettings + { + UseDownloadManager = DownloadIsUseDownloadManagerSelected, + DownloadDirectory = DownloadDirectory, + Timeout = DownloadTimeoutValue ?? DEFAULT_DOWNLOAD_TIMEOUT, + Attempts = DownloadAttemptCountValue ?? DEFAULT_DOWNLOAD_ATTEMPT_COUNT, + RetryDelay = DownloadRetryDelayValue ?? DEFAULT_DOWNLOAD_RETRY_DELAY }; - mainModel.AppSettings.Mirrors = new MirrorSettings + MainModel.AppSettings.Mirrors = new MirrorSettings { NonFictionBooksMirrorName = ParseDisplayMirrorName(MirrorsSelectedNonFictionBooksMirror), NonFictionCoversMirrorName = ParseDisplayMirrorName(MirrorsSelectedNonFictionCoversMirror), @@ -804,44 +1059,46 @@ private void OkButtonClick() FictionCoversMirrorName = ParseDisplayMirrorName(MirrorsSelectedFictionCoversMirror), ArticlesMirrorMirrorName = ParseDisplayMirrorName(MirrorsSelectedArticlesMirror) }; - mainModel.AppSettings.Search = new SearchSettings + MainModel.AppSettings.Search = new SearchSettings { LimitResults = SearchIsLimitResultsOn, MaximumResultCount = SearchMaximumResultCountValue ?? DEFAULT_MAXIMUM_SEARCH_RESULT_COUNT }; if (SearchIsOpenInModalWindowSelected) { - mainModel.AppSettings.Search.OpenDetailsMode = SearchSettings.DetailsMode.NEW_MODAL_WINDOW; + MainModel.AppSettings.Search.OpenDetailsMode = SearchSettings.DetailsMode.NEW_MODAL_WINDOW; } else if (SearchIsOpenInNonModalWindowSelected) { - mainModel.AppSettings.Search.OpenDetailsMode = SearchSettings.DetailsMode.NEW_NON_MODAL_WINDOW; + MainModel.AppSettings.Search.OpenDetailsMode = SearchSettings.DetailsMode.NEW_NON_MODAL_WINDOW; } else if (SearchIsOpenInNewTabSelected) { - mainModel.AppSettings.Search.OpenDetailsMode = SearchSettings.DetailsMode.NEW_TAB; + MainModel.AppSettings.Search.OpenDetailsMode = SearchSettings.DetailsMode.NEW_TAB; } - mainModel.AppSettings.Export = new ExportSettings + MainModel.AppSettings.Export = new ExportSettings { OpenResultsAfterExport = ExportIsOpenResultsAfterExportEnabled, SplitIntoMultipleFiles = ExportIsSplitIntoMultipleFilesEnabled, MaximumRowsPerFile = ExportMaximumRowsPerFileValue ?? MAX_EXPORT_ROWS_PER_FILE }; - if (advancedIsLoggingEnabled != mainModel.AppSettings.Advanced.LoggingEnabled) + if (advancedIsLoggingEnabled != MainModel.AppSettings.Advanced.LoggingEnabled) { - mainModel.AppSettings.Advanced.LoggingEnabled = advancedIsLoggingEnabled; + MainModel.AppSettings.Advanced.LoggingEnabled = advancedIsLoggingEnabled; if (advancedIsLoggingEnabled) { - mainModel.EnableLogging(); + MainModel.EnableLogging(); } else { - mainModel.DisableLogging(); + MainModel.DisableLogging(); } } - mainModel.SaveSettings(); - mainModel.CreateNewHttpClient(); - mainModel.ConfigureUpdater(); + MainModel.SaveSettings(); + MainModel.Localization.SwitchLanguage(GeneralSelectedLanguage.Value); + MainModel.CreateNewHttpClient(); + MainModel.ConfigureUpdater(); + MainModel.ConfigureDownloader(); settingsChanged = false; CurrentWindowContext.CloseDialog(true); } @@ -853,17 +1110,17 @@ private void CancelButtonClick() private string GetDisplayMirrorName(string mirrorName) { - return mirrorName ?? NULL_MIRROR_NAME; + return mirrorName ?? Localization.MirrorsNoMirror; } private string ParseDisplayMirrorName(string displayMirrorName) { - return displayMirrorName != NULL_MIRROR_NAME ? displayMirrorName : null; + return displayMirrorName != Localization.MirrorsNoMirror ? displayMirrorName : null; } private bool WindowClosing() { - return !settingsChanged || MessageBoxWindow.ShowPrompt("Отменить изменения?", "Настройки были изменены. Вы действительно хотите отменить сделанные изменения?", CurrentWindowContext); + return !settingsChanged || ShowPrompt(Localization.DiscardChangesPromptTitle, Localization.DiscardChangesPromptText); } } } diff --git a/LibgenDesktop/ViewModels/SynchronizationWindowViewModel.cs b/LibgenDesktop/ViewModels/Windows/SynchronizationWindowViewModel.cs similarity index 74% rename from LibgenDesktop/ViewModels/SynchronizationWindowViewModel.cs rename to LibgenDesktop/ViewModels/Windows/SynchronizationWindowViewModel.cs index a16f5f1..5294e05 100644 --- a/LibgenDesktop/ViewModels/SynchronizationWindowViewModel.cs +++ b/LibgenDesktop/ViewModels/Windows/SynchronizationWindowViewModel.cs @@ -3,10 +3,11 @@ using System.Threading; using LibgenDesktop.Infrastructure; using LibgenDesktop.Models; +using LibgenDesktop.Models.Localization.Localizators; using LibgenDesktop.Models.ProgressArgs; -using LibgenDesktop.Models.Utils; +using LibgenDesktop.ViewModels.Panels; -namespace LibgenDesktop.ViewModels +namespace LibgenDesktop.ViewModels.Windows { internal class SynchronizationWindowViewModel : LibgenWindowViewModel { @@ -18,7 +19,6 @@ private enum Step SYNCHRONIZATION } - private readonly MainModel mainModel; private readonly CancellationTokenSource cancellationTokenSource; private readonly Timer elapsedTimer; private bool isInProgress; @@ -35,9 +35,10 @@ private enum Step private TimeSpan lastElapsedTime; public SynchronizationWindowViewModel(MainModel mainModel) + : base(mainModel) { - this.mainModel = mainModel; cancellationTokenSource = new CancellationTokenSource(); + Localization = mainModel.Localization.CurrentLanguage.Synchronization; elapsedTimer = new Timer(state => UpdateElapsedTime()); Logs = new ImportLogPanelViewModel(); CancelCommand = new Command(Cancel); @@ -46,6 +47,7 @@ public SynchronizationWindowViewModel(MainModel mainModel) Initialize(); } + public SynchronizationLocalizator Localization { get; } public ImportLogPanelViewModel Logs { get; } public bool IsInProgress @@ -158,12 +160,12 @@ private void Initialize() currentStep = Step.PREPARATION; currentStepIndex = 0; totalSteps = 2; - UpdateStatus("Подготовка к синхронизации"); + UpdateStatus(Localization.StatusPreparation); startDateTime = DateTime.Now; lastElapsedTime = TimeSpan.Zero; elapsedTimer.Change(TimeSpan.Zero, TimeSpan.FromMilliseconds(100)); elapsed = GetElapsedString(lastElapsedTime); - cancelButtonText = "ПРЕРВАТЬ"; + cancelButtonText = Localization.Interrupt; isCancelButtonVisible = true; isCancelButtonEnabled = true; isCloseButtonVisible = false; @@ -177,14 +179,14 @@ private async void Syncrhonize() MainModel.SynchronizationResult synchronizationResult; try { - synchronizationResult = await mainModel.SynchronizeNonFictionAsync(synchronizationProgressHandler, cancellationToken); + synchronizationResult = await MainModel.SynchronizeNonFictionAsync(synchronizationProgressHandler, cancellationToken); } catch (Exception exception) { elapsedTimer.Change(Timeout.InfiniteTimeSpan, Timeout.InfiniteTimeSpan); - Logs.ShowErrorLogLine("Синхронизация завершилась с ошибками."); + Logs.ShowErrorLogLine(Localization.LogLineSynchronizationError); IsInProgress = false; - Status = "Синхронизация завершилась с ошибками"; + Status = Localization.StatusSynchronizationError; IsCancelButtonVisible = false; IsCloseButtonVisible = true; ShowErrorWindow(exception, CurrentWindowContext); @@ -194,12 +196,12 @@ private async void Syncrhonize() switch (synchronizationResult) { case MainModel.SynchronizationResult.COMPLETED: - Logs.ShowResultLogLine("Синхронизация выполнена успешно."); - Status = "Синхронизация завершена"; + Logs.ShowResultLogLine(Localization.LogLineSynchronizationSuccessful); + Status = Localization.StatusSynchronizationComplete; break; case MainModel.SynchronizationResult.CANCELLED: - Logs.ShowErrorLogLine("Синхронизация была прервана пользователем."); - Status = "Синхронизация прервана"; + Logs.ShowErrorLogLine(Localization.LogLineSynchronizationCancelled); + Status = Localization.StatusSynchronizationCancelled; break; } IsInProgress = false; @@ -219,20 +221,20 @@ private void HandleSynchronizationProgress(object progress) currentStep = Step.CREATING_INDEXES; currentStepIndex++; totalSteps++; - UpdateStatus("Создание индексов"); - Logs.AddLogItem(currentStepIndex, "Создание недостающих индексов"); + UpdateStatus(Localization.StatusCreatingIndexes); + Logs.AddLogItem(Localization.GetLogLineStep(currentStepIndex), Localization.LogLineCreatingIndexes); } - CurrentLogItem.LogLines.Add($"Создается индекс для столбца {createIndexProgress.ColumnName}..."); + CurrentLogItem.LogLines.Add(Localization.GetLogLineCreatingIndexForColumn(createIndexProgress.ColumnName)); break; case ImportLoadLibgenIdsProgress importLoadLibgenIdsProgress: if (currentStep != Step.LOADING_EXISTING_IDS) { currentStep = Step.LOADING_EXISTING_IDS; currentStepIndex++; - UpdateStatus("Загрузка идентификаторов"); - Logs.AddLogItem(currentStepIndex, "Загрузка идентификаторов существующих данных"); + UpdateStatus(Localization.StatusLoadingIds); + Logs.AddLogItem(Localization.GetLogLineStep(currentStepIndex), Localization.LogLineLoadingIds); } - CurrentLogItem.LogLines.Add($"Загрузка значений столбца LibgenId..."); + CurrentLogItem.LogLines.Add(Localization.GetLogLineLoadingColumnValues("LibgenId")); break; case SynchronizationProgress synchronizationProgress: string secondLogLine = GetSynchronizedBookCountLogLine(synchronizationProgress.ObjectsDownloaded, synchronizationProgress.ObjectsAdded, @@ -241,9 +243,10 @@ private void HandleSynchronizationProgress(object progress) { currentStep = Step.SYNCHRONIZATION; currentStepIndex++; - Logs.AddLogItem(currentStepIndex, "Синхронизация списка книг", "Скачивание информации о новых книгах"); + Logs.AddLogItem(Localization.GetLogLineStep(currentStepIndex), Localization.LogLineSynchronizingBookList, + Localization.LogLineDownloadingNewBooks); CurrentLogItem.LogLines.Add(secondLogLine); - UpdateStatus("Синхронизация"); + UpdateStatus(Localization.StatusSynchronizingData); } else { @@ -272,7 +275,7 @@ private void UpdateElapsedTime() private void Cancel() { IsCancelButtonEnabled = false; - CancelButtonText = "ПРЕРЫВАЕТСЯ..."; + CancelButtonText = Localization.Interrupting; cancellationTokenSource.Cancel(); } @@ -291,11 +294,8 @@ private void UpdateStatus(string statusDescription) StringBuilder statusBuilder = new StringBuilder(); if (currentStepIndex > 0) { - statusBuilder.Append("Шаг "); - statusBuilder.Append(currentStepIndex); - statusBuilder.Append(" из "); - statusBuilder.Append(totalSteps); - statusBuilder.Append(". "); + statusBuilder.Append(Localization.GetStatusStep(currentStepIndex, totalSteps)); + statusBuilder.Append(" "); } statusBuilder.Append(statusDescription); statusBuilder.Append("..."); @@ -304,33 +304,42 @@ private void UpdateStatus(string statusDescription) private string GetSynchronizedBookCountLogLine(int downloadedObjectCount, int addedObjectCount, int updatedObjectCount) { - StringBuilder resultBuilder = new StringBuilder(); - resultBuilder.Append("Скачано книг: "); - resultBuilder.Append(downloadedObjectCount.ToFormattedString()); if (addedObjectCount > 0) { - resultBuilder.Append(", добавлено книг: "); - resultBuilder.Append(addedObjectCount.ToFormattedString()); + if (updatedObjectCount > 0) + { + return Localization.GetLogLineSynchronizationProgressAddedAndUpdated(downloadedObjectCount, addedObjectCount, updatedObjectCount); + } + else + { + return Localization.GetLogLineSynchronizationProgressAdded(downloadedObjectCount, addedObjectCount); + } } - if (updatedObjectCount > 0) + else { - resultBuilder.Append(", обновлено книг: "); - resultBuilder.Append(updatedObjectCount.ToFormattedString()); + if (updatedObjectCount > 0) + { + return Localization.GetLogLineSynchronizationProgressUpdated(downloadedObjectCount, updatedObjectCount); + } + else + { + return Localization.GetLogLineSynchronizationProgressNoAddedNoUpdated(downloadedObjectCount); + } } - resultBuilder.Append("."); - return resultBuilder.ToString(); } private string GetElapsedString(TimeSpan elapsedTime) { + string elapsed; if (elapsedTime.Hours > 0) { - return $"Прошло {Math.Truncate(elapsedTime.TotalHours)}:{elapsedTime:mm\\:ss}"; + elapsed = $"{Math.Truncate(elapsedTime.TotalHours)}:{elapsedTime:mm\\:ss}"; } else { - return $"Прошло {elapsedTime:mm\\:ss}"; + elapsed = $"{elapsedTime:mm\\:ss}"; } + return Localization.GetElapsedString(elapsed); } } } diff --git a/LibgenDesktop/Views/Controls/AddTabButton.xaml b/LibgenDesktop/Views/Controls/AddTabButton.xaml index 36f9758..4b73cdd 100644 --- a/LibgenDesktop/Views/Controls/AddTabButton.xaml +++ b/LibgenDesktop/Views/Controls/AddTabButton.xaml @@ -4,14 +4,14 @@ xmlns:md="http://materialdesigninxaml.net/winfx/xaml/themes"> - - + - - + + diff --git a/LibgenDesktop/Views/Controls/AddTabButton.xaml.cs b/LibgenDesktop/Views/Controls/AddTabButton.xaml.cs index 81aed2a..92b5d6a 100644 --- a/LibgenDesktop/Views/Controls/AddTabButton.xaml.cs +++ b/LibgenDesktop/Views/Controls/AddTabButton.xaml.cs @@ -6,6 +6,5 @@ public AddTabButton() { InitializeComponent(); } - } } diff --git a/LibgenDesktop/Views/Controls/AutoScrollViewer.cs b/LibgenDesktop/Views/Controls/AutoScrollViewer.cs new file mode 100644 index 0000000..47a81fc --- /dev/null +++ b/LibgenDesktop/Views/Controls/AutoScrollViewer.cs @@ -0,0 +1,27 @@ +using System.Windows.Controls; + +namespace LibgenDesktop.Views.Controls +{ + internal class AutoScrollViewer : ScrollViewer + { + private bool autoScroll; + + public AutoScrollViewer() + { + autoScroll = false; + } + + protected override void OnScrollChanged(ScrollChangedEventArgs e) + { + base.OnScrollChanged(e); + if (e.ExtentHeightChange == 0) + { + autoScroll = VerticalOffset == ScrollableHeight; + } + if (autoScroll && e.ExtentHeightChange != 0) + { + ScrollToVerticalOffset(ExtentHeight); + } + } + } +} diff --git a/LibgenDesktop/Views/Controls/BookAttributeValueLabel.xaml.cs b/LibgenDesktop/Views/Controls/BookAttributeValueLabel.xaml.cs index e02525a..5f8f043 100644 --- a/LibgenDesktop/Views/Controls/BookAttributeValueLabel.xaml.cs +++ b/LibgenDesktop/Views/Controls/BookAttributeValueLabel.xaml.cs @@ -8,6 +8,9 @@ namespace LibgenDesktop.Views.Controls { public partial class BookAttributeValueLabel { + public static readonly DependencyProperty ContextMenuItemFormatProperty = DependencyProperty.Register("ContextMenuItemFormat", typeof(string), + typeof(BookAttributeValueLabel), new PropertyMetadata(OnContextMenuItemFormatChanged)); + public BookAttributeValueLabel() { InitializeComponent(); @@ -15,20 +18,53 @@ public BookAttributeValueLabel() propertyDescriptor.AddValueChanged(this, TextChanged); } + public string ContextMenuItemFormat + { + get + { + return (string)GetValue(ContextMenuItemFormatProperty); + } + set + { + SetValue(ContextMenuItemFormatProperty, value); + } + } + private void CopyMenuItemClick(object sender, RoutedEventArgs e) { WindowManager.SetClipboardText(Text); } - private void TextChanged(object sender, EventArgs e) + private void CreateContextMenu() { if (!String.IsNullOrWhiteSpace(Text)) { ContextMenu labelContextMenu = Resources["labelContextMenu"] as ContextMenu; MenuItem copyMenuItem = labelContextMenu.Items[0] as MenuItem; - copyMenuItem.Header = $"Копировать \"{Text}\""; + string contextMenuItemFormat = ContextMenuItemFormat; + if (contextMenuItemFormat.Contains("{0}")) + { + copyMenuItem.Header = String.Format(contextMenuItemFormat, Text); + } + else + { + copyMenuItem.Header = contextMenuItemFormat; + } ContextMenu = labelContextMenu; } } + + private void TextChanged(object sender, EventArgs e) + { + CreateContextMenu(); + } + + private static void OnContextMenuItemFormatChanged(DependencyObject dependencyObject, DependencyPropertyChangedEventArgs e) + { + if (dependencyObject is BookAttributeValueLabel bookAttributeValueLabel) + { + bookAttributeValueLabel.CreateContextMenu(); + } + } } } diff --git a/LibgenDesktop/Views/Controls/CloseTabButton.xaml b/LibgenDesktop/Views/Controls/CloseTabButton.xaml index 479ca42..1516f37 100644 --- a/LibgenDesktop/Views/Controls/CloseTabButton.xaml +++ b/LibgenDesktop/Views/Controls/CloseTabButton.xaml @@ -4,14 +4,14 @@ xmlns:md="http://materialdesigninxaml.net/winfx/xaml/themes"> - - + - - + + diff --git a/LibgenDesktop/Views/Controls/CloseTabButton.xaml.cs b/LibgenDesktop/Views/Controls/CloseTabButton.xaml.cs index de10d36..ee499bd 100644 --- a/LibgenDesktop/Views/Controls/CloseTabButton.xaml.cs +++ b/LibgenDesktop/Views/Controls/CloseTabButton.xaml.cs @@ -6,6 +6,5 @@ public CloseTabButton() { InitializeComponent(); } - } } diff --git a/LibgenDesktop/Views/Controls/DownloaderListBox.cs b/LibgenDesktop/Views/Controls/DownloaderListBox.cs new file mode 100644 index 0000000..c5768d5 --- /dev/null +++ b/LibgenDesktop/Views/Controls/DownloaderListBox.cs @@ -0,0 +1,67 @@ +using System.Windows; +using System.Windows.Controls; +using System.Windows.Input; +using System.Windows.Media; + +namespace LibgenDesktop.Views.Controls +{ + internal class DownloaderListBox : ListBox + { + public static readonly DependencyProperty SelectionChangedCommandProperty = DependencyProperty.Register("SelectionChangedCommand", typeof(ICommand), typeof(DownloaderListBox)); + public static readonly DependencyProperty DoubleClickCommandProperty = DependencyProperty.Register("DoubleClickCommand", typeof(ICommand), typeof(DownloaderListBox)); + + public ICommand SelectionChangedCommand + { + get + { + return (ICommand)GetValue(SelectionChangedCommandProperty); + } + set + { + SetValue(SelectionChangedCommandProperty, value); + } + } + + public ICommand DoubleClickCommand + { + get + { + return (ICommand)GetValue(DoubleClickCommandProperty); + } + set + { + SetValue(DoubleClickCommandProperty, value); + } + } + + protected override void OnSelectionChanged(SelectionChangedEventArgs e) + { + ICommand selectionChangedCommand = SelectionChangedCommand; + if (selectionChangedCommand != null) + { + selectionChangedCommand.Execute(null); + } + base.OnSelectionChanged(e); + } + + protected override void OnMouseDoubleClick(MouseButtonEventArgs e) + { + base.OnMouseDoubleClick(e); + ICommand doubleClickCommand = DoubleClickCommand; + if (doubleClickCommand != null) + { + doubleClickCommand.Execute(null); + } + } + + protected override void OnMouseDown(MouseButtonEventArgs e) + { + base.OnMouseDown(e); + HitTestResult r = VisualTreeHelper.HitTest(this, e.GetPosition(this)); + if (r.VisualHit.GetType() != typeof(ListBoxItem)) + { + UnselectAll(); + } + } + } +} diff --git a/LibgenDesktop/Views/Controls/ExportPanel.xaml b/LibgenDesktop/Views/Controls/ExportPanel.xaml index 7dc5326..d95e1ca 100644 --- a/LibgenDesktop/Views/Controls/ExportPanel.xaml +++ b/LibgenDesktop/Views/Controls/ExportPanel.xaml @@ -9,7 +9,7 @@ - + @@ -23,52 +23,52 @@ - + - + - + - - + - + - + - + - + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/LibgenDesktop/Views/Styles/MainWindowStyles.xaml b/LibgenDesktop/Views/Styles/MainWindowStyles.xaml index 0b72760..66606e2 100644 --- a/LibgenDesktop/Views/Styles/MainWindowStyles.xaml +++ b/LibgenDesktop/Views/Styles/MainWindowStyles.xaml @@ -7,11 +7,12 @@ + + @@ -57,16 +76,44 @@ - + - + + + + + + + + + + + + + - + - + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/LibgenDesktop/Views/Tabs/DownloadManagerTab.xaml.cs b/LibgenDesktop/Views/Tabs/DownloadManagerTab.xaml.cs index 448e182..edd91a4 100644 --- a/LibgenDesktop/Views/Tabs/DownloadManagerTab.xaml.cs +++ b/LibgenDesktop/Views/Tabs/DownloadManagerTab.xaml.cs @@ -11,6 +11,13 @@ public DownloadManagerTab() public void OnViewModelEvent(ViewModelEvent viewModelEvent) { + if (viewModelEvent.EventId == ViewModelEvent.RegisteredEventId.SCROLL_TO_SELECTION) + { + if (downloaderListBox.SelectedItems.Count > 0) + { + downloaderListBox.ScrollIntoView(downloaderListBox.SelectedItems[0]); + } + } } } } diff --git a/LibgenDesktop/Views/Tabs/FictionDetailsTab.xaml b/LibgenDesktop/Views/Tabs/FictionDetailsTab.xaml index 7f3adb2..e5a57e8 100644 --- a/LibgenDesktop/Views/Tabs/FictionDetailsTab.xaml +++ b/LibgenDesktop/Views/Tabs/FictionDetailsTab.xaml @@ -1,6 +1,7 @@  @@ -14,13 +15,18 @@ - - + - + + + + @@ -49,52 +55,54 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + - - - - - - - - + + + + + + + + - - - -