From a53d2c8000c123a58e99c293ea521c7bc099b479 Mon Sep 17 00:00:00 2001 From: libgenapps <33476799+libgenapps@users.noreply.github.com> Date: Sun, 28 Jan 2018 05:05:16 +0300 Subject: [PATCH] Update check --- LibgenDesktop.Setup/Constants.cs | 2 +- LibgenDesktop/App.xaml.cs | 4 +- LibgenDesktop/Common/Constants.cs | 11 +- LibgenDesktop/Common/Environment.cs | 2 +- LibgenDesktop/Common/Logger.cs | 1 + .../Infrastructure/RegisteredWindows.cs | 6 +- LibgenDesktop/LibgenDesktop.csproj | 34 +- .../Models/Database/LocalDatabase.cs | 111 ++++++- LibgenDesktop/Models/Database/SqlScripts.cs | 16 + .../Models/Entities/DatabaseMetadata.cs | 17 + .../Models/Import/FictionImporter.cs | 7 +- LibgenDesktop/Models/Import/Importer.cs | 85 +++-- .../Models/Import/NonFictionImporter.cs | 11 +- LibgenDesktop/Models/Import/SciMagImporter.cs | 7 +- LibgenDesktop/Models/JsonApi/JsonApiClient.cs | 5 +- .../Localization/LocalizationStorage.cs | 15 + LibgenDesktop/Models/MainModel.cs | 211 ++++++++++-- .../ProgressArgs/DownloadFileProgress.cs | 14 + .../ImportLoadLibgenIdsProgress.cs | 6 + .../ProgressArgs/JsonApiDownloadProgress.cs | 12 - .../ProgressArgs/SynchronizationProgress.cs | 16 + LibgenDesktop/Models/Settings/AppSettings.cs | 75 ++++- .../Models/Update/GitHubApiRelease.cs | 30 ++ LibgenDesktop/Models/Update/Updater.cs | 167 ++++++++++ LibgenDesktop/Models/Utils/UrlGenerator.cs | 209 +++++++++++- .../ApplicationUpdateWindowViewModel.cs | 303 ++++++++++++++++++ ...el.cs => CreateDatabaseWindowViewModel.cs} | 4 +- .../FictionSearchResultsTabViewModel.cs | 1 + .../ViewModels/ImportLogPanelViewModel.cs | 186 +++++++++++ .../ViewModels/ImportWindowViewModel.cs | 114 ++----- .../ViewModels/MainWindowViewModel.cs | 51 ++- .../ViewModels/ProgressLogItemViewModel.cs | 77 ----- .../SciMagSearchResultsTabViewModel.cs | 1 + .../ViewModels/SettingsWindowViewModel.cs | 148 +++++++-- .../SynchronizationWindowViewModel.cs | 161 +++------- .../Views/ApplicationUpdateWindow.xaml | 41 +++ .../Views/ApplicationUpdateWindow.xaml.cs | 10 + .../Views/Controls/ControlExtensions.cs | 41 ++- .../Views/Controls/ImportLogPanel.xaml | 35 ++ .../Views/Controls/ImportLogPanel.xaml.cs | 28 ++ LibgenDesktop/Views/Controls/Toolbar.xaml | 50 ++- LibgenDesktop/Views/ImportWindow.xaml | 28 +- LibgenDesktop/Views/MainWindow.xaml | 8 +- LibgenDesktop/Views/MainWindow.xaml.cs | 10 +- LibgenDesktop/Views/SettingsWindow.xaml | 21 ++ .../Styles/ApplicationUpdateWindowStyles.xaml | 25 ++ LibgenDesktop/Views/Styles/CommonStyles.xaml | 4 +- .../Views/Styles/ImportLogPanelStyles.xaml | 30 ++ .../Views/Styles/ImportWindowStyles.xaml | 23 -- .../Views/Styles/SettingsWindowStyles.xaml | 14 +- LibgenDesktop/Views/Styles/ToolbarStyles.xaml | 28 +- LibgenDesktop/Views/Styles/WindowStyles.xaml | 6 + .../Views/SynchronizationWindow.xaml | 28 +- .../Views/Tabs/DownloadManagerTab.xaml | 4 - .../Views/Utils/PasswordBoxExtensions.cs | 6 +- 55 files changed, 2032 insertions(+), 528 deletions(-) create mode 100644 LibgenDesktop/Models/Localization/LocalizationStorage.cs create mode 100644 LibgenDesktop/Models/ProgressArgs/DownloadFileProgress.cs create mode 100644 LibgenDesktop/Models/ProgressArgs/ImportLoadLibgenIdsProgress.cs delete mode 100644 LibgenDesktop/Models/ProgressArgs/JsonApiDownloadProgress.cs create mode 100644 LibgenDesktop/Models/ProgressArgs/SynchronizationProgress.cs create mode 100644 LibgenDesktop/Models/Update/GitHubApiRelease.cs create mode 100644 LibgenDesktop/Models/Update/Updater.cs create mode 100644 LibgenDesktop/ViewModels/ApplicationUpdateWindowViewModel.cs rename LibgenDesktop/ViewModels/{CreateDatabaseViewModel.cs => CreateDatabaseWindowViewModel.cs} (97%) create mode 100644 LibgenDesktop/ViewModels/ImportLogPanelViewModel.cs delete mode 100644 LibgenDesktop/ViewModels/ProgressLogItemViewModel.cs create mode 100644 LibgenDesktop/Views/ApplicationUpdateWindow.xaml create mode 100644 LibgenDesktop/Views/ApplicationUpdateWindow.xaml.cs create mode 100644 LibgenDesktop/Views/Controls/ImportLogPanel.xaml create mode 100644 LibgenDesktop/Views/Controls/ImportLogPanel.xaml.cs create mode 100644 LibgenDesktop/Views/Styles/ApplicationUpdateWindowStyles.xaml create mode 100644 LibgenDesktop/Views/Styles/ImportLogPanelStyles.xaml diff --git a/LibgenDesktop.Setup/Constants.cs b/LibgenDesktop.Setup/Constants.cs index 095bce9..c94f144 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.11"; + public const string CURRENT_VERSION = "0.12"; 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/App.xaml.cs b/LibgenDesktop/App.xaml.cs index fb26632..ec1d3e5 100644 --- a/LibgenDesktop/App.xaml.cs +++ b/LibgenDesktop/App.xaml.cs @@ -44,8 +44,8 @@ private void ShowMainWindow(MainModel mainModel) private void ShowCreateDatabaseWindow(MainModel mainModel) { - CreateDatabaseViewModel createDatabaseViewModel = new CreateDatabaseViewModel(mainModel); - IWindowContext windowContext = WindowManager.CreateWindow(RegisteredWindows.WindowKey.CREATE_DATABASE_WINDOW, createDatabaseViewModel); + CreateDatabaseWindowViewModel createDatabaseWindowViewModel = new CreateDatabaseWindowViewModel(mainModel); + IWindowContext windowContext = WindowManager.CreateWindow(RegisteredWindows.WindowKey.CREATE_DATABASE_WINDOW, createDatabaseWindowViewModel); bool? result = windowContext.ShowDialog(); if (result == true) { diff --git a/LibgenDesktop/Common/Constants.cs b/LibgenDesktop/Common/Constants.cs index 06743c5..818fe62 100644 --- a/LibgenDesktop/Common/Constants.cs +++ b/LibgenDesktop/Common/Constants.cs @@ -2,7 +2,8 @@ { internal static class Constants { - public const string CURRENT_VERSION = "0.11"; + public const string CURRENT_VERSION = "0.12"; + public const string CURRENT_GITHUB_RELEASE_NAME = "v0.12 alpha"; public const string CURRENT_DATABASE_VERSION = "0.7"; public const string APP_SETTINGS_FILE_NAME = "libgen.config"; @@ -41,15 +42,16 @@ internal static class Constants public const int ERROR_WINDOW_DEFAULT_HEIGHT = 450; public const int ERROR_WINDOW_MIN_WIDTH = 400; public const int ERROR_WINDOW_MIN_HEIGHT = 300; - public const int IMPORT_WINDOW_MIN_WIDTH = 500; + public const int IMPORT_WINDOW_MIN_WIDTH = 530; public const int IMPORT_WINDOW_MIN_HEIGHT = 400; public const int CREATE_DATABASE_WINDOW_WIDTH = 500; public const int SETTINGS_WINDOW_DEFAULT_WIDTH = 760; public const int SETTINGS_WINDOW_DEFAULT_HEIGHT = 550; public const int SETTINGS_WINDOW_MIN_WIDTH = 760; public const int SETTINGS_WINDOW_MIN_HEIGHT = 550; - public const int SYNCHRONIZATION_WINDOW_MIN_WIDTH = 500; + public const int SYNCHRONIZATION_WINDOW_MIN_WIDTH = 530; public const int SYNCHRONIZATION_WINDOW_MIN_HEIGHT = 400; + public const int APPLICATION_UPDATE_WINDOW_WIDTH = 700; public const int MESSAGE_BOX_WINDOW_WIDTH = 500; public const string DEFAULT_DATABASE_FILE_NAME = "libgen.db"; @@ -86,9 +88,12 @@ internal static class Constants public const int DEFAULT_MAXIMUM_SEARCH_RESULT_COUNT = 50000; public const double SEARCH_PROGRESS_REPORT_INTERVAL = 0.1; + 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 string GITHUB_RELEASE_API_URL = "https://api.github.com/repos/libgenapps/LibgenDesktop/releases"; public const string USER_AGENT = "LibgenDesktop/" + CURRENT_VERSION; public const int MIN_PROXY_PORT = 1; public const int MAX_PROXY_PORT = 65535; diff --git a/LibgenDesktop/Common/Environment.cs b/LibgenDesktop/Common/Environment.cs index ceebc48..73c69f9 100644 --- a/LibgenDesktop/Common/Environment.cs +++ b/LibgenDesktop/Common/Environment.cs @@ -58,7 +58,7 @@ private static string GetOsVersion() ManagementObject osInfo = new ManagementObjectSearcher("SELECT * FROM Win32_OperatingSystem").Get().OfType().FirstOrDefault(); if (osInfo != null) { - return $"{osInfo.Properties["Caption"].Value.ToString()}{osInfo.Properties["Version"].Value.ToString()} {osInfo.Properties["OSArchitecture"].Value.ToString()}"; + return $"{osInfo.Properties["Caption"].Value.ToString()} {osInfo.Properties["Version"].Value.ToString()} {osInfo.Properties["OSArchitecture"].Value.ToString()}"; } else { diff --git a/LibgenDesktop/Common/Logger.cs b/LibgenDesktop/Common/Logger.cs index c737a81..f186e2a 100644 --- a/LibgenDesktop/Common/Logger.cs +++ b/LibgenDesktop/Common/Logger.cs @@ -64,6 +64,7 @@ private static void CreateLogger() private static void LogEnvironmentInformation() { logger.Debug("Libgen Desktop " + CURRENT_VERSION); + logger.Debug("Release: " + CURRENT_GITHUB_RELEASE_NAME); logger.Debug("OS: " + Environment.OsVersion); logger.Debug(".NET Framework: " + Environment.NetFrameworkVersion); logger.Debug("Is in 64-bit process: " + Environment.IsIn64BitProcess); diff --git a/LibgenDesktop/Infrastructure/RegisteredWindows.cs b/LibgenDesktop/Infrastructure/RegisteredWindows.cs index 0b24892..c09a763 100644 --- a/LibgenDesktop/Infrastructure/RegisteredWindows.cs +++ b/LibgenDesktop/Infrastructure/RegisteredWindows.cs @@ -17,7 +17,8 @@ internal enum WindowKey IMPORT_WINDOW, CREATE_DATABASE_WINDOW, SETTINGS_WINDOW, - SYNCHRONIZATION_WINDOW + SYNCHRONIZATION_WINDOW, + APPLICATION_UPDATE_WINDOW } internal class RegisteredWindow @@ -43,9 +44,10 @@ static RegisteredWindows() RegisterWindow(WindowKey.SCI_MAG_DETAILS_WINDOW, typeof(SciMagDetailsWindow), typeof(SciMagDetailsWindowViewModel)); RegisterWindow(WindowKey.ERROR_WINDOW, typeof(ErrorWindow), typeof(ErrorWindowViewModel)); RegisterWindow(WindowKey.IMPORT_WINDOW, typeof(ImportWindow), typeof(ImportWindowViewModel)); - RegisterWindow(WindowKey.CREATE_DATABASE_WINDOW, typeof(CreateDatabaseWindow), typeof(CreateDatabaseViewModel)); + RegisterWindow(WindowKey.CREATE_DATABASE_WINDOW, typeof(CreateDatabaseWindow), typeof(CreateDatabaseWindowViewModel)); 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)); } public static Dictionary AllWindows { get; } diff --git a/LibgenDesktop/LibgenDesktop.csproj b/LibgenDesktop/LibgenDesktop.csproj index 64bde94..e04a69b 100644 --- a/LibgenDesktop/LibgenDesktop.csproj +++ b/LibgenDesktop/LibgenDesktop.csproj @@ -143,13 +143,16 @@ + + + - + @@ -161,11 +164,14 @@ + + + + - @@ -175,7 +181,7 @@ - + @@ -184,6 +190,9 @@ + + ApplicationUpdateWindow.xaml + AddTabButton.xaml @@ -193,6 +202,9 @@ ExportPanel.xaml + + ImportLogPanel.xaml + MessageBoxWindow.xaml @@ -267,6 +279,10 @@ + + Designer + MSBuild:Compile + MSBuild:Compile Designer @@ -279,14 +295,26 @@ Designer MSBuild:Compile + + Designer + MSBuild:Compile + MSBuild:Compile Designer + + Designer + MSBuild:Compile + Designer MSBuild:Compile + + Designer + MSBuild:Compile + MSBuild:Compile Designer diff --git a/LibgenDesktop/Models/Database/LocalDatabase.cs b/LibgenDesktop/Models/Database/LocalDatabase.cs index cbc3b63..94332cb 100644 --- a/LibgenDesktop/Models/Database/LocalDatabase.cs +++ b/LibgenDesktop/Models/Database/LocalDatabase.cs @@ -1,4 +1,5 @@ using System; +using System.Collections; using System.Collections.Generic; using System.Data; using System.Data.SQLite; @@ -57,7 +58,7 @@ public void CreateMetadataTable() public bool CheckIfMetadataExists() { - return ExecuteScalarCommand(SqlScripts.CHECK_IF_METADATA_TABLE_EXIST) == 1; + return ExecuteIntScalarCommand(SqlScripts.CHECK_IF_METADATA_TABLE_EXIST) == 1; } public DatabaseMetadata GetMetadata() @@ -96,6 +97,36 @@ public void AddMetadata(DatabaseMetadata databaseMetadata) } } + public void UpdateMetadata(DatabaseMetadata newMetadata) + { + DatabaseMetadata oldMetadata = GetMetadata(); + foreach (DatabaseMetadata.FieldDefinition fieldDefinition in DatabaseMetadata.FieldDefinitions.Values) + { + if (fieldDefinition.Getter(oldMetadata) != fieldDefinition.Getter(newMetadata)) + { + SetMetadataValue(newMetadata, fieldDefinition); + } + } + } + + public void SetMetadataValue(DatabaseMetadata databaseMetadata, DatabaseMetadata.FieldDefinition field) + { + bool metadataItemExist; + using (SQLiteCommand command = connection.CreateCommand()) + { + command.CommandText = SqlScripts.CHECK_IF_METADATA_ITEM_EXIST; + command.Parameters.AddWithValue("Key", field.FieldName); + metadataItemExist = ParseIntScalarResult(command.ExecuteScalar()) == 1; + } + using (SQLiteCommand command = connection.CreateCommand()) + { + command.CommandText = metadataItemExist ? SqlScripts.UPDATE_METADATA_ITEM : SqlScripts.INSERT_METADATA_ITEM; + command.Parameters.AddWithValue("Key", field.FieldName); + command.Parameters.AddWithValue("Value", field.Getter(databaseMetadata)); + command.ExecuteNonQuery(); + } + } + public void CreateNonFictionTables() { ExecuteCommands(SqlScripts.CREATE_NON_FICTION_TABLE); @@ -112,9 +143,14 @@ public void CreateNonFictionLibgenIdIndex() ExecuteCommands(SqlScripts.CREATE_NON_FICTION_LIBGENID_INDEX); } + public BitArray GetNonFictionLibgenIdsBitArray() + { + return GetLibgenIdsBitArray(SqlScripts.GET_ALL_NON_FICTION_LIBGEN_IDS, GetNonFictionMaxLibgenId()); + } + public int CountNonFictionBooks() { - return ExecuteScalarCommand(SqlScripts.COUNT_NON_FICTION); + return ExecuteIntScalarCommand(SqlScripts.COUNT_NON_FICTION); } public NonFictionBook GetNonFictionBookById(int id) @@ -151,6 +187,11 @@ public NonFictionBook GetLastModifiedNonFictionBook() } } + public int GetNonFictionMaxLibgenId() + { + return ExecuteIntScalarCommand(SqlScripts.GET_NON_FICTION_MAX_LIBGEN_ID); + } + public IEnumerable SearchNonFictionBooks(string searchQuery, int? resultLimit) { searchQuery = EscapeSearchQuery(searchQuery); @@ -454,9 +495,14 @@ public void CreateFictionLibgenIdIndex() ExecuteCommands(SqlScripts.CREATE_FICTION_LIBGENID_INDEX); } + public BitArray GetFictionLibgenIdsBitArray() + { + return GetLibgenIdsBitArray(SqlScripts.GET_ALL_FICTION_LIBGEN_IDS, GetFictionMaxLibgenId()); + } + public int CountFictionBooks() { - return ExecuteScalarCommand(SqlScripts.COUNT_FICTION); + return ExecuteIntScalarCommand(SqlScripts.COUNT_FICTION); } public FictionBook GetFictionBookById(int id) @@ -493,6 +539,11 @@ public FictionBook GetLastModifiedFictionBook() } } + public int GetFictionMaxLibgenId() + { + return ExecuteIntScalarCommand(SqlScripts.GET_FICTION_MAX_LIBGEN_ID); + } + public IEnumerable SearchFictionBooks(string searchQuery, int? resultLimit) { searchQuery = EscapeSearchQuery(searchQuery); @@ -925,9 +976,14 @@ public void CreateSciMagAddedDateTimeIndex() ExecuteCommands(SqlScripts.CREATE_SCIMAG_ADDEDDATETIME_INDEX); } + public BitArray GetSciMagLibgenIdsBitArray() + { + return GetLibgenIdsBitArray(SqlScripts.GET_ALL_SCIMAG_LIBGEN_IDS, GetSciMagMaxLibgenId()); + } + public int CountSciMagArticles() { - return ExecuteScalarCommand(SqlScripts.COUNT_SCIMAG); + return ExecuteIntScalarCommand(SqlScripts.COUNT_SCIMAG); } public SciMagArticle GetSciMagArticleById(int id) @@ -964,6 +1020,11 @@ public SciMagArticle GetLastAddedSciMagArticle() } } + public int GetSciMagMaxLibgenId() + { + return ExecuteIntScalarCommand(SqlScripts.GET_SCIMAG_MAX_LIBGEN_ID); + } + public IEnumerable SearchSciMagArticles(string searchQuery, int? resultLimit) { searchQuery = EscapeSearchQuery(searchQuery); @@ -1258,16 +1319,35 @@ private void ExecuteCommands(params string[] commands) } } - private int ExecuteScalarCommand(string commandText) + private int ExecuteIntScalarCommand(string commandText) + { + return ParseIntScalarResult(ExecuteScalarCommand(commandText)); + } + + private string ExecuteStringScalarCommand(string commandText) + { + return ParseStringScalarResult(ExecuteScalarCommand(commandText)); + } + + private object ExecuteScalarCommand(string commandText) { using (SQLiteCommand command = connection.CreateCommand()) { command.CommandText = commandText; - object objectResult = command.ExecuteScalar(); - return objectResult != DBNull.Value ? (int)(long)objectResult : 0; + return command.ExecuteScalar(); } } + private int ParseIntScalarResult(object objectResult) + { + return objectResult != DBNull.Value ? (int)(long)objectResult : 0; + } + + private string ParseStringScalarResult(object objectResult) + { + return objectResult != DBNull.Value ? objectResult.ToString() : null; + } + [MethodImpl(MethodImplOptions.AggressiveInlining)] private DateTime ParseDbDate(string input) { @@ -1324,5 +1404,22 @@ private List GetIndexList(string getIndexListQuery) } } } + + private BitArray GetLibgenIdsBitArray(string query, int maxLibgenId) + { + BitArray result = new BitArray(maxLibgenId + 1); + using (SQLiteCommand command = connection.CreateCommand()) + { + command.CommandText = query; + using (SQLiteDataReader dataReader = command.ExecuteReader()) + { + while (dataReader.Read()) + { + result[dataReader.GetInt32(0)] = true; + } + } + } + return result; + } } } diff --git a/LibgenDesktop/Models/Database/SqlScripts.cs b/LibgenDesktop/Models/Database/SqlScripts.cs index 56b71df..6563fe6 100644 --- a/LibgenDesktop/Models/Database/SqlScripts.cs +++ b/LibgenDesktop/Models/Database/SqlScripts.cs @@ -12,6 +12,10 @@ internal static class SqlScripts public const string GET_ALL_METADATA_ITEMS = "SELECT Key,Value FROM metadata"; + public const string CHECK_IF_METADATA_ITEM_EXIST = "SELECT COUNT(*) FROM metadata WHERE Key = @Key"; + + public const string GET_METADATA_ITEM = "SELECT Value FROM metadata WHERE Key = @Key"; + public const string INSERT_METADATA_ITEM = "INSERT INTO metadata VALUES (@Key,@Value)"; public const string UPDATE_METADATA_ITEM = "UPDATE metadata SET Value = @Value WHERE Key = @Key"; @@ -72,6 +76,8 @@ internal static class SqlScripts "CREATE VIRTUAL TABLE IF NOT EXISTS non_fiction_fts USING fts5 (Title, Series, Authors, Publisher, IdentifierPlain, " + "content=non_fiction, content_rowid=Id)"; + public const string GET_ALL_NON_FICTION_LIBGEN_IDS = "SELECT LibgenId FROM non_fiction"; + public const string COUNT_NON_FICTION = "SELECT MAX(Id) FROM non_fiction LIMIT 1"; public const string GET_NON_FICTION_BY_ID = "SELECT * FROM non_fiction WHERE Id = @Id"; @@ -81,6 +87,8 @@ internal static class SqlScripts public const string GET_LAST_MODIFIED_NON_FICTION = "SELECT * FROM non_fiction WHERE LastModifiedDateTime = (SELECT MAX(LastModifiedDateTime) FROM non_fiction) ORDER BY LibgenId DESC LIMIT 1"; + public const string GET_NON_FICTION_MAX_LIBGEN_ID = "SELECT MAX(LibgenId) FROM non_fiction LIMIT 1"; + public const string SEARCH_NON_FICTION = "SELECT * FROM non_fiction " + "WHERE Id IN (SELECT rowid FROM non_fiction_fts WHERE non_fiction_fts MATCH @SearchQuery) ORDER BY Id"; @@ -219,6 +227,8 @@ internal static class SqlScripts "AuthorFamily4, AuthorName4, AuthorSurname4, Pseudonim4, RussianAuthorFamily, RussianAuthorName, RussianAuthorSurname, " + "Series1, Series2, Series3, Series4, Publisher, Identifier, content=fiction, content_rowid=Id)"; + public const string GET_ALL_FICTION_LIBGEN_IDS = "SELECT LibgenId FROM fiction"; + public const string COUNT_FICTION = "SELECT MAX(Id) FROM fiction LIMIT 1"; public const string GET_FICTION_BY_ID = "SELECT * FROM fiction WHERE Id = @Id"; @@ -228,6 +238,8 @@ internal static class SqlScripts public const string GET_LAST_MODIFIED_FICTION = "SELECT * FROM fiction WHERE LastModifiedDateTime = (SELECT MAX(LastModifiedDateTime) FROM fiction) ORDER BY LibgenId DESC LIMIT 1"; + public const string GET_FICTION_MAX_LIBGEN_ID = "SELECT MAX(LibgenId) FROM fiction LIMIT 1"; + public const string SEARCH_FICTION = "SELECT * FROM fiction WHERE Id IN (SELECT rowid FROM fiction_fts WHERE fiction_fts MATCH @SearchQuery) ORDER BY Id"; public const string INSERT_FICTION = @@ -361,6 +373,8 @@ internal static class SqlScripts public const string CREATE_SCIMAG_FTS_TABLE = "CREATE VIRTUAL TABLE IF NOT EXISTS scimag_fts USING fts5 "+ "(Title, Authors, Doi, Doi2, PubmedId, Journal, Issnp, Issne, content=scimag, content_rowid=Id)"; + public const string GET_ALL_SCIMAG_LIBGEN_IDS = "SELECT LibgenId FROM scimag"; + public const string COUNT_SCIMAG = "SELECT MAX(Id) FROM scimag LIMIT 1"; public const string GET_SCIMAG_BY_ID = "SELECT * FROM scimag WHERE Id = @Id"; @@ -370,6 +384,8 @@ internal static class SqlScripts public const string GET_LAST_ADDED_SCIMAG = "SELECT * FROM scimag WHERE AddedDateTime = (SELECT MAX(AddedDateTime) FROM scimag) ORDER BY LibgenId DESC LIMIT 1"; + public const string GET_SCIMAG_MAX_LIBGEN_ID = "SELECT MAX(LibgenId) FROM scimag LIMIT 1"; + public const string SEARCH_SCIMAG = "SELECT * FROM scimag WHERE Id IN (SELECT rowid FROM scimag_fts WHERE scimag_fts MATCH @SearchQuery) ORDER BY Id"; public const string INSERT_SCIMAG = diff --git a/LibgenDesktop/Models/Entities/DatabaseMetadata.cs b/LibgenDesktop/Models/Entities/DatabaseMetadata.cs index 58b66ec..b8c4e7a 100644 --- a/LibgenDesktop/Models/Entities/DatabaseMetadata.cs +++ b/LibgenDesktop/Models/Entities/DatabaseMetadata.cs @@ -23,11 +23,28 @@ static DatabaseMetadata() { FieldDefinitions = new Dictionary(); AddField("Version", metadata => metadata.Version, (metadata, value) => metadata.Version = value); + AddField("NonFictionFirstImportComplete", metadata => metadata.NonFictionFirstImportComplete.ToString(), + (metadata, value) => metadata.NonFictionFirstImportComplete = value == Boolean.TrueString); + AddField("FictionFirstImportComplete", metadata => metadata.FictionFirstImportComplete.ToString(), + (metadata, value) => metadata.FictionFirstImportComplete = value == Boolean.TrueString); + AddField("SciMagFirstImportComplete", metadata => metadata.SciMagFirstImportComplete.ToString(), + (metadata, value) => metadata.SciMagFirstImportComplete = value == Boolean.TrueString); + } + + public DatabaseMetadata() + { + Version = null; + NonFictionFirstImportComplete = null; + FictionFirstImportComplete = null; + SciMagFirstImportComplete = null; } public static Dictionary FieldDefinitions { get; } public string Version { get; set; } + public bool? NonFictionFirstImportComplete { get; set; } + public bool? FictionFirstImportComplete { get; set; } + public bool? SciMagFirstImportComplete { get; set; } private static void AddField(string fieldName, Func getter, Action setter) { diff --git a/LibgenDesktop/Models/Import/FictionImporter.cs b/LibgenDesktop/Models/Import/FictionImporter.cs index f70e1cb..0296274 100644 --- a/LibgenDesktop/Models/Import/FictionImporter.cs +++ b/LibgenDesktop/Models/Import/FictionImporter.cs @@ -1,4 +1,5 @@ using System; +using System.Collections; using System.Collections.Generic; using LibgenDesktop.Models.Database; using LibgenDesktop.Models.Entities; @@ -11,10 +12,10 @@ internal class FictionImporter : Importer private readonly DateTime lastModifiedDateTime; private readonly int lastModifiedLibgenId; - public FictionImporter(LocalDatabase localDatabase, bool isUpdateMode) - : base(localDatabase, isUpdateMode, TableDefinitions.Fiction) + public FictionImporter(LocalDatabase localDatabase, BitArray existingLibgenIds) + : base(localDatabase, existingLibgenIds, TableDefinitions.Fiction) { - if (isUpdateMode) + if (IsUpdateMode) { FictionBook lastModifiedFictionBook = LocalDatabase.GetLastModifiedFictionBook(); lastModifiedDateTime = lastModifiedFictionBook.LastModifiedDateTime; diff --git a/LibgenDesktop/Models/Import/Importer.cs b/LibgenDesktop/Models/Import/Importer.cs index b948904..4c1382b 100644 --- a/LibgenDesktop/Models/Import/Importer.cs +++ b/LibgenDesktop/Models/Import/Importer.cs @@ -1,10 +1,10 @@ using System; +using System.Collections; using System.Collections.Generic; using System.Linq; using System.Threading; using LibgenDesktop.Models.Database; using LibgenDesktop.Models.Entities; -using LibgenDesktop.Models.ProgressArgs; using LibgenDesktop.Models.SqlDump; using static LibgenDesktop.Common.Constants; @@ -12,53 +12,68 @@ namespace LibgenDesktop.Models.Import { internal abstract class Importer { - public abstract void Import(SqlDumpReader sqlDumpReader, IProgress progressHandler, CancellationToken cancellationToken, - SqlDumpReader.ParsedTableDefinition parsedTableDefinition); + internal delegate void ImportProgressReporter(int objectsAdded, int objectsUpdated); + + internal class ImportResult + { + public ImportResult(int addedObjectCount, int updatedObjectCount) + { + AddedObjectCount = addedObjectCount; + UpdatedObjectCount = updatedObjectCount; + } + + public int AddedObjectCount { get; } + public int UpdatedObjectCount { get; } + } + + public abstract ImportResult Import(SqlDumpReader sqlDumpReader, ImportProgressReporter progressReporter, double progressUpdateInterval, + CancellationToken cancellationToken, SqlDumpReader.ParsedTableDefinition parsedTableDefinition); } internal abstract class Importer : Importer where T : LibgenObject, new() { - private const double IMPORT_PROGRESS_UPDATE_INTERVAL = 0.5; - - private readonly bool isUpdateMode; + private readonly BitArray existingLibgenIds; private readonly TableDefinition tableDefinition; private readonly List currentBatchObjectsToInsert; private readonly List currentBatchObjectsToUpdate; - protected Importer(LocalDatabase localDatabase, bool isUpdateMode, TableDefinition tableDefinition) + protected Importer(LocalDatabase localDatabase, BitArray existingLibgenIds, TableDefinition tableDefinition) { LocalDatabase = localDatabase; - this.isUpdateMode = isUpdateMode; + this.existingLibgenIds = existingLibgenIds; + IsUpdateMode = existingLibgenIds != null && existingLibgenIds.Length > 0; this.tableDefinition = tableDefinition; currentBatchObjectsToInsert = new List(DATABASE_TRANSACTION_BATCH); currentBatchObjectsToUpdate = new List(DATABASE_TRANSACTION_BATCH); } protected LocalDatabase LocalDatabase { get; } + protected bool IsUpdateMode { get; } - public override void Import(SqlDumpReader sqlDumpReader, IProgress progressHandler, CancellationToken cancellationToken, - SqlDumpReader.ParsedTableDefinition parsedTableDefinition) + public override ImportResult Import(SqlDumpReader sqlDumpReader, ImportProgressReporter progressReporter, double progressUpdateInterval, + CancellationToken cancellationToken, SqlDumpReader.ParsedTableDefinition parsedTableDefinition) { List> sortedColumnSetters = tableDefinition.GetSortedColumnSetters(parsedTableDefinition.Columns.Select(column => column.ColumnName)); - Import(sqlDumpReader.ParseImportObjects(sortedColumnSetters), progressHandler, cancellationToken); + return Import(sqlDumpReader.ParseImportObjects(sortedColumnSetters), progressReporter, progressUpdateInterval, cancellationToken); } - public void Import(IEnumerable importingObjects, IProgress progressHandler, CancellationToken cancellationToken) + public ImportResult Import(IEnumerable importingObjects, ImportProgressReporter progressReporter, double progressUpdateInterval, + CancellationToken cancellationToken) { DateTime lastProgressUpdateDateTime = DateTime.Now; int addedObjectCount = 0; int updatedObjectCount = 0; currentBatchObjectsToInsert.Clear(); currentBatchObjectsToUpdate.Clear(); - ReportProgress(progressHandler, addedObjectCount, updatedObjectCount); + progressReporter(addedObjectCount, updatedObjectCount); foreach (T importingObject in importingObjects) { if (cancellationToken.IsCancellationRequested) { - return; + break; } - if (!isUpdateMode) + if (!IsUpdateMode || existingLibgenIds.Length <= importingObject.LibgenId || !existingLibgenIds[importingObject.LibgenId]) { currentBatchObjectsToInsert.Add(importingObject); } @@ -77,26 +92,30 @@ public void Import(IEnumerable importingObjects, IProgress progressHa } if (currentBatchObjectsToInsert.Count + currentBatchObjectsToUpdate.Count == DATABASE_TRANSACTION_BATCH) { - InsertBatch(currentBatchObjectsToInsert); - addedObjectCount += currentBatchObjectsToInsert.Count; - if (cancellationToken.IsCancellationRequested) + if (currentBatchObjectsToInsert.Any()) { - ReportProgress(progressHandler, addedObjectCount, updatedObjectCount); - return; + InsertBatch(currentBatchObjectsToInsert); + addedObjectCount += currentBatchObjectsToInsert.Count; + currentBatchObjectsToInsert.Clear(); + if (cancellationToken.IsCancellationRequested) + { + break; + } } - currentBatchObjectsToInsert.Clear(); - UpdateBatch(currentBatchObjectsToUpdate); - updatedObjectCount += currentBatchObjectsToUpdate.Count; - if (cancellationToken.IsCancellationRequested) + if (currentBatchObjectsToUpdate.Any()) { - ReportProgress(progressHandler, addedObjectCount, updatedObjectCount); - return; + UpdateBatch(currentBatchObjectsToUpdate); + updatedObjectCount += currentBatchObjectsToUpdate.Count; + currentBatchObjectsToUpdate.Clear(); + if (cancellationToken.IsCancellationRequested) + { + break; + } } - currentBatchObjectsToUpdate.Clear(); DateTime now = DateTime.Now; - if ((now - lastProgressUpdateDateTime).TotalSeconds > IMPORT_PROGRESS_UPDATE_INTERVAL) + if ((now - lastProgressUpdateDateTime).TotalSeconds > progressUpdateInterval) { - ReportProgress(progressHandler, addedObjectCount, updatedObjectCount); + progressReporter(addedObjectCount, updatedObjectCount); lastProgressUpdateDateTime = now; } } @@ -111,17 +130,13 @@ public void Import(IEnumerable importingObjects, IProgress progressHa UpdateBatch(currentBatchObjectsToUpdate); updatedObjectCount += currentBatchObjectsToUpdate.Count; } - ReportProgress(progressHandler, addedObjectCount, updatedObjectCount); + progressReporter(addedObjectCount, updatedObjectCount); + return new ImportResult(addedObjectCount, updatedObjectCount); } protected abstract void InsertBatch(List objectBatch); protected abstract void UpdateBatch(List objectBatch); protected abstract bool IsNewObject(T importingObject); protected abstract int? GetExistingObjectIdByLibgenId(int libgenId); - - private void ReportProgress(IProgress progressHandler, int objectsAdded, int objectsUpdated) - { - progressHandler.Report(new ImportObjectsProgress(objectsAdded, objectsUpdated)); - } } } diff --git a/LibgenDesktop/Models/Import/NonFictionImporter.cs b/LibgenDesktop/Models/Import/NonFictionImporter.cs index 6cbb09d..051d386 100644 --- a/LibgenDesktop/Models/Import/NonFictionImporter.cs +++ b/LibgenDesktop/Models/Import/NonFictionImporter.cs @@ -1,4 +1,5 @@ using System; +using System.Collections; using System.Collections.Generic; using LibgenDesktop.Models.Database; using LibgenDesktop.Models.Entities; @@ -11,10 +12,10 @@ internal class NonFictionImporter : Importer private readonly DateTime lastModifiedDateTime; private readonly int lastModifiedLibgenId; - public NonFictionImporter(LocalDatabase localDatabase, bool isUpdateMode) - : base(localDatabase, isUpdateMode, TableDefinitions.NonFiction) + public NonFictionImporter(LocalDatabase localDatabase, BitArray existingLibgenIds) + : base(localDatabase, existingLibgenIds, TableDefinitions.NonFiction) { - if (isUpdateMode) + if (IsUpdateMode) { NonFictionBook lastModifiedNonFictionBook = LocalDatabase.GetLastModifiedNonFictionBook(); lastModifiedDateTime = lastModifiedNonFictionBook.LastModifiedDateTime; @@ -22,8 +23,8 @@ public NonFictionImporter(LocalDatabase localDatabase, bool isUpdateMode) } } - public NonFictionImporter(LocalDatabase localDatabase, NonFictionBook lastModifiedNonFictionBook) - : base(localDatabase, true, TableDefinitions.NonFiction) + public NonFictionImporter(LocalDatabase localDatabase, BitArray existingLibgenIds, NonFictionBook lastModifiedNonFictionBook) + : base(localDatabase, existingLibgenIds, TableDefinitions.NonFiction) { lastModifiedDateTime = lastModifiedNonFictionBook.LastModifiedDateTime; lastModifiedLibgenId = lastModifiedNonFictionBook.LibgenId; diff --git a/LibgenDesktop/Models/Import/SciMagImporter.cs b/LibgenDesktop/Models/Import/SciMagImporter.cs index 4aa3d7b..37214b4 100644 --- a/LibgenDesktop/Models/Import/SciMagImporter.cs +++ b/LibgenDesktop/Models/Import/SciMagImporter.cs @@ -1,4 +1,5 @@ using System; +using System.Collections; using System.Collections.Generic; using LibgenDesktop.Models.Database; using LibgenDesktop.Models.Entities; @@ -11,10 +12,10 @@ internal class SciMagImporter : Importer private readonly DateTime lastAddedDateTime; private readonly int lastModifiedLibgenId; - public SciMagImporter(LocalDatabase localDatabase, bool isUpdateMode) - : base(localDatabase, isUpdateMode, TableDefinitions.SciMag) + public SciMagImporter(LocalDatabase localDatabase, BitArray existingLibgenIds) + : base(localDatabase, existingLibgenIds, TableDefinitions.SciMag) { - if (isUpdateMode) + if (IsUpdateMode) { SciMagArticle lastAddedSciMagArticle = LocalDatabase.GetLastAddedSciMagArticle(); lastAddedDateTime = lastAddedSciMagArticle.AddedDateTime ?? DateTime.MinValue; diff --git a/LibgenDesktop/Models/JsonApi/JsonApiClient.cs b/LibgenDesktop/Models/JsonApi/JsonApiClient.cs index d30cc2a..3047609 100644 --- a/LibgenDesktop/Models/JsonApi/JsonApiClient.cs +++ b/LibgenDesktop/Models/JsonApi/JsonApiClient.cs @@ -9,7 +9,6 @@ using LibgenDesktop.Common; using LibgenDesktop.Models.Entities; using Newtonsoft.Json; -using static LibgenDesktop.Common.Constants; namespace LibgenDesktop.Models.JsonApi { @@ -38,10 +37,10 @@ public async Task> DownloadNextBatchAsync(CancellationToken Logger.Debug($"Sending a request to {url}"); HttpResponseMessage response = await httpClient.GetAsync(url, cancellationToken); Logger.Debug($"Response status code: {(int)response.StatusCode} {response.StatusCode}."); - Logger.Debug("Response headers:", response.Headers.ToString().TrimEnd()); + Logger.Debug("Response headers:", response.Headers.ToString().TrimEnd(), response.Content.Headers.ToString().TrimEnd()); if (response.StatusCode != HttpStatusCode.OK) { - throw new Exception($"JSON API returned {response.StatusCode}."); + throw new Exception($"JSON API returned {(int)response.StatusCode} {response.StatusCode}."); } string responseContent = await response.Content.ReadAsStringAsync(); Logger.Debug("Response content:", responseContent); diff --git a/LibgenDesktop/Models/Localization/LocalizationStorage.cs b/LibgenDesktop/Models/Localization/LocalizationStorage.cs new file mode 100644 index 0000000..5f27c56 --- /dev/null +++ b/LibgenDesktop/Models/Localization/LocalizationStorage.cs @@ -0,0 +1,15 @@ +using System.Collections.Generic; + +namespace LibgenDesktop.Models.Localization +{ + internal static class LocalizationStorage + { + public static Dictionary LoadLanguages() + { + return new Dictionary + { + { "Russian", "Russian (русский)" } + }; + } + } +} diff --git a/LibgenDesktop/Models/MainModel.cs b/LibgenDesktop/Models/MainModel.cs index 64b24c9..bf451a9 100644 --- a/LibgenDesktop/Models/MainModel.cs +++ b/LibgenDesktop/Models/MainModel.cs @@ -1,4 +1,5 @@ using System; +using System.Collections; using System.Collections.Generic; using System.Collections.ObjectModel; using System.IO; @@ -13,9 +14,11 @@ using LibgenDesktop.Models.Export; using LibgenDesktop.Models.Import; using LibgenDesktop.Models.JsonApi; +using LibgenDesktop.Models.Localization; using LibgenDesktop.Models.ProgressArgs; using LibgenDesktop.Models.Settings; using LibgenDesktop.Models.SqlDump; +using LibgenDesktop.Models.Update; using static LibgenDesktop.Common.Constants; using Environment = LibgenDesktop.Common.Environment; @@ -44,6 +47,14 @@ internal enum SynchronizationResult CANCELLED } + internal enum DownloadFileResult + { + COMPLETED = 1, + INCOMPLETE, + CANCELLED + } + + private Updater updater; private LocalDatabase localDatabase; public MainModel() @@ -55,18 +66,28 @@ public MainModel() } Mirrors = MirrorStorage.LoadMirrors(Environment.MirrorsFilePath); ValidateAndCorrectSelectedMirrors(); + Languages = LocalizationStorage.LoadLanguages(); + AppSettings.General.Language = Languages.First().Key; CreateNewHttpClient(); OpenDatabase(AppSettings.DatabaseFileName); + LastApplicationUpdateCheckResult = null; + updater = new Updater(); + updater.UpdateCheck += ApplicationUpdateCheck; + ConfigureUpdater(); } 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 HttpClient HttpClient { get; private set; } public int NonFictionBookCount { get; private set; } public int FictionBookCount { get; private set; } public int SciMagArticleCount { get; private set; } + public Updater.UpdateCheckResult LastApplicationUpdateCheckResult { get; set; } + + public event EventHandler ApplicationUpdateCheckCompleted; public Task> SearchNonFictionAsync(string searchQuery, IProgress progressHandler, CancellationToken cancellationToken) @@ -215,6 +236,7 @@ public Task ImportSqlDumpAsync(string sqlDumpFilePath, IPro } Logger.Debug($"Table type is {tableType}."); Importer importer; + BitArray existingLibgenIds = null; switch (tableType) { case TableType.NON_FICTION: @@ -225,8 +247,10 @@ public Task ImportSqlDumpAsync(string sqlDumpFilePath, IPro { return ImportSqlDumpResult.CANCELLED; } + progressHandler.Report(new ImportLoadLibgenIdsProgress()); + existingLibgenIds = localDatabase.GetNonFictionLibgenIdsBitArray(); } - importer = new NonFictionImporter(localDatabase, isUpdateMode: NonFictionBookCount != 0); + importer = new NonFictionImporter(localDatabase, existingLibgenIds); break; case TableType.FICTION: if (FictionBookCount != 0) @@ -236,8 +260,10 @@ public Task ImportSqlDumpAsync(string sqlDumpFilePath, IPro { return ImportSqlDumpResult.CANCELLED; } + progressHandler.Report(new ImportLoadLibgenIdsProgress()); + existingLibgenIds = localDatabase.GetFictionLibgenIdsBitArray(); } - importer = new FictionImporter(localDatabase, isUpdateMode: FictionBookCount != 0); + importer = new FictionImporter(localDatabase, existingLibgenIds); break; case TableType.SCI_MAG: if (SciMagArticleCount != 0) @@ -247,8 +273,10 @@ public Task ImportSqlDumpAsync(string sqlDumpFilePath, IPro { return ImportSqlDumpResult.CANCELLED; } + progressHandler.Report(new ImportLoadLibgenIdsProgress()); + existingLibgenIds = localDatabase.GetSciMagLibgenIdsBitArray(); } - importer = new SciMagImporter(localDatabase, isUpdateMode: SciMagArticleCount != 0); + importer = new SciMagImporter(localDatabase, existingLibgenIds); break; default: throw new Exception($"Unknown table type: {tableType}."); @@ -258,8 +286,12 @@ public Task ImportSqlDumpAsync(string sqlDumpFilePath, IPro Logger.Debug("SQL dump import has been cancelled."); return ImportSqlDumpResult.CANCELLED; } + Importer.ImportProgressReporter importProgressReporter = (int objectsAdded, int objectsUpdated) => + { + progressHandler.Report(new ImportObjectsProgress(objectsAdded, objectsUpdated)); + }; Logger.Debug("Importing data."); - importer.Import(sqlDumpReader, progressHandler, cancellationToken, parsedTableDefinition); + importer.Import(sqlDumpReader, importProgressReporter, IMPORT_PROGRESS_UPDATE_INTERVAL, cancellationToken, parsedTableDefinition); switch (tableType) { case TableType.NON_FICTION: @@ -277,6 +309,19 @@ public Task ImportSqlDumpAsync(string sqlDumpFilePath, IPro Logger.Debug("SQL dump import has been cancelled."); return ImportSqlDumpResult.CANCELLED; } + switch (tableType) + { + case TableType.NON_FICTION: + DatabaseMetadata.NonFictionFirstImportComplete = true; + break; + case TableType.FICTION: + DatabaseMetadata.FictionFirstImportComplete = true; + break; + case TableType.SCI_MAG: + DatabaseMetadata.SciMagFirstImportComplete = true; + break; + } + localDatabase.UpdateMetadata(DatabaseMetadata); Logger.Debug("SQL dump import has been completed successfully."); return ImportSqlDumpResult.COMPLETED; } @@ -284,7 +329,7 @@ public Task ImportSqlDumpAsync(string sqlDumpFilePath, IPro }); } - public Task SynchronizeNonFiction(IProgress progressHandler, CancellationToken cancellationToken) + public Task SynchronizeNonFictionAsync(IProgress progressHandler, CancellationToken cancellationToken) { return Task.Run(async () => { @@ -309,8 +354,18 @@ public Task SynchronizeNonFiction(IProgress progr } JsonApiClient jsonApiClient = new JsonApiClient(HttpClient, jsonApiUrl, lastModifiedNonFictionBook.LastModifiedDateTime, lastModifiedNonFictionBook.LibgenId); - List downloadedBooks = new List(); - progressHandler.Report(new JsonApiDownloadProgress(0)); + progressHandler.Report(new ImportLoadLibgenIdsProgress()); + BitArray existingLibgenIds = localDatabase.GetNonFictionLibgenIdsBitArray(); + NonFictionImporter importer = new NonFictionImporter(localDatabase, existingLibgenIds, lastModifiedNonFictionBook); + progressHandler.Report(new SynchronizationProgress(0, 0, 0)); + int downloadedBookCount = 0; + int totalAddedBookCount = 0; + int totalUpdatedBookCount = 0; + Importer.ImportProgressReporter importProgressReporter = (int objectsAdded, int objectsUpdated) => + { + progressHandler.Report(new SynchronizationProgress(downloadedBookCount, totalAddedBookCount + objectsAdded, + totalUpdatedBookCount + objectsUpdated)); + }; while (true) { List currentBatch; @@ -329,28 +384,95 @@ public Task SynchronizeNonFiction(IProgress progr Logger.Debug("Current batch is empty, download is complete."); break; } - downloadedBooks.AddRange(currentBatch); - Logger.Debug($"{downloadedBooks.Count} books have been downloaded so far."); - progressHandler.Report(new JsonApiDownloadProgress(downloadedBooks.Count)); + downloadedBookCount += currentBatch.Count; + Logger.Debug($"Batch download is complete, {downloadedBookCount} books have been downloaded so far."); + progressHandler.Report(new SynchronizationProgress(downloadedBookCount, totalAddedBookCount, totalUpdatedBookCount)); if (cancellationToken.IsCancellationRequested) { Logger.Debug("Synchronization has been cancelled."); return SynchronizationResult.CANCELLED; } - } - NonFictionImporter importer = new NonFictionImporter(localDatabase, lastModifiedNonFictionBook); - Logger.Debug("Importing data."); - importer.Import(downloadedBooks, progressHandler, cancellationToken); - if (cancellationToken.IsCancellationRequested) - { - Logger.Debug("Synchronization has been cancelled."); - return SynchronizationResult.CANCELLED; + Logger.Debug("Importing downloaded batch."); + Importer.ImportResult importResult = + importer.Import(currentBatch, importProgressReporter, SYNCHRONIZATION_PROGRESS_UPDATE_INTERVAL, cancellationToken); + if (cancellationToken.IsCancellationRequested) + { + Logger.Debug("Synchronization has been cancelled."); + return SynchronizationResult.CANCELLED; + } + totalAddedBookCount += importResult.AddedObjectCount; + totalUpdatedBookCount += importResult.UpdatedObjectCount; + Logger.Debug($"Batch has been imported, total added book count = {totalAddedBookCount}, total updated book count = {totalUpdatedBookCount}."); } Logger.Debug("Synchronization has been completed successfully."); return SynchronizationResult.COMPLETED; }); } + public Task DownloadFileAsync(string fileUrl, string destinationPath, IProgress progressHandler, + CancellationToken cancellationToken) + { + return Task.Run(async () => + { + Logger.Debug($"Requesting {fileUrl}"); + HttpResponseMessage response; + try + { + response = await HttpClient.GetAsync(fileUrl, HttpCompletionOption.ResponseHeadersRead, cancellationToken); + } + catch (TaskCanceledException) + { + Logger.Debug("File download has been cancelled."); + return DownloadFileResult.CANCELLED; + } + Logger.Debug($"Response status code: {(int)response.StatusCode} {response.StatusCode}."); + Logger.Debug("Response headers:", response.Headers.ToString().TrimEnd(), response.Content.Headers.ToString().TrimEnd()); + if (response.StatusCode != HttpStatusCode.OK) + { + throw new Exception($"Server returned {(int)response.StatusCode} {response.StatusCode}."); + } + long? contentLength = response.Content.Headers.ContentLength; + if (!contentLength.HasValue) + { + throw new Exception($"Server did not return Content-Length value."); + } + int fileSize = (int)contentLength.Value; + Logger.Debug($"File size is {fileSize} bytes."); + Stream downloadStream = await response.Content.ReadAsStreamAsync(); + byte[] buffer = new byte[4096]; + int downloadedBytes = 0; + using (FileStream destinationFileStream = new FileStream(destinationPath, FileMode.Create)) + { + while (true) + { + int bytesRead; + try + { + bytesRead = downloadStream.Read(buffer, 0, buffer.Length); + } + catch + { + if (cancellationToken.IsCancellationRequested) + { + Logger.Debug("File download has been cancelled."); + return DownloadFileResult.CANCELLED; + } + throw; + } + if (bytesRead == 0) + { + bool isCompleted = downloadedBytes == fileSize; + Logger.Debug($"File download is {(isCompleted ? "complete" : "incomplete")}."); + return isCompleted ? DownloadFileResult.COMPLETED : DownloadFileResult.INCOMPLETE; + } + destinationFileStream.Write(buffer, 0, bytesRead); + downloadedBytes += bytesRead; + progressHandler.Report(new DownloadFileProgress(downloadedBytes, fileSize)); + } + } + }); + } + public void SaveSettings() { SettingsStorage.SaveSettings(AppSettings, Environment.AppSettingsFilePath); @@ -450,11 +572,14 @@ public bool CreateDatabase(string databaseFilePath) localDatabase.CreateNonFictionTables(); localDatabase.CreateFictionTables(); localDatabase.CreateSciMagTables(); - DatabaseMetadata databaseMetadata = new DatabaseMetadata + DatabaseMetadata = new DatabaseMetadata { - Version = CURRENT_DATABASE_VERSION + Version = CURRENT_DATABASE_VERSION, + NonFictionFirstImportComplete = false, + FictionFirstImportComplete = false, + SciMagFirstImportComplete = false }; - localDatabase.AddMetadata(databaseMetadata); + localDatabase.AddMetadata(DatabaseMetadata); NonFictionBookCount = 0; FictionBookCount = 0; SciMagArticleCount = 0; @@ -502,6 +627,41 @@ public void DisableLogging() Logger.DisableLogging(); } + public void ConfigureUpdater() + { + DateTime? nextUpdateCheck; + if (AppSettings.General.UpdateCheck == AppSettings.GeneralSettings.UpdateCheckInterval.NEVER || AppSettings.Network.OfflineMode) + { + nextUpdateCheck = null; + } + else + { + DateTime? lastUpdateCheck = AppSettings.LastUpdate.LastCheckedAt; + if (!lastUpdateCheck.HasValue) + { + nextUpdateCheck = DateTime.Now; + } + else + { + switch (AppSettings.General.UpdateCheck) + { + case AppSettings.GeneralSettings.UpdateCheckInterval.DAILY: + nextUpdateCheck = lastUpdateCheck.Value.AddDays(1); + break; + case AppSettings.GeneralSettings.UpdateCheckInterval.WEEKLY: + nextUpdateCheck = lastUpdateCheck.Value.AddDays(7); + break; + case AppSettings.GeneralSettings.UpdateCheckInterval.MONTHLY: + nextUpdateCheck = lastUpdateCheck.Value.AddMonths(1); + break; + default: + throw new Exception($"Unexpected update check interval: {AppSettings.General.UpdateCheck}."); + } + } + } + updater.Configure(HttpClient, nextUpdateCheck, AppSettings.LastUpdate.IgnoreReleaseName); + } + private Task> SearchItemsAsync(Func> searchFunction, string searchQuery, IProgress progressHandler, CancellationToken cancellationToken) { @@ -728,5 +888,14 @@ private string ValidateAndCorrectSelectedMirror(string selectedMirrorName, Func< } return null; } + + private void ApplicationUpdateCheck(object sender, Updater.UpdateCheckEventArgs e) + { + LastApplicationUpdateCheckResult = e.Result; + ApplicationUpdateCheckCompleted?.Invoke(this, EventArgs.Empty); + AppSettings.LastUpdate.LastCheckedAt = DateTime.Now; + SaveSettings(); + ConfigureUpdater(); + } } } diff --git a/LibgenDesktop/Models/ProgressArgs/DownloadFileProgress.cs b/LibgenDesktop/Models/ProgressArgs/DownloadFileProgress.cs new file mode 100644 index 0000000..25fa407 --- /dev/null +++ b/LibgenDesktop/Models/ProgressArgs/DownloadFileProgress.cs @@ -0,0 +1,14 @@ +namespace LibgenDesktop.Models.ProgressArgs +{ + internal class DownloadFileProgress + { + public DownloadFileProgress(int downloadedBytes, int fileSize) + { + DownloadedBytes = downloadedBytes; + FileSize = fileSize; + } + + public int DownloadedBytes { get; } + public int FileSize { get; } + } +} diff --git a/LibgenDesktop/Models/ProgressArgs/ImportLoadLibgenIdsProgress.cs b/LibgenDesktop/Models/ProgressArgs/ImportLoadLibgenIdsProgress.cs new file mode 100644 index 0000000..4fbe558 --- /dev/null +++ b/LibgenDesktop/Models/ProgressArgs/ImportLoadLibgenIdsProgress.cs @@ -0,0 +1,6 @@ +namespace LibgenDesktop.Models.ProgressArgs +{ + internal class ImportLoadLibgenIdsProgress + { + } +} diff --git a/LibgenDesktop/Models/ProgressArgs/JsonApiDownloadProgress.cs b/LibgenDesktop/Models/ProgressArgs/JsonApiDownloadProgress.cs deleted file mode 100644 index 6e79730..0000000 --- a/LibgenDesktop/Models/ProgressArgs/JsonApiDownloadProgress.cs +++ /dev/null @@ -1,12 +0,0 @@ -namespace LibgenDesktop.Models.ProgressArgs -{ - internal class JsonApiDownloadProgress - { - public JsonApiDownloadProgress(int booksDownloaded) - { - BooksDownloaded = booksDownloaded; - } - - public int BooksDownloaded { get; } - } -} diff --git a/LibgenDesktop/Models/ProgressArgs/SynchronizationProgress.cs b/LibgenDesktop/Models/ProgressArgs/SynchronizationProgress.cs new file mode 100644 index 0000000..b26fb97 --- /dev/null +++ b/LibgenDesktop/Models/ProgressArgs/SynchronizationProgress.cs @@ -0,0 +1,16 @@ +namespace LibgenDesktop.Models.ProgressArgs +{ + internal class SynchronizationProgress + { + public SynchronizationProgress(int objectsDownloaded, int objectsAdded, int objectsUpdated) + { + ObjectsDownloaded = objectsDownloaded; + ObjectsAdded = objectsAdded; + ObjectsUpdated = objectsUpdated; + } + + public int ObjectsDownloaded { get; } + public int ObjectsAdded { get; } + public int ObjectsUpdated { get; } + } +} diff --git a/LibgenDesktop/Models/Settings/AppSettings.cs b/LibgenDesktop/Models/Settings/AppSettings.cs index ea64c54..5c795c5 100644 --- a/LibgenDesktop/Models/Settings/AppSettings.cs +++ b/LibgenDesktop/Models/Settings/AppSettings.cs @@ -260,6 +260,50 @@ public static SciMagSettings Default public ExportPanelSettngs ExportPanel { get; set; } } + internal class LastUpdateSettings + { + public static LastUpdateSettings Default + { + get + { + return new LastUpdateSettings + { + LastCheckedAt = null, + IgnoreReleaseName = null + }; + } + } + + public DateTime? LastCheckedAt { get; set; } + public string IgnoreReleaseName { get; set; } + } + + internal class GeneralSettings + { + internal enum UpdateCheckInterval + { + NEVER = 1, + DAILY, + WEEKLY, + MONTHLY + } + + public static GeneralSettings Default + { + get + { + return new GeneralSettings + { + Language = null, + UpdateCheck = UpdateCheckInterval.NEVER + }; + } + } + + public string Language { get; set; } + public UpdateCheckInterval UpdateCheck { get; set; } + } + internal class NetworkSettings { public static NetworkSettings Default @@ -386,6 +430,8 @@ public static AppSettings Default NonFiction = NonFictionSettings.Default, Fiction = FictionSettings.Default, SciMag = SciMagSettings.Default, + LastUpdate = LastUpdateSettings.Default, + General = GeneralSettings.Default, Network = NetworkSettings.Default, Mirrors = MirrorSettings.Default, Search = SearchSettings.Default, @@ -400,6 +446,8 @@ public static AppSettings Default public NonFictionSettings NonFiction { get; set; } public FictionSettings Fiction { get; set; } public SciMagSettings SciMag { get; set; } + public LastUpdateSettings LastUpdate { get; set; } + public GeneralSettings General { get; set; } public NetworkSettings Network { get; set; } public MirrorSettings Mirrors { get; set; } public SearchSettings Search { get; set; } @@ -418,10 +466,12 @@ public static AppSettings ValidateAndCorrect(AppSettings appSettings) appSettings.ValidateAndCorrectMainWindowSettings(); appSettings.ValidateAndCorrectNonFictionSettings(); appSettings.ValidateAndCorrectFictionSettings(); + appSettings.ValidateAndCorrectSciMagSettings(); + appSettings.ValidateAndCorrectLastUpdateSettings(); + appSettings.ValidateAndCorrectGeneralSettings(); appSettings.ValidateAndCorrectNetworkSettings(); appSettings.ValidateAndCorrectMirrorSettings(); appSettings.ValidateAndCorrectSearchSettings(); - appSettings.ValidateAndCorrectSciMagSettings(); appSettings.ValidateAndCorrectExportSettings(); appSettings.ValidateAndCorrectAdvancedSettings(); return appSettings; @@ -683,6 +733,29 @@ private ExportPanelSettngs ValidateAndCorrectExportPanelSettings(ExportPanelSett return exportPanelSettngs; } + private void ValidateAndCorrectLastUpdateSettings() + { + if (LastUpdate == null) + { + LastUpdate = LastUpdateSettings.Default; + } + } + + private void ValidateAndCorrectGeneralSettings() + { + if (General == null) + { + General = GeneralSettings.Default; + } + else + { + if (!Enum.IsDefined(typeof(GeneralSettings.UpdateCheckInterval), General.UpdateCheck)) + { + General.UpdateCheck = GeneralSettings.UpdateCheckInterval.NEVER; + } + } + } + private void ValidateAndCorrectNetworkSettings() { if (Network == null) diff --git a/LibgenDesktop/Models/Update/GitHubApiRelease.cs b/LibgenDesktop/Models/Update/GitHubApiRelease.cs new file mode 100644 index 0000000..d0975f1 --- /dev/null +++ b/LibgenDesktop/Models/Update/GitHubApiRelease.cs @@ -0,0 +1,30 @@ +using System; +using System.Collections.Generic; +using Newtonsoft.Json; + +namespace LibgenDesktop.Models.Update +{ + internal class GitHubApiRelease + { + internal class Asset + { + [JsonProperty("name")] + public string Name { get; set; } + + [JsonProperty("size")] + public int Size { get; set; } + + [JsonProperty("browser_download_url")] + public string DownloadUrl { get; set; } + } + + [JsonProperty("name")] + public string Name { get; set; } + + [JsonProperty("published_at")] + public DateTime PublishedAt { get; set; } + + [JsonProperty("assets")] + public List Assets { get; set; } + } +} diff --git a/LibgenDesktop/Models/Update/Updater.cs b/LibgenDesktop/Models/Update/Updater.cs new file mode 100644 index 0000000..c581cc7 --- /dev/null +++ b/LibgenDesktop/Models/Update/Updater.cs @@ -0,0 +1,167 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Net; +using System.Net.Http; +using System.Threading; +using System.Threading.Tasks; +using LibgenDesktop.Common; +using Newtonsoft.Json; +using static LibgenDesktop.Common.Constants; +using Environment = LibgenDesktop.Common.Environment; + +namespace LibgenDesktop.Models.Update +{ + internal class Updater : IDisposable + { + internal class UpdateCheckResult + { + public UpdateCheckResult(string newReleaseName, DateTime publishedAt, string fileName, int fileSize, string downloadUrl) + { + NewReleaseName = newReleaseName; + PublishedAt = publishedAt; + FileName = fileName; + FileSize = fileSize; + DownloadUrl = downloadUrl; + } + + public string NewReleaseName { get; } + public DateTime PublishedAt { get; } + public string FileName { get; } + public int FileSize { get; } + public string DownloadUrl { get; } + } + + internal class UpdateCheckEventArgs : EventArgs + { + public UpdateCheckEventArgs(UpdateCheckResult result) + { + Result = result; + } + + public UpdateCheckResult Result { get; } + } + + private readonly string expectedAssetName; + private HttpClient httpClient; + private Timer timer; + private string ignoreReleaseName; + private bool disposed; + + public Updater() + { + expectedAssetName = GetExpectedAssetName(); + httpClient = null; + timer = null; + ignoreReleaseName = null; + disposed = false; + } + + public event EventHandler UpdateCheck; + + public void Configure(HttpClient httpClient, DateTime? nextUpdateCheck, string ignoreReleaseName) + { + this.httpClient = httpClient; + this.ignoreReleaseName = ignoreReleaseName; + if (timer != null) + { + return; + timer.Dispose(); + timer = null; + } + if (nextUpdateCheck.HasValue) + { + TimeSpan nextUpdateTimeSpan = nextUpdateCheck.Value - DateTime.Now; + if (nextUpdateTimeSpan.TotalSeconds < 0) + { + nextUpdateTimeSpan = TimeSpan.Zero; + } + timer = new Timer(state => TimerTick(), null, nextUpdateTimeSpan, Timeout.InfiniteTimeSpan); + } + } + + public void Dispose() + { + if (!disposed) + { + if (timer != null) + { + timer.Dispose(); + timer = null; + } + disposed = true; + } + } + + public async Task CheckForUpdateAsync() + { + UpdateCheckResult result = null; + if (httpClient != null) + { + string url = GITHUB_RELEASE_API_URL; + Logger.Debug($"Sending a request to {url}"); + HttpResponseMessage response = await httpClient.GetAsync(url); + Logger.Debug($"Response status code: {(int)response.StatusCode} {response.StatusCode}."); + Logger.Debug("Response headers:", response.Headers.ToString().TrimEnd(), response.Content.Headers.ToString().TrimEnd()); + if (response.StatusCode != HttpStatusCode.OK) + { + throw new Exception($"GitHub API returned {(int)response.StatusCode} {response.StatusCode}."); + } + string responseContent = await response.Content.ReadAsStringAsync(); + Logger.Debug("Response content:", responseContent); + List releases; + try + { + releases = JsonConvert.DeserializeObject>(responseContent); + } + catch (Exception exception) + { + throw new Exception("GitHub API response is not a valid JSON string.", exception); + } + Logger.Debug($"{releases.Count} releases have been parsed from the GitHub API response."); + if (releases.Any()) + { + GitHubApiRelease latestRelease = releases.First(); + Logger.Debug($@"Latest release is ""{latestRelease.Name}""."); + if (latestRelease.Name != CURRENT_GITHUB_RELEASE_NAME && latestRelease.Name != ignoreReleaseName) + { + GitHubApiRelease.Asset asset = latestRelease.Assets.FirstOrDefault(assetItem => assetItem.Name == expectedAssetName); + if (asset != null) + { + Logger.Debug($"New asset is {asset.Name} ({asset.Size} bytes), download URL = {asset.DownloadUrl}."); + result = new UpdateCheckResult(latestRelease.Name, latestRelease.PublishedAt, asset.Name, asset.Size, asset.DownloadUrl); + } + } + } + } + return result; + } + + private void TimerTick() + { + UpdateCheckResult updateCheckResult; + try + { + updateCheckResult = CheckForUpdateAsync().Result; + } + catch (Exception exception) + { + Logger.Exception(exception); + updateCheckResult = null; + } + UpdateCheck?.Invoke(this, new UpdateCheckEventArgs(updateCheckResult)); + } + + private string GetExpectedAssetName() + { + if (Environment.IsInPortableMode) + { + return Environment.IsIn64BitProcess ? "LibgenDesktop.Portable.64-bit.zip" : "LibgenDesktop.Portable.32-bit.zip"; + } + else + { + return Environment.IsIn64BitProcess ? "LibgenDesktop.Setup.64-bit.msi" : "LibgenDesktop.Setup.32-bit.msi"; + } + } + } +} diff --git a/LibgenDesktop/Models/Utils/UrlGenerator.cs b/LibgenDesktop/Models/Utils/UrlGenerator.cs index e90b115..1994ce3 100644 --- a/LibgenDesktop/Models/Utils/UrlGenerator.cs +++ b/LibgenDesktop/Models/Utils/UrlGenerator.cs @@ -1,4 +1,6 @@ using System; +using System.Collections.Generic; +using System.Text.RegularExpressions; using LibgenDesktop.Models.Entities; using LibgenDesktop.Models.Settings; @@ -6,40 +8,229 @@ namespace LibgenDesktop.Models.Utils { internal static class UrlGenerator { + private static Regex regex; + private static Dictionary> nonFictionTransformations; + private static Dictionary> fictionTransformations; + private static Dictionary> sciMagTransformations; + + static UrlGenerator() + { + regex = new Regex("{([^}]+)}"); + nonFictionTransformations = new Dictionary> + { + { "id", book => book.LibgenId.ToString() }, + { "title", book => book.Title }, + { "volume", book => book.VolumeInfo }, + { "series", book => book.Series }, + { "periodical", book => book.Periodical }, + { "authors", book => book.Authors }, + { "year", book => book.Year }, + { "edition", book => book.Edition }, + { "publisher", book => book.Publisher }, + { "city", book => book.City }, + { "pages", book => book.Pages }, + { "pages-in-file", book => book.PagesInFile.ToString() }, + { "language", book => book.Language }, + { "topic", book => book.Topic }, + { "library", book => book.Library }, + { "issue", book => book.Issue }, + { "isbn", book => book.Identifier }, + { "issn", book => book.Issn }, + { "asin", book => book.Asin }, + { "udc", book => book.Udc }, + { "lbc", book => book.Lbc }, + { "ddc", book => book.Ddc }, + { "lcc", book => book.Lcc }, + { "doi", book => book.Doi }, + { "google-book-id", book => book.GoogleBookId }, + { "open-library-id", book => book.OpenLibraryId }, + { "commentary", book => book.Commentary }, + { "dpi", book => book.Dpi.ToString() }, + { "color", book => book.Color }, + { "cleaned", book => book.Cleaned }, + { "orientation", book => book.Orientation }, + { "paginated", book => book.Paginated }, + { "scanned", book => book.Scanned }, + { "bookmarked", book => book.Bookmarked }, + { "searchable", book => book.Searchable }, + { "size", book => book.SizeInBytes.ToString() }, + { "ext", book => book.Format }, + { "md5", book => book.Md5Hash }, + { "generic", book => book.Generic }, + { "visible", book => book.Visible }, + { "locator", book => book.Locator }, + { "local", book => book.Local.ToString() }, + { "added", book => book.AddedDateTime.ToString("yyyy-MM-dd HH:mm:ss") }, + { "last-modified", book => book.LastModifiedDateTime.ToString("yyyy-MM-dd HH:mm:ss") }, + { "cover-url", book => book.CoverUrl }, + { "tags", book => book.Tags }, + { "isbn-plain", book => book.IdentifierPlain }, + { "thousand-index", book => (book.LibgenId / 1000).ToString("D4") }, + { "thousand-bucket", book => (book.LibgenId / 1000 * 1000).ToString() } + }; + fictionTransformations = new Dictionary> + { + { "id", book => book.LibgenId.ToString() }, + { "title", book => book.Title }, + { "authors", book => book.Authors }, + { "series", book => book.Series }, + { "russian-author", book => book.RussianAuthor }, + { "author1-last-name", book => book.AuthorFamily1 }, + { "author1-first-name", book => book.AuthorName1 }, + { "author1-surname", book => book.AuthorSurname1 }, + { "author1-role", book => book.Role1 }, + { "author1-pseudonim", book => book.Pseudonim1 }, + { "author2-last-name", book => book.AuthorFamily2 }, + { "author2-first-name", book => book.AuthorName2 }, + { "author2-surname", book => book.AuthorSurname2 }, + { "author2-role", book => book.Role2 }, + { "author2-pseudonim", book => book.Pseudonim2 }, + { "author3-last-name", book => book.AuthorFamily3 }, + { "author3-first-name", book => book.AuthorName3 }, + { "author3-surname", book => book.AuthorSurname3 }, + { "author3-role", book => book.Role3 }, + { "author3-pseudonim", book => book.Pseudonim3 }, + { "author4-last-name", book => book.AuthorFamily4 }, + { "author4-first-name", book => book.AuthorName4 }, + { "author4-surname", book => book.AuthorSurname4 }, + { "author4-role", book => book.Role4 }, + { "author4-pseudonim", book => book.Pseudonim4 }, + { "series1", book => book.Series1 }, + { "series2", book => book.Series2 }, + { "series3", book => book.Series3 }, + { "series4", book => book.Series4 }, + { "ext", book => book.Format }, + { "version", book => book.Version }, + { "size", book => book.SizeInBytes.ToString() }, + { "md5", book => book.Md5Hash }, + { "path", book => book.Path }, + { "language", book => book.Language }, + { "pages", book => book.Pages }, + { "isbn", book => book.Identifier }, + { "year", book => book.Year }, + { "publisher", book => book.Publisher }, + { "edition", book => book.Edition }, + { "commentary", book => book.Commentary }, + { "added", book => book.AddedDateTime?.ToString("yyyy-MM-dd HH:mm:ss") ?? String.Empty }, + { "last-modified", book => book.LastModifiedDateTime.ToString("yyyy-MM-dd HH:mm:ss") }, + { "russian-author-last-name", book => book.RussianAuthorFamily }, + { "russian-author-first-name", book => book.RussianAuthorName }, + { "russian-author-surname", book => book.RussianAuthorSurname }, + { "cover", book => book.Cover }, + { "google-book-id", book => book.GoogleBookId }, + { "asin", book => book.Asin }, + { "author-hash", book => book.AuthorHash }, + { "title-hash", book => book.TitleHash }, + { "visible", book => book.Visible }, + { "thousand-index", book => (book.LibgenId / 1000).ToString("D4") }, + { "thousand-bucket", book => (book.LibgenId / 1000 * 1000).ToString() } + }; + sciMagTransformations = new Dictionary> + { + { "id", article => article.LibgenId.ToString() }, + { "title", article => article.Title }, + { "authors", article => article.Authors }, + { "doi", article => article.Doi }, + { "doi2", article => article.Doi2 }, + { "year", article => article.Year }, + { "month", article => article.Month }, + { "day", article => article.Day }, + { "volume", article => article.Volume }, + { "issue", article => article.Issue }, + { "first-page", article => article.FirstPage }, + { "last-page", article => article.LastPage }, + { "journal", article => article.Journal }, + { "isbn", article => article.Isbn }, + { "issnp", article => article.Issnp }, + { "issne", article => article.Issne }, + { "md5", article => article.Md5Hash }, + { "size", article => article.SizeInBytes.ToString() }, + { "added", article => article.AddedDateTime?.ToString("yyyy-MM-dd HH:mm:ss") ?? String.Empty }, + { "journal-id", article => article.JournalId }, + { "abstract-url", article => article.AbstractUrl }, + { "attribute1", article => article.Attribute1 }, + { "attribute2", article => article.Attribute2 }, + { "attribute3", article => article.Attribute3 }, + { "attribute4", article => article.Attribute4 }, + { "attribute5", article => article.Attribute5 }, + { "attribute6", article => article.Attribute6 }, + { "visible", article => article.Visible }, + { "pubmed-id", article => article.PubmedId }, + { "pmc", article => article.Pmc }, + { "pii", article => article.Pii }, + { "thousand-index", book => (book.LibgenId / 1000).ToString("D4") }, + { "thousand-bucket", book => (book.LibgenId / 1000 * 1000).ToString() } + }; + } + public static string GetNonFictionDownloadUrl(Mirrors.MirrorConfiguration mirror, NonFictionBook book) { - return Replace(mirror.NonFictionDownloadUrl, book.LibgenId, book.Md5Hash, book.CoverUrl, String.Empty); + return Replace(mirror.NonFictionDownloadUrl, nonFictionTransformations, book); } public static string GetNonFictionCoverUrl(Mirrors.MirrorConfiguration mirror, NonFictionBook book) { - return Replace(mirror.NonFictionCoverUrl, book.LibgenId, book.Md5Hash, book.CoverUrl, String.Empty); + return Replace(mirror.NonFictionCoverUrl, nonFictionTransformations, book); } public static string GetFictionDownloadUrl(Mirrors.MirrorConfiguration mirror, FictionBook book) { - return Replace(mirror.FictionDownloadUrl, book.LibgenId, book.Md5Hash, String.Empty, String.Empty); + return Replace(mirror.FictionDownloadUrl, fictionTransformations, book); } public static string GetFictionCoverUrl(Mirrors.MirrorConfiguration mirror, FictionBook book) { - return Replace(mirror.FictionCoverUrl, book.LibgenId, book.Md5Hash, String.Empty, String.Empty); + return Replace(mirror.FictionCoverUrl, fictionTransformations, book); } public static string GetSciMagDownloadUrl(Mirrors.MirrorConfiguration mirror, SciMagArticle article) { - return Replace(mirror.SciMagDownloadUrl, article.LibgenId, String.Empty, String.Empty, article.Doi); + return Replace(mirror.SciMagDownloadUrl, sciMagTransformations, article); } - private static string Replace(string template, int id, string md5, string coverUrl, string doi) + private static string Replace(string template, Dictionary> transformations, T objectValues) { if (String.IsNullOrWhiteSpace(template)) { return null; } - string thousandBucket = (id / 1000 * 1000).ToString(); - return template.Replace("{id}", id.ToString()).Replace("{md5}", md5).Replace("{cover-url}", coverUrl).Replace("{doi}", doi). - Replace("{thousand-bucket}", thousandBucket); + string result = regex.Replace(template, match => + { + string transformationKey = match.Groups[1].Value.ToLower(); + bool toUpperCase = false; + bool toLowerCase = false; + if (transformationKey.EndsWith(":u")) + { + toUpperCase = true; + } + else if (transformationKey.EndsWith(":l")) + { + toLowerCase = true; + } + if (toUpperCase || toLowerCase) + { + transformationKey = transformationKey.Substring(0, transformationKey.Length - 2); + } + string replaceWith; + if (transformations.TryGetValue(transformationKey, out Func transformation)) + { + replaceWith = transformation(objectValues); + if (toUpperCase) + { + replaceWith = replaceWith.ToUpper(); + } + else if (toLowerCase) + { + replaceWith = replaceWith.ToLower(); + } + } + else + { + replaceWith = match.ToString(); + } + return replaceWith; + }); + return result; } } } diff --git a/LibgenDesktop/ViewModels/ApplicationUpdateWindowViewModel.cs b/LibgenDesktop/ViewModels/ApplicationUpdateWindowViewModel.cs new file mode 100644 index 0000000..2fbe8a6 --- /dev/null +++ b/LibgenDesktop/ViewModels/ApplicationUpdateWindowViewModel.cs @@ -0,0 +1,303 @@ +using System; +using System.Diagnostics; +using System.IO; +using System.Threading; +using LibgenDesktop.Infrastructure; +using LibgenDesktop.Models; +using LibgenDesktop.Models.ProgressArgs; +using LibgenDesktop.Models.Update; +using LibgenDesktop.Views; +using Environment = LibgenDesktop.Common.Environment; + +namespace LibgenDesktop.ViewModels +{ + internal class ApplicationUpdateWindowViewModel : LibgenWindowViewModel + { + private readonly MainModel mainModel; + private readonly Updater.UpdateCheckResult updateCheckResult; + private readonly CancellationTokenSource cancellationTokenSource; + private string newVersionText; + private double downloadProgress; + private bool isSkipButtonVisible; + private string downloadButtonText; + private bool isDownloadButtonVisible; + private bool isCancelButtonVisible; + private bool isInterruptButtonVisible; + private bool isCloseButtonVisible; + private bool error; + private bool isInterruptButtonEnabled; + private string interruptButtonText; + + public ApplicationUpdateWindowViewModel(MainModel mainModel, Updater.UpdateCheckResult updateCheckResult) + { + this.mainModel = mainModel; + this.updateCheckResult = updateCheckResult; + cancellationTokenSource = new CancellationTokenSource(); + WindowClosingCommand = new FuncCommand(WindowClosing); + SkipVersionCommand = new Command(SkipVersion); + DownloadCommand = new Command(DownloadAsync); + CancelCommand = new Command(Cancel); + InterruptDownloadCommand = new Command(InterruptDownload); + CloseCommand = new Command(Close); + Initialize(); + } + + public string NewVersionText + { + get + { + return newVersionText; + } + set + { + newVersionText = value; + NotifyPropertyChanged(); + } + } + + public double DownloadProgress + { + get + { + return downloadProgress; + } + set + { + downloadProgress = value; + NotifyPropertyChanged(); + } + } + + public bool IsSkipButtonVisible + { + get + { + return isSkipButtonVisible; + } + set + { + isSkipButtonVisible = value; + NotifyPropertyChanged(); + } + } + + public string DownloadButtonText + { + get + { + return downloadButtonText; + } + set + { + downloadButtonText = value; + NotifyPropertyChanged(); + } + } + + public bool IsDownloadButtonVisible + { + get + { + return isDownloadButtonVisible; + } + set + { + isDownloadButtonVisible = value; + NotifyPropertyChanged(); + } + } + + public bool IsCancelButtonVisible + { + get + { + return isCancelButtonVisible; + } + set + { + isCancelButtonVisible = value; + NotifyPropertyChanged(); + } + } + + public string InterruptButtonText + { + get + { + return interruptButtonText; + } + set + { + interruptButtonText = value; + NotifyPropertyChanged(); + } + } + + public bool IsInterruptButtonEnabled + { + get + { + return isInterruptButtonEnabled; + } + set + { + isInterruptButtonEnabled = value; + NotifyPropertyChanged(); + } + } + + public bool IsInterruptButtonVisible + { + get + { + return isInterruptButtonVisible; + } + set + { + isInterruptButtonVisible = value; + NotifyPropertyChanged(); + } + } + + public bool IsCloseButtonVisible + { + get + { + return isCloseButtonVisible; + } + set + { + isCloseButtonVisible = value; + NotifyPropertyChanged(); + } + } + + public FuncCommand WindowClosingCommand { get; } + public Command SkipVersionCommand { get; } + public Command DownloadCommand { get; } + public Command CancelCommand { get; } + public Command InterruptDownloadCommand { get; } + public Command CloseCommand { get; } + + public event EventHandler ApplicationShutdownRequested; + + private void Initialize() + { + newVersionText = $"Новая версия: {updateCheckResult.NewReleaseName} от {updateCheckResult.PublishedAt:dd.MM.yyyy}"; + isSkipButtonVisible = true; + downloadProgress = 0; + downloadButtonText = Environment.IsInPortableMode ? "СКАЧАТЬ" : "СКАЧАТЬ И УСТАНОВИТЬ"; + isDownloadButtonVisible = true; + isCancelButtonVisible = true; + interruptButtonText = "ПРЕРВАТЬ"; + isInterruptButtonEnabled = true; + isInterruptButtonVisible = false; + isCloseButtonVisible = false; + error = false; + } + + private void SkipVersion() + { + mainModel.AppSettings.LastUpdate.IgnoreReleaseName = updateCheckResult.NewReleaseName; + mainModel.SaveSettings(); + mainModel.LastApplicationUpdateCheckResult = null; + CurrentWindowContext.CloseDialog(true); + } + + private async void DownloadAsync() + { + IsSkipButtonVisible = false; + IsDownloadButtonVisible = false; + IsCancelButtonVisible = false; + IsInterruptButtonVisible = true; + string downloadFilePath = null; + MainModel.DownloadFileResult? result = null; + try + { + string downloadDirectory = Path.Combine(Environment.AppDataDirectory, "Updates"); + if (!Directory.Exists(downloadDirectory)) + { + Directory.CreateDirectory(downloadDirectory); + } + downloadFilePath = Path.Combine(downloadDirectory, updateCheckResult.FileName); + Progress downloadProgressHandler = new Progress(HandleDownloadProgress); + result = await mainModel.DownloadFileAsync(updateCheckResult.DownloadUrl, downloadFilePath, downloadProgressHandler, + cancellationTokenSource.Token); + } + catch (Exception exception) + { + ShowErrorWindow(exception, CurrentWindowContext); + error = true; + } + if (!error && result != MainModel.DownloadFileResult.COMPLETED) + { + if (result == MainModel.DownloadFileResult.INCOMPLETE) + { + MessageBoxWindow.ShowMessage("Ошибка", "Файл обновления не был загружен полностью.", CurrentWindowContext); + } + error = true; + } + IsInterruptButtonVisible = false; + IsCloseButtonVisible = true; + if (!error) + { + if (Environment.IsInPortableMode) + { + Process.Start("explorer.exe", $@"/select, ""{downloadFilePath}"""); + CurrentWindowContext.CloseDialog(false); + } + else + { + Process.Start(downloadFilePath); + CurrentWindowContext.CloseDialog(false); + ApplicationShutdownRequested?.Invoke(this, EventArgs.Empty); + } + } + } + + private void HandleDownloadProgress(object progress) + { + if (progress is DownloadFileProgress downloadFileProgress) + { + DownloadProgress = (double)downloadFileProgress.DownloadedBytes * 100 / downloadFileProgress.FileSize; + } + } + + private void Cancel() + { + CurrentWindowContext.CloseDialog(false); + } + + private void InterruptDownload() + { + IsInterruptButtonEnabled = false; + InterruptButtonText = "ПРЕРЫВАЕТСЯ..."; + cancellationTokenSource.Cancel(); + } + + private void Close() + { + CurrentWindowContext.CloseDialog(!error); + } + + private bool WindowClosing() + { + if (IsInterruptButtonVisible) + { + if (MessageBoxWindow.ShowPrompt("Прервать загрузку?", "Прервать загрузку обновления?", CurrentWindowContext)) + { + cancellationTokenSource.Cancel(); + return true; + } + else + { + return false; + } + } + else + { + return true; + } + } + } +} diff --git a/LibgenDesktop/ViewModels/CreateDatabaseViewModel.cs b/LibgenDesktop/ViewModels/CreateDatabaseWindowViewModel.cs similarity index 97% rename from LibgenDesktop/ViewModels/CreateDatabaseViewModel.cs rename to LibgenDesktop/ViewModels/CreateDatabaseWindowViewModel.cs index e77f21f..8ab60aa 100644 --- a/LibgenDesktop/ViewModels/CreateDatabaseViewModel.cs +++ b/LibgenDesktop/ViewModels/CreateDatabaseWindowViewModel.cs @@ -9,7 +9,7 @@ namespace LibgenDesktop.ViewModels { - internal class CreateDatabaseViewModel : LibgenWindowViewModel + internal class CreateDatabaseWindowViewModel : LibgenWindowViewModel { internal enum EventType { @@ -24,7 +24,7 @@ internal enum EventType private bool isCreateDatabaseSelected; private bool isOpenDatabaseSelected; - public CreateDatabaseViewModel(MainModel mainModel) + public CreateDatabaseWindowViewModel(MainModel mainModel) { this.mainModel = mainModel; OkButtonCommand = new Command(OkButtonClick); diff --git a/LibgenDesktop/ViewModels/FictionSearchResultsTabViewModel.cs b/LibgenDesktop/ViewModels/FictionSearchResultsTabViewModel.cs index fb57754..10cc5db 100644 --- a/LibgenDesktop/ViewModels/FictionSearchResultsTabViewModel.cs +++ b/LibgenDesktop/ViewModels/FictionSearchResultsTabViewModel.cs @@ -43,6 +43,7 @@ public FictionSearchResultsTabViewModel(MainModel mainModel, IWindowContext pare ExportPanelViewModel.ClosePanel += CloseExportPanel; OpenDetailsCommand = new Command(param => OpenDetails(param as FictionBook)); SearchCommand = new Command(Search); + ExportCommand = new Command(ShowExportPanel); BookDataGridEnterKeyCommand = new Command(BookDataGridEnterKeyPressed); Initialize(); } diff --git a/LibgenDesktop/ViewModels/ImportLogPanelViewModel.cs b/LibgenDesktop/ViewModels/ImportLogPanelViewModel.cs new file mode 100644 index 0000000..c10e81e --- /dev/null +++ b/LibgenDesktop/ViewModels/ImportLogPanelViewModel.cs @@ -0,0 +1,186 @@ +using System; +using System.Collections.ObjectModel; +using System.Linq; + +namespace LibgenDesktop.ViewModels +{ + internal class ImportLogPanelViewModel : ViewModel + { + internal class ImportLogItemViewModel : ViewModel + { + private string stepIndex; + private bool isStepIndexVisible; + private string headerText; + private ObservableCollection logLines; + + public ImportLogItemViewModel(int? stepIndex, string headerText) + { + this.stepIndex = stepIndex.HasValue ? $"Шаг {stepIndex.Value}" : String.Empty; + isStepIndexVisible = stepIndex.HasValue; + this.headerText = headerText; + logLines = new ObservableCollection(); + } + + public string StepIndex + { + get + { + return stepIndex; + } + set + { + stepIndex = value; + NotifyPropertyChanged(); + } + } + + public bool IsStepIndexVisible + { + get + { + return isStepIndexVisible; + } + set + { + isStepIndexVisible = value; + NotifyPropertyChanged(); + } + } + + public string HeaderText + { + get + { + return headerText; + } + set + { + headerText = value; + NotifyPropertyChanged(); + } + } + + public ObservableCollection LogLines + { + get + { + return logLines; + } + set + { + logLines = value; + NotifyPropertyChanged(); + } + } + + public string LogLine + { + get + { + return logLines.FirstOrDefault(); + } + set + { + if (!logLines.Any()) + { + logLines.Add(value); + } + else + { + logLines[logLines.Count - 1] = value; + } + } + } + } + + private string resultLogLine; + private bool isResultLogLineVisible; + private string errorLogLine; + private bool isErrorLogLineVisible; + + public ImportLogPanelViewModel() + { + resultLogLine = String.Empty; + isResultLogLineVisible = false; + errorLogLine = String.Empty; + isErrorLogLineVisible = false; + LogItems = new ObservableCollection(); + } + + public ObservableCollection LogItems { get; } + + public string ResultLogLine + { + get + { + return resultLogLine; + } + set + { + resultLogLine = value; + NotifyPropertyChanged(); + } + } + + public bool IsResultLogLineVisible + { + get + { + return isResultLogLineVisible; + } + set + { + isResultLogLineVisible = value; + NotifyPropertyChanged(); + } + } + + public string ErrorLogLine + { + get + { + return errorLogLine; + } + set + { + errorLogLine = value; + NotifyPropertyChanged(); + } + } + + public bool IsErrorLogLineVisible + { + get + { + return isErrorLogLineVisible; + } + set + { + isErrorLogLineVisible = value; + NotifyPropertyChanged(); + } + } + + public void AddLogItem(int? stepIndex, string headerText, string logLine = null) + { + ImportLogItemViewModel importLogItemViewModel = new ImportLogItemViewModel(stepIndex, headerText); + if (logLine != null) + { + importLogItemViewModel.LogLine = logLine; + } + LogItems.Add(importLogItemViewModel); + } + + public void ShowErrorLogLine(string errorLogLine) + { + ErrorLogLine = errorLogLine; + IsErrorLogLineVisible = true; + } + + public void ShowResultLogLine(string resultLogLine) + { + ResultLogLine = resultLogLine; + IsResultLogLineVisible = true; + } + } +} diff --git a/LibgenDesktop/ViewModels/ImportWindowViewModel.cs b/LibgenDesktop/ViewModels/ImportWindowViewModel.cs index 13eb454..d60ac02 100644 --- a/LibgenDesktop/ViewModels/ImportWindowViewModel.cs +++ b/LibgenDesktop/ViewModels/ImportWindowViewModel.cs @@ -1,6 +1,4 @@ using System; -using System.Collections.ObjectModel; -using System.Linq; using System.Text; using System.Threading; using LibgenDesktop.Infrastructure; @@ -17,6 +15,7 @@ private enum Step { SEARCHING_TABLE_DEFINITION = 1, CREATING_INDEXES, + LOADING_EXISTING_IDS, IMPORTING_DATA } @@ -26,11 +25,6 @@ private enum Step private readonly Timer elapsedTimer; private bool isInProgress; private string status; - private ObservableCollection logs; - private string resultLogLine; - private bool isResultLogLineVisible; - private string errorLogLine; - private bool isErrorLogLineVisible; private string elapsed; private string cancelButtonText; private bool isCancelButtonVisible; @@ -50,12 +44,15 @@ public ImportWindowViewModel(MainModel mainModel, string dumpFilePath) this.dumpFilePath = dumpFilePath; cancellationTokenSource = new CancellationTokenSource(); elapsedTimer = new Timer(state => UpdateElapsedTime()); + Logs = new ImportLogPanelViewModel(); CancelCommand = new Command(Cancel); CloseCommand = new Command(Close); WindowClosedCommand = new Command(WindowClosed); Initialize(); } + public ImportLogPanelViewModel Logs { get; } + public bool IsInProgress { get @@ -82,71 +79,6 @@ public string Status } } - public ObservableCollection Logs - { - get - { - return logs; - } - set - { - logs = value; - NotifyPropertyChanged(); - } - } - - public string ResultLogLine - { - get - { - return resultLogLine; - } - set - { - resultLogLine = value; - NotifyPropertyChanged(); - } - } - - public bool IsResultLogLineVisible - { - get - { - return isResultLogLineVisible; - } - set - { - isResultLogLineVisible = value; - NotifyPropertyChanged(); - } - } - - public string ErrorLogLine - { - get - { - return errorLogLine; - } - set - { - errorLogLine = value; - NotifyPropertyChanged(); - } - } - - public bool IsErrorLogLineVisible - { - get - { - return isErrorLogLineVisible; - } - set - { - isErrorLogLineVisible = value; - NotifyPropertyChanged(); - } - } - public string Elapsed { get @@ -217,11 +149,11 @@ public bool IsCloseButtonVisible public Command CloseCommand { get; } public Command WindowClosedCommand { get; } - private ProgressLogItemViewModel CurrentLogItem + private ImportLogPanelViewModel.ImportLogItemViewModel CurrentLogItem { get { - return logs[currentStepIndex - 1]; + return Logs.LogItems[currentStepIndex - 1]; } } @@ -232,9 +164,6 @@ private void Initialize() currentStepIndex = 1; totalSteps = 2; UpdateStatus("Поиск данных для импорта"); - logs = new ObservableCollection(); - isResultLogLineVisible = false; - isErrorLogLineVisible = false; startDateTime = DateTime.Now; lastElapsedTime = TimeSpan.Zero; elapsedTimer.Change(TimeSpan.Zero, TimeSpan.FromMilliseconds(100)); @@ -244,9 +173,7 @@ private void Initialize() isCancelButtonEnabled = true; isCloseButtonVisible = false; lastScannedPercentage = 0; - ProgressLogItemViewModel searchHeaderLogItem = new ProgressLogItemViewModel(currentStepIndex, "Поиск данных для импорта в файле"); - searchHeaderLogItem.LogLine = "Идет сканирование файла..."; - logs.Add(searchHeaderLogItem); + Logs.AddLogItem(currentStepIndex, "Поиск данных для импорта в файле", "Идет сканирование файла..."); Import(); } @@ -262,8 +189,7 @@ private async void Import() catch (Exception exception) { elapsedTimer.Change(Timeout.InfiniteTimeSpan, Timeout.InfiniteTimeSpan); - ErrorLogLine = "Импорт завершился с ошибками."; - IsErrorLogLineVisible = true; + Logs.ShowErrorLogLine("Импорт завершился с ошибками."); IsInProgress = false; Status = "Импорт завершен с ошибками"; IsCancelButtonVisible = false; @@ -275,18 +201,15 @@ private async void Import() switch (importResult) { case MainModel.ImportSqlDumpResult.COMPLETED: - ResultLogLine = "Импорт выполнен успешно."; - IsResultLogLineVisible = true; + Logs.ShowResultLogLine("Импорт выполнен успешно."); Status = "Импорт завершен"; break; case MainModel.ImportSqlDumpResult.CANCELLED: - ErrorLogLine = "Импорт был прерван пользователем."; - IsErrorLogLineVisible = true; + Logs.ShowErrorLogLine("Импорт был прерван пользователем."); Status = "Импорт прерван"; break; case MainModel.ImportSqlDumpResult.DATA_NOT_FOUND: - ErrorLogLine = "Не найдены данные для импорта."; - IsErrorLogLineVisible = true; + Logs.ShowErrorLogLine("Не найдены данные для импорта."); Status = "Данные не найдены"; break; } @@ -336,17 +259,28 @@ private void HandleImportProgress(object progress) currentStepIndex++; totalSteps++; UpdateStatus("Создание индексов"); - logs.Add(new ProgressLogItemViewModel(currentStepIndex, "Создание недостающих индексов")); + Logs.AddLogItem(currentStepIndex, "Создание недостающих индексов"); } CurrentLogItem.LogLines.Add($"Создается индекс для столбца {createIndexProgress.ColumnName}..."); break; + case ImportLoadLibgenIdsProgress importLoadLibgenIdsProgress: + if (currentStep != Step.LOADING_EXISTING_IDS) + { + currentStep = Step.LOADING_EXISTING_IDS; + currentStepIndex++; + totalSteps++; + UpdateStatus("Загрузка идентификаторов"); + Logs.AddLogItem(currentStepIndex, "Загрузка идентификаторов существующих данных"); + } + CurrentLogItem.LogLines.Add($"Загрузка значений столбца LibgenId..."); + break; case ImportObjectsProgress importObjectsProgress: if (currentStep != Step.IMPORTING_DATA) { currentStep = Step.IMPORTING_DATA; currentStepIndex++; UpdateStatus("Импорт данных"); - logs.Add(new ProgressLogItemViewModel(currentStepIndex, "Импорт данных")); + Logs.AddLogItem(currentStepIndex, "Импорт данных"); } string logLine = GetImportedObjectCountLogLine(importObjectsProgress.ObjectsAdded, importObjectsProgress.ObjectsUpdated); CurrentLogItem.LogLine = logLine; diff --git a/LibgenDesktop/ViewModels/MainWindowViewModel.cs b/LibgenDesktop/ViewModels/MainWindowViewModel.cs index a30f0fd..e32e492 100644 --- a/LibgenDesktop/ViewModels/MainWindowViewModel.cs +++ b/LibgenDesktop/ViewModels/MainWindowViewModel.cs @@ -16,22 +16,26 @@ internal class MainWindowViewModel : LibgenWindowViewModel private readonly MainModel mainModel; private SearchTabViewModel defaultSearchTabViewModel; private TabViewModel selectedTabViewModel; + private bool isApplicationUpdateAvailable; public MainWindowViewModel(MainModel mainModel) { this.mainModel = mainModel; defaultSearchTabViewModel = null; + Events = new EventProvider(); NewTabCommand = new Command(NewTab); CloseTabCommand = new Command(param => CloseTab(param as TabViewModel)); CloseCurrentTabCommand = new Command(CloseCurrentTab); ExportCommand = new Command(Export); DownloadManagerCommand = new Command(ShowDownloadManager); + ShowApplicationUpdateCommand = new Command(ShowApplicationUpdate); ImportCommand = new Command(Import); SynchronizeCommand = new Command(Synchronize); SettingsCommand = new Command(SettingsMenuItemClick); WindowClosedCommand = new Command(WindowClosed); TabViewModels = new ObservableCollection(); Initialize(); + mainModel.ApplicationUpdateCheckCompleted += ApplicationUpdateCheckCompleted; } public int WindowWidth { get; set; } @@ -39,7 +43,7 @@ public MainWindowViewModel(MainModel mainModel) public int WindowLeft { get; set; } public int WindowTop { get; set; } public bool IsWindowMaximized { get; set; } - + public EventProvider Events { get; } public ObservableCollection TabViewModels { get; } public SearchTabViewModel DefaultSearchTabViewModel @@ -91,11 +95,25 @@ public bool IsNewTabButtonVisible } } + public bool IsApplicationUpdateAvailable + { + get + { + return isApplicationUpdateAvailable; + } + set + { + isApplicationUpdateAvailable = value; + NotifyPropertyChanged(); + } + } + public Command NewTabCommand { get; } public Command CloseTabCommand { get; } public Command CloseCurrentTabCommand { get; } public Command ExportCommand { get; } public Command DownloadManagerCommand { get; } + public Command ShowApplicationUpdateCommand { get; } public Command ImportCommand { get; } public Command SynchronizeCommand { get; } public Command SettingsCommand { get; } @@ -104,7 +122,7 @@ public bool IsNewTabButtonVisible private void Initialize() { AppSettings appSettings = mainModel.AppSettings; - AppSettings.MainWindowSettings mainWindowSettings = appSettings.MainWindow; + MainWindowSettings mainWindowSettings = appSettings.MainWindow; WindowWidth = mainWindowSettings.Width; WindowHeight = mainWindowSettings.Height; WindowLeft = mainWindowSettings.Left; @@ -115,6 +133,7 @@ private void Initialize() DefaultSearchTabViewModel.FictionSearchComplete += SearchTabFictionSearchComplete; DefaultSearchTabViewModel.SciMagSearchComplete += SearchTabSciMagSearchComplete; selectedTabViewModel = null; + isApplicationUpdateAvailable = false; } private void DefaultSearchTabViewModel_ImportRequested(object sender, EventArgs e) @@ -351,6 +370,20 @@ private void ShowDownloadManager() } } + private void ShowApplicationUpdate() + { + ApplicationUpdateWindowViewModel applicationUpdateWindowViewModel = + new ApplicationUpdateWindowViewModel(mainModel, mainModel.LastApplicationUpdateCheckResult); + applicationUpdateWindowViewModel.ApplicationShutdownRequested += Shutdown; + IWindowContext applicationUpdateWindowContext = WindowManager.CreateWindow(RegisteredWindows.WindowKey.APPLICATION_UPDATE_WINDOW, + applicationUpdateWindowViewModel, CurrentWindowContext); + if (applicationUpdateWindowContext.ShowDialog() == true) + { + IsApplicationUpdateAvailable = false; + } + applicationUpdateWindowViewModel.ApplicationShutdownRequested -= Shutdown; + } + private void Import() { OpenFileDialogParameters selectSqlDumpFileDialogParameters = new OpenFileDialogParameters @@ -381,7 +414,7 @@ private void Import() public void Synchronize() { - if (mainModel.NonFictionBookCount == 0) + if (mainModel.DatabaseMetadata.NonFictionFirstImportComplete != true) { MessageBoxWindow.ShowMessage("Ошибка", @"Перед синхронизацией списка нехудожественной литературы необходимо выполнить импорт из дампа базы данных (пункт ""Импорт"" в меню).", CurrentWindowContext); return; @@ -426,9 +459,19 @@ private void SettingsMenuItemClick() settingsWindowContext.ShowDialog(); } + private void ApplicationUpdateCheckCompleted(object sender, EventArgs e) + { + IsApplicationUpdateAvailable = mainModel.LastApplicationUpdateCheckResult != null; + } + + private void Shutdown(object sender, EventArgs e) + { + CurrentWindowContext.Close(); + } + private void WindowClosed() { - mainModel.AppSettings.MainWindow = new AppSettings.MainWindowSettings + mainModel.AppSettings.MainWindow = new MainWindowSettings { Width = WindowWidth, Height = WindowHeight, diff --git a/LibgenDesktop/ViewModels/ProgressLogItemViewModel.cs b/LibgenDesktop/ViewModels/ProgressLogItemViewModel.cs deleted file mode 100644 index 71e8438..0000000 --- a/LibgenDesktop/ViewModels/ProgressLogItemViewModel.cs +++ /dev/null @@ -1,77 +0,0 @@ -using System.Collections.ObjectModel; -using System.Linq; - -namespace LibgenDesktop.ViewModels -{ - internal class ProgressLogItemViewModel : ViewModel - { - private string step; - private string header; - private ObservableCollection logLines; - - public ProgressLogItemViewModel(int stepIndex, string header) - { - step = $"Шаг {stepIndex}"; - this.header = header; - logLines = new ObservableCollection(); - } - - public string Step - { - get - { - return step; - } - set - { - step = value; - NotifyPropertyChanged(); - } - } - - public string Header - { - get - { - return header; - } - set - { - header = value; - NotifyPropertyChanged(); - } - } - - public ObservableCollection LogLines - { - get - { - return logLines; - } - set - { - logLines = value; - NotifyPropertyChanged(); - } - } - - public string LogLine - { - get - { - return logLines.FirstOrDefault(); - } - set - { - if (!logLines.Any()) - { - logLines.Add(value); - } - else - { - logLines[logLines.Count - 1] = value; - } - } - } - } -} diff --git a/LibgenDesktop/ViewModels/SciMagSearchResultsTabViewModel.cs b/LibgenDesktop/ViewModels/SciMagSearchResultsTabViewModel.cs index eb3d10a..5e5abed 100644 --- a/LibgenDesktop/ViewModels/SciMagSearchResultsTabViewModel.cs +++ b/LibgenDesktop/ViewModels/SciMagSearchResultsTabViewModel.cs @@ -43,6 +43,7 @@ public SciMagSearchResultsTabViewModel(MainModel mainModel, IWindowContext paren ExportPanelViewModel.ClosePanel += CloseExportPanel; OpenDetailsCommand = new Command(param => OpenDetails(param as SciMagArticle)); SearchCommand = new Command(Search); + ExportCommand = new Command(ShowExportPanel); ArticleDataGridEnterKeyCommand = new Command(ArticleDataGridEnterKeyPressed); Initialize(); } diff --git a/LibgenDesktop/ViewModels/SettingsWindowViewModel.cs b/LibgenDesktop/ViewModels/SettingsWindowViewModel.cs index e243bc8..8fb45ac 100644 --- a/LibgenDesktop/ViewModels/SettingsWindowViewModel.cs +++ b/LibgenDesktop/ViewModels/SettingsWindowViewModel.cs @@ -9,6 +9,7 @@ using LibgenDesktop.Models.Settings; using LibgenDesktop.Views; using static LibgenDesktop.Common.Constants; +using static LibgenDesktop.Models.Settings.AppSettings; namespace LibgenDesktop.ViewModels { @@ -18,11 +19,16 @@ internal class SettingsWindowViewModel : LibgenWindowViewModel, INotifyDataError private readonly MainModel mainModel; private readonly Dictionary errors; + private bool isGeneralTabSelected; private bool isNetworkTabSelected; private bool isMirrorsTabSelected; private bool isSearchTabSelected; private bool isExportTabSelected; private bool isAdvancedTabSelected; + private Dictionary generalLanguagesList; + private KeyValuePair generalSelectedLanguage; + private Dictionary generalUpdateCheckIntervalList; + private KeyValuePair generalSelectedUpdateCheckInterval; private bool networkIsOfflineModeOn; private bool networkUseProxy; private string networkProxyAddress; @@ -65,6 +71,19 @@ public SettingsWindowViewModel(MainModel mainModel) Initialize(); } + public bool IsGeneralTabSelected + { + get + { + return isGeneralTabSelected; + } + set + { + isGeneralTabSelected = value; + NotifyPropertyChanged(); + } + } + public bool IsNetworkTabSelected { get @@ -130,6 +149,60 @@ public bool IsAdvancedTabSelected } } + public Dictionary GeneralLanguagesList + { + get + { + return generalLanguagesList; + } + set + { + generalLanguagesList = value; + NotifyPropertyChanged(); + } + } + + public KeyValuePair GeneralSelectedLanguage + { + get + { + return generalSelectedLanguage; + } + set + { + generalSelectedLanguage = value; + NotifyPropertyChanged(); + settingsChanged = true; + } + } + + public Dictionary GeneralUpdateCheckIntervalList + { + get + { + return generalUpdateCheckIntervalList; + } + set + { + generalUpdateCheckIntervalList = value; + NotifyPropertyChanged(); + } + } + + public KeyValuePair GeneralSelectedUpdateCheckInterval + { + get + { + return generalSelectedUpdateCheckInterval; + } + set + { + generalSelectedUpdateCheckInterval = value; + NotifyPropertyChanged(); + settingsChanged = true; + } + } + public bool NetworkIsOfflineModeOn { get @@ -139,8 +212,8 @@ public bool NetworkIsOfflineModeOn set { networkIsOfflineModeOn = value; - settingsChanged = true; NotifyPropertyChanged(); + settingsChanged = true; } } @@ -153,8 +226,8 @@ public bool NetworkUseProxy set { networkUseProxy = value; - settingsChanged = true; NotifyPropertyChanged(); + settingsChanged = true; Validate(); } } @@ -168,8 +241,8 @@ public string NetworkProxyAddress set { networkProxyAddress = value; - settingsChanged = true; NotifyPropertyChanged(); + settingsChanged = true; Validate(); } } @@ -183,8 +256,8 @@ public string NetworkProxyPort set { networkProxyPort = value; - settingsChanged = true; NotifyPropertyChanged(); + settingsChanged = true; Validate(); } } @@ -198,8 +271,8 @@ public string NetworkProxyUserName set { networkProxyUserName = value; - settingsChanged = true; NotifyPropertyChanged(); + settingsChanged = true; } } @@ -212,8 +285,8 @@ public string NetworkProxyPassword set { networkProxyPassword = value; - settingsChanged = true; NotifyPropertyChanged(); + settingsChanged = true; } } @@ -239,8 +312,8 @@ public string MirrorsSelectedNonFictionBooksMirror set { mirrorsSelectedNonFictionBooksMirror = value; - settingsChanged = true; NotifyPropertyChanged(); + settingsChanged = true; } } @@ -266,8 +339,8 @@ public string MirrorsSelectedNonFictionCoversMirror set { mirrorsSelectedNonFictionCoversMirror = value; - settingsChanged = true; NotifyPropertyChanged(); + settingsChanged = true; } } @@ -293,8 +366,8 @@ public string MirrorsSelectedNonFictionSynchronizationMirror set { mirrorsSelectedNonFictionSynchronizationMirror = value; - settingsChanged = true; NotifyPropertyChanged(); + settingsChanged = true; } } @@ -320,8 +393,8 @@ public string MirrorsSelectedFictionBooksMirror set { mirrorsSelectedFictionBooksMirror = value; - settingsChanged = true; NotifyPropertyChanged(); + settingsChanged = true; } } @@ -347,8 +420,8 @@ public string MirrorsSelectedFictionCoversMirror set { mirrorsSelectedFictionCoversMirror = value; - settingsChanged = true; NotifyPropertyChanged(); + settingsChanged = true; } } @@ -374,8 +447,8 @@ public string MirrorsSelectedArticlesMirror set { mirrorsSelectedArticlesMirror = value; - settingsChanged = true; NotifyPropertyChanged(); + settingsChanged = true; } } @@ -388,8 +461,8 @@ public bool SearchIsLimitResultsOn set { searchIsLimitResultsOn = value; - settingsChanged = true; NotifyPropertyChanged(); + settingsChanged = true; Validate(); } } @@ -416,8 +489,8 @@ public string SearchMaximumResultCount set { searchMaximumResultCount = value; - settingsChanged = true; NotifyPropertyChanged(); + settingsChanged = true; Validate(); } } @@ -431,8 +504,8 @@ public bool SearchIsOpenInModalWindowSelected set { searchIsOpenInModalWindowSelected = value; - settingsChanged = true; NotifyPropertyChanged(); + settingsChanged = true; } } @@ -445,8 +518,8 @@ public bool SearchIsOpenInNonModalWindowSelected set { searchIsOpenInNonModalWindowSelected = value; - settingsChanged = true; NotifyPropertyChanged(); + settingsChanged = true; } } @@ -459,8 +532,8 @@ public bool SearchIsOpenInNewTabSelected set { searchIsOpenInNewTabSelected = value; - settingsChanged = true; NotifyPropertyChanged(); + settingsChanged = true; } } @@ -515,8 +588,8 @@ public string ExportMaximumRowsPerFile set { exportMaximumRowsPerFile = value; - settingsChanged = true; NotifyPropertyChanged(); + settingsChanged = true; Validate(); } } @@ -613,11 +686,22 @@ private void Initialize() { AppSettings appSettings = mainModel.AppSettings; settingsChanged = false; - isNetworkTabSelected = true; + isGeneralTabSelected = true; + isNetworkTabSelected = 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, "один раз в месяц" } + }; + generalSelectedUpdateCheckInterval = generalUpdateCheckIntervalList.First(interval => interval.Key == appSettings.General.UpdateCheck); networkIsOfflineModeOn = appSettings.Network.OfflineMode; networkUseProxy = appSettings.Network.UseProxy; networkProxyAddress = appSettings.Network.ProxyAddress; @@ -644,13 +728,13 @@ private void Initialize() searchIsOpenInNewTabSelected = false; switch (appSettings.Search.OpenDetailsMode) { - case AppSettings.SearchSettings.DetailsMode.NEW_MODAL_WINDOW: + case SearchSettings.DetailsMode.NEW_MODAL_WINDOW: searchIsOpenInModalWindowSelected = true; break; - case AppSettings.SearchSettings.DetailsMode.NEW_NON_MODAL_WINDOW: + case SearchSettings.DetailsMode.NEW_NON_MODAL_WINDOW: searchIsOpenInNonModalWindowSelected = true; break; - case AppSettings.SearchSettings.DetailsMode.NEW_TAB: + case SearchSettings.DetailsMode.NEW_TAB: searchIsOpenInNewTabSelected = true; break; } @@ -697,7 +781,12 @@ private void UpdateValidationState(string propertyName, bool isValid, string err private void OkButtonClick() { - mainModel.AppSettings.Network = new AppSettings.NetworkSettings + mainModel.AppSettings.General = new GeneralSettings + { + Language = GeneralSelectedLanguage.Key, + UpdateCheck = GeneralSelectedUpdateCheckInterval.Key + }; + mainModel.AppSettings.Network = new NetworkSettings { OfflineMode = NetworkIsOfflineModeOn, UseProxy = NetworkUseProxy, @@ -706,7 +795,7 @@ private void OkButtonClick() ProxyUserName = NetworkProxyUserName, ProxyPassword = NetworkProxyPassword }; - mainModel.AppSettings.Mirrors = new AppSettings.MirrorSettings + mainModel.AppSettings.Mirrors = new MirrorSettings { NonFictionBooksMirrorName = ParseDisplayMirrorName(MirrorsSelectedNonFictionBooksMirror), NonFictionCoversMirrorName = ParseDisplayMirrorName(MirrorsSelectedNonFictionCoversMirror), @@ -715,24 +804,24 @@ private void OkButtonClick() FictionCoversMirrorName = ParseDisplayMirrorName(MirrorsSelectedFictionCoversMirror), ArticlesMirrorMirrorName = ParseDisplayMirrorName(MirrorsSelectedArticlesMirror) }; - mainModel.AppSettings.Search = new AppSettings.SearchSettings + mainModel.AppSettings.Search = new SearchSettings { LimitResults = SearchIsLimitResultsOn, MaximumResultCount = SearchMaximumResultCountValue ?? DEFAULT_MAXIMUM_SEARCH_RESULT_COUNT }; if (SearchIsOpenInModalWindowSelected) { - mainModel.AppSettings.Search.OpenDetailsMode = AppSettings.SearchSettings.DetailsMode.NEW_MODAL_WINDOW; + mainModel.AppSettings.Search.OpenDetailsMode = SearchSettings.DetailsMode.NEW_MODAL_WINDOW; } else if (SearchIsOpenInNonModalWindowSelected) { - mainModel.AppSettings.Search.OpenDetailsMode = AppSettings.SearchSettings.DetailsMode.NEW_NON_MODAL_WINDOW; + mainModel.AppSettings.Search.OpenDetailsMode = SearchSettings.DetailsMode.NEW_NON_MODAL_WINDOW; } else if (SearchIsOpenInNewTabSelected) { - mainModel.AppSettings.Search.OpenDetailsMode = AppSettings.SearchSettings.DetailsMode.NEW_TAB; + mainModel.AppSettings.Search.OpenDetailsMode = SearchSettings.DetailsMode.NEW_TAB; } - mainModel.AppSettings.Export = new AppSettings.ExportSettings + mainModel.AppSettings.Export = new ExportSettings { OpenResultsAfterExport = ExportIsOpenResultsAfterExportEnabled, SplitIntoMultipleFiles = ExportIsSplitIntoMultipleFilesEnabled, @@ -752,6 +841,7 @@ private void OkButtonClick() } mainModel.SaveSettings(); mainModel.CreateNewHttpClient(); + mainModel.ConfigureUpdater(); settingsChanged = false; CurrentWindowContext.CloseDialog(true); } diff --git a/LibgenDesktop/ViewModels/SynchronizationWindowViewModel.cs b/LibgenDesktop/ViewModels/SynchronizationWindowViewModel.cs index e380542..a16f5f1 100644 --- a/LibgenDesktop/ViewModels/SynchronizationWindowViewModel.cs +++ b/LibgenDesktop/ViewModels/SynchronizationWindowViewModel.cs @@ -1,5 +1,4 @@ using System; -using System.Collections.ObjectModel; using System.Text; using System.Threading; using LibgenDesktop.Infrastructure; @@ -15,8 +14,8 @@ private enum Step { PREPARATION = 1, CREATING_INDEXES, - DOWNLOADING_BOOKS, - IMPORTING_DATA + LOADING_EXISTING_IDS, + SYNCHRONIZATION } private readonly MainModel mainModel; @@ -24,11 +23,6 @@ private enum Step private readonly Timer elapsedTimer; private bool isInProgress; private string status; - private ObservableCollection logs; - private string resultLogLine; - private bool isResultLogLineVisible; - private string errorLogLine; - private bool isErrorLogLineVisible; private string elapsed; private string cancelButtonText; private bool isCancelButtonVisible; @@ -45,12 +39,15 @@ public SynchronizationWindowViewModel(MainModel mainModel) this.mainModel = mainModel; cancellationTokenSource = new CancellationTokenSource(); elapsedTimer = new Timer(state => UpdateElapsedTime()); + Logs = new ImportLogPanelViewModel(); CancelCommand = new Command(Cancel); CloseCommand = new Command(Close); WindowClosedCommand = new Command(WindowClosed); Initialize(); } + public ImportLogPanelViewModel Logs { get; } + public bool IsInProgress { get @@ -77,71 +74,6 @@ public string Status } } - public ObservableCollection Logs - { - get - { - return logs; - } - set - { - logs = value; - NotifyPropertyChanged(); - } - } - - public string ResultLogLine - { - get - { - return resultLogLine; - } - set - { - resultLogLine = value; - NotifyPropertyChanged(); - } - } - - public bool IsResultLogLineVisible - { - get - { - return isResultLogLineVisible; - } - set - { - isResultLogLineVisible = value; - NotifyPropertyChanged(); - } - } - - public string ErrorLogLine - { - get - { - return errorLogLine; - } - set - { - errorLogLine = value; - NotifyPropertyChanged(); - } - } - - public bool IsErrorLogLineVisible - { - get - { - return isErrorLogLineVisible; - } - set - { - isErrorLogLineVisible = value; - NotifyPropertyChanged(); - } - } - public string Elapsed { get @@ -212,11 +144,11 @@ public bool IsCloseButtonVisible public Command CloseCommand { get; } public Command WindowClosedCommand { get; } - private ProgressLogItemViewModel CurrentLogItem + private ImportLogPanelViewModel.ImportLogItemViewModel CurrentLogItem { get { - return logs[currentStepIndex - 1]; + return Logs.LogItems[currentStepIndex - 1]; } } @@ -224,12 +156,9 @@ private void Initialize() { isInProgress = true; currentStep = Step.PREPARATION; - currentStepIndex = 1; + currentStepIndex = 0; totalSteps = 2; UpdateStatus("Подготовка к синхронизации"); - logs = new ObservableCollection(); - isResultLogLineVisible = false; - isErrorLogLineVisible = false; startDateTime = DateTime.Now; lastElapsedTime = TimeSpan.Zero; elapsedTimer.Change(TimeSpan.Zero, TimeSpan.FromMilliseconds(100)); @@ -248,13 +177,12 @@ private async void Syncrhonize() MainModel.SynchronizationResult synchronizationResult; try { - synchronizationResult = await mainModel.SynchronizeNonFiction(synchronizationProgressHandler, cancellationToken); + synchronizationResult = await mainModel.SynchronizeNonFictionAsync(synchronizationProgressHandler, cancellationToken); } catch (Exception exception) { elapsedTimer.Change(Timeout.InfiniteTimeSpan, Timeout.InfiniteTimeSpan); - ErrorLogLine = "Синхронизация завершилась с ошибками."; - IsErrorLogLineVisible = true; + Logs.ShowErrorLogLine("Синхронизация завершилась с ошибками."); IsInProgress = false; Status = "Синхронизация завершилась с ошибками"; IsCancelButtonVisible = false; @@ -266,13 +194,11 @@ private async void Syncrhonize() switch (synchronizationResult) { case MainModel.SynchronizationResult.COMPLETED: - ResultLogLine = "Синхронизация выполнена успешно."; - IsResultLogLineVisible = true; + Logs.ShowResultLogLine("Синхронизация выполнена успешно."); Status = "Синхронизация завершена"; break; case MainModel.SynchronizationResult.CANCELLED: - ErrorLogLine = "Синхронизация была прервана пользователем."; - IsErrorLogLineVisible = true; + Logs.ShowErrorLogLine("Синхронизация была прервана пользователем."); Status = "Синхронизация прервана"; break; } @@ -291,35 +217,38 @@ private void HandleSynchronizationProgress(object progress) if (currentStep != Step.CREATING_INDEXES) { currentStep = Step.CREATING_INDEXES; + currentStepIndex++; totalSteps++; UpdateStatus("Создание индексов"); - logs.Add(new ProgressLogItemViewModel(currentStepIndex, "Создание недостающих индексов")); + Logs.AddLogItem(currentStepIndex, "Создание недостающих индексов"); } CurrentLogItem.LogLines.Add($"Создается индекс для столбца {createIndexProgress.ColumnName}..."); break; - case JsonApiDownloadProgress jsonApiDownloadProgress: - if (currentStep != Step.DOWNLOADING_BOOKS) + case ImportLoadLibgenIdsProgress importLoadLibgenIdsProgress: + if (currentStep != Step.LOADING_EXISTING_IDS) { - if (currentStep == Step.CREATING_INDEXES) - { - currentStepIndex++; - } - currentStep = Step.DOWNLOADING_BOOKS; - UpdateStatus("Скачивание данных"); - logs.Add(new ProgressLogItemViewModel(currentStepIndex, "Скачивание информации о новых книгах")); + currentStep = Step.LOADING_EXISTING_IDS; + currentStepIndex++; + UpdateStatus("Загрузка идентификаторов"); + Logs.AddLogItem(currentStepIndex, "Загрузка идентификаторов существующих данных"); } - CurrentLogItem.LogLine = $"Скачано книг: {jsonApiDownloadProgress.BooksDownloaded}."; + CurrentLogItem.LogLines.Add($"Загрузка значений столбца LibgenId..."); break; - case ImportObjectsProgress importObjectsProgress: - if (currentStep != Step.IMPORTING_DATA) + case SynchronizationProgress synchronizationProgress: + string secondLogLine = GetSynchronizedBookCountLogLine(synchronizationProgress.ObjectsDownloaded, synchronizationProgress.ObjectsAdded, + synchronizationProgress.ObjectsUpdated); + if (currentStep != Step.SYNCHRONIZATION) { - currentStep = Step.IMPORTING_DATA; + currentStep = Step.SYNCHRONIZATION; currentStepIndex++; - UpdateStatus("Импорт данных"); - logs.Add(new ProgressLogItemViewModel(currentStepIndex, "Импорт данных")); + Logs.AddLogItem(currentStepIndex, "Синхронизация списка книг", "Скачивание информации о новых книгах"); + CurrentLogItem.LogLines.Add(secondLogLine); + UpdateStatus("Синхронизация"); + } + else + { + CurrentLogItem.LogLines[1] = secondLogLine; } - string logLine = GetSynchronizedBookCountLogLine(importObjectsProgress.ObjectsAdded, importObjectsProgress.ObjectsUpdated); - CurrentLogItem.LogLine = logLine; break; } } @@ -359,14 +288,30 @@ private void WindowClosed() private void UpdateStatus(string statusDescription) { - Status = $"Шаг {currentStepIndex} из {totalSteps}. {statusDescription}..."; + StringBuilder statusBuilder = new StringBuilder(); + if (currentStepIndex > 0) + { + statusBuilder.Append("Шаг "); + statusBuilder.Append(currentStepIndex); + statusBuilder.Append(" из "); + statusBuilder.Append(totalSteps); + statusBuilder.Append(". "); + } + statusBuilder.Append(statusDescription); + statusBuilder.Append("..."); + Status = statusBuilder.ToString(); } - private string GetSynchronizedBookCountLogLine(int addedObjectCount, int updatedObjectCount) + private string GetSynchronizedBookCountLogLine(int downloadedObjectCount, int addedObjectCount, int updatedObjectCount) { StringBuilder resultBuilder = new StringBuilder(); - resultBuilder.Append("Добавлено книг: "); - resultBuilder.Append(addedObjectCount.ToFormattedString()); + resultBuilder.Append("Скачано книг: "); + resultBuilder.Append(downloadedObjectCount.ToFormattedString()); + if (addedObjectCount > 0) + { + resultBuilder.Append(", добавлено книг: "); + resultBuilder.Append(addedObjectCount.ToFormattedString()); + } if (updatedObjectCount > 0) { resultBuilder.Append(", обновлено книг: "); diff --git a/LibgenDesktop/Views/ApplicationUpdateWindow.xaml b/LibgenDesktop/Views/ApplicationUpdateWindow.xaml new file mode 100644 index 0000000..eae12d3 --- /dev/null +++ b/LibgenDesktop/Views/ApplicationUpdateWindow.xaml @@ -0,0 +1,41 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/LibgenDesktop/Views/ImportWindow.xaml b/LibgenDesktop/Views/ImportWindow.xaml index 36b19be..40b1012 100644 --- a/LibgenDesktop/Views/ImportWindow.xaml +++ b/LibgenDesktop/Views/ImportWindow.xaml @@ -18,33 +18,7 @@ - - - - - - - - - - - - - - - - - - - - - - - - - + diff --git a/LibgenDesktop/Views/MainWindow.xaml b/LibgenDesktop/Views/MainWindow.xaml index 1138186..1a4c71b 100644 --- a/LibgenDesktop/Views/MainWindow.xaml +++ b/LibgenDesktop/Views/MainWindow.xaml @@ -1,18 +1,16 @@  @@ -74,7 +72,7 @@ + Visibility="{Binding IsNewTabButtonVisible, Converter={StaticResource booleanToHiddenConverter}}" /> diff --git a/LibgenDesktop/Views/MainWindow.xaml.cs b/LibgenDesktop/Views/MainWindow.xaml.cs index 1e4204f..3455f19 100644 --- a/LibgenDesktop/Views/MainWindow.xaml.cs +++ b/LibgenDesktop/Views/MainWindow.xaml.cs @@ -1,10 +1,16 @@ -namespace LibgenDesktop.Views +using LibgenDesktop.Infrastructure; + +namespace LibgenDesktop.Views { - public partial class MainWindow + public partial class MainWindow : IEventListener { public MainWindow() { InitializeComponent(); } + + public void OnViewModelEvent(ViewModelEvent viewModelEvent) + { + } } } diff --git a/LibgenDesktop/Views/SettingsWindow.xaml b/LibgenDesktop/Views/SettingsWindow.xaml index 226c335..a7ba2dd 100644 --- a/LibgenDesktop/Views/SettingsWindow.xaml +++ b/LibgenDesktop/Views/SettingsWindow.xaml @@ -19,6 +19,9 @@ + + + @@ -35,6 +38,24 @@ + + + + + + + + + + + + + + + diff --git a/LibgenDesktop/Views/Styles/ApplicationUpdateWindowStyles.xaml b/LibgenDesktop/Views/Styles/ApplicationUpdateWindowStyles.xaml new file mode 100644 index 0000000..ae66aec --- /dev/null +++ b/LibgenDesktop/Views/Styles/ApplicationUpdateWindowStyles.xaml @@ -0,0 +1,25 @@ + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/LibgenDesktop/Views/Styles/ImportWindowStyles.xaml b/LibgenDesktop/Views/Styles/ImportWindowStyles.xaml index 67850ad..2c724d3 100644 --- a/LibgenDesktop/Views/Styles/ImportWindowStyles.xaml +++ b/LibgenDesktop/Views/Styles/ImportWindowStyles.xaml @@ -17,29 +17,6 @@ - - - - - - - diff --git a/LibgenDesktop/Views/Styles/SettingsWindowStyles.xaml b/LibgenDesktop/Views/Styles/SettingsWindowStyles.xaml index 5edcdbb..394a45a 100644 --- a/LibgenDesktop/Views/Styles/SettingsWindowStyles.xaml +++ b/LibgenDesktop/Views/Styles/SettingsWindowStyles.xaml @@ -33,10 +33,19 @@ + + - - + @@ -19,5 +22,26 @@ + + + + + + + + + + \ No newline at end of file diff --git a/LibgenDesktop/Views/Styles/WindowStyles.xaml b/LibgenDesktop/Views/Styles/WindowStyles.xaml index 4993a3a..72e5647 100644 --- a/LibgenDesktop/Views/Styles/WindowStyles.xaml +++ b/LibgenDesktop/Views/Styles/WindowStyles.xaml @@ -81,6 +81,12 @@ +