diff --git a/assets/localization/en.json b/assets/localization/en.json index 675cc44c20..4517499d4d 100644 --- a/assets/localization/en.json +++ b/assets/localization/en.json @@ -1,6 +1,14 @@ { "%about_versionLabel_format%": "Version: {version}", "%about_licenseLabel_format%": "License: {license}", + "%downloadResources_errorRegistrationInvalid%": "User registration is not valid. Cannot retrieve resources from DBL.", + "%downloadResources_errorInstallResource_resourceNotFound%": "Resource not available from DBL", + "%downloadResources_errorInstallResource_resourceAlreadyInstalled%": "Resource is already installed and up to date. Installation skipped.", + "%downloadResources_errorInstallResource_installationFailed%": "Resource cannot be found after attempted installation. Installation failed.", + "%downloadResources_errorUninstallResource_resourceNotFound%": "Resource not found on list of DBL resources.", + "%downloadResources_errorUninstallResource_resourceNotInstalled%": "Resource is not currently installed, so it can't be removed.", + "%downloadResources_errorUninstallResource_localResourceNotFound%": "Resource cannot be located, so it can't be removed.", + "%downloadResources_errorUninstallResource_localResourceStillPresent%": "Resource is still present. Removing failed.", "%downloadUpdateProjectTab_aria_downloadable%": "downloadable projects", "%downloadUpdateProjectTab_aria_downloaded%": "downloaded projects", "%downloadUpdateProjectTab_button_delete%": "Delete", diff --git a/c-sharp/Program.cs b/c-sharp/Program.cs index 62ac75cc89..7e4cfce959 100644 --- a/c-sharp/Program.cs +++ b/c-sharp/Program.cs @@ -1,6 +1,7 @@ using Paranext.DataProvider.Checks; using Paranext.DataProvider.NetworkObjects; using Paranext.DataProvider.Projects; +using Paranext.DataProvider.Projects.DigitalBibleLibrary; using Paranext.DataProvider.Services; using Paranext.DataProvider.Users; using Paratext.Data; @@ -32,10 +33,12 @@ public static async Task Main() var paratextFactory = new ParatextProjectDataProviderFactory(papi, paratextProjects); var checkRunner = new CheckRunner(papi); + var dblResources = new DblResourcesDataProvider(papi); var paratextRegistrationService = new ParatextRegistrationService(papi); await Task.WhenAll( paratextFactory.InitializeAsync(), checkRunner.RegisterDataProviderAsync(), + dblResources.RegisterDataProviderAsync(), paratextRegistrationService.InitializeAsync() ); diff --git a/c-sharp/Projects/DigitalBibleLibrary/DblDownloadableDataProvider.cs b/c-sharp/Projects/DigitalBibleLibrary/DblDownloadableDataProvider.cs new file mode 100644 index 0000000000..2c0cd5f998 --- /dev/null +++ b/c-sharp/Projects/DigitalBibleLibrary/DblDownloadableDataProvider.cs @@ -0,0 +1,228 @@ +using System.Text.Json; +using Paranext.DataProvider.Services; +using Paratext.Data; +using Paratext.Data.Archiving; +using Paratext.Data.Users; + +namespace Paranext.DataProvider.Projects.DigitalBibleLibrary; + +/// +/// Data provider that can install, update and uninstall DBL (Digital Bible Library) resources +/// +internal class DblResourcesDataProvider(PapiClient papiClient) + : NetworkObjects.DataProvider("paratextBibleDownloadResources.dblResourcesProvider", papiClient) +{ + #region Internal classes + + private class DblResourceData( + string DblEntryUid, + string DisplayName, + string FullName, + string BestLanguageName, + long Size, + bool Installed, + bool UpdateAvailable + ) + { + public string DblEntryUid { get; set; } = DblEntryUid; + public string DisplayName { get; set; } = DisplayName; + public string FullName { get; set; } = FullName; + public string BestLanguageName { get; set; } = BestLanguageName; + public long Size { get; set; } = Size; + public bool Installed { get; set; } = Installed; + public bool UpdateAvailable { get; set; } = UpdateAvailable; + } + + #endregion + + #region Consts and member variables + + public const string DBL_RESOURCES = "DblResources"; + + private List _resources = []; + + #endregion + + #region DataProvider methods + + protected override List<(string functionName, Delegate function)> GetFunctions() + { + return + [ + ("getDblResources", GetDblResources), + ("installDblResource", InstallDblResource), + ("uninstallDblResource", UninstallDblResource), + ]; + } + + protected override Task StartDataProviderAsync() + { + return Task.CompletedTask; + } + + #endregion + + #region Private properties and methods + + /// + /// Fetch list DBL resources + /// + /// + /// A list of all available resources on the DBL, along with information about their + /// installation status on the local machine + /// + private void FetchAvailableDBLResources() + { + _resources = InstallableDBLResource.GetInstallableDBLResources( + RegistrationInfo.DefaultUser, + new DBLRESTClientFactory(), + new DblProjectDeleter(), + new DblMigrationOperations(), + new DblResourcePasswordProvider() + ); + } + + /// + /// Check user registration and, if registration is valid, return a list of information about + /// available DBL resources + /// + /// + /// A list with some information about all available resources on the DBL, for the purpose of + /// presenting the resources and their installation status on the front-end + /// + private List GetDblResources(JsonElement _ignore) + { + if (!RegistrationInfo.DefaultUser.IsValid) + { + throw new Exception( + LocalizationService.GetLocalizedString( + PapiClient, + "%downloadResources_errorRegistrationInvalid%", + $"User registration is not valid. Cannot retrieve resources from DBL." + ) + ); + } + + FetchAvailableDBLResources(); + + return _resources + .Select(resource => new DblResourceData( + resource.DBLEntryUid.Id, + resource.DisplayName, + resource.FullName, + resource.BestLanguageName, + resource.Size, + resource.Installed, + resource.IsNewerThanCurrentlyInstalled() + )) + .ToList(); + } + + private void FindResource( + string dblEntryUid, + string messageToThrowIfNotFound, + out InstallableResource resource + ) + { + resource = + _resources?.FirstOrDefault(r => r.DBLEntryUid.Id == dblEntryUid) + ?? throw new Exception(messageToThrowIfNotFound); + } + + /// + /// Try to install DBL resource with specified DBL id + /// + private void InstallDblResource(string DBLEntryUid) + { + FindResource( + DBLEntryUid, + LocalizationService.GetLocalizedString( + PapiClient, + "%downloadResources_errorInstallResource_resourceNotFound%", + $"Resource not available from DBL." + ), + out var installableResource + ); + + if (installableResource.Installed && !installableResource.IsNewerThanCurrentlyInstalled()) + throw new Exception( + LocalizationService.GetLocalizedString( + PapiClient, + "%downloadResources_errorInstallResource_resourceAlreadyInstalled%", + $"Resource is already installed and up to date. Installation skipped." + ) + ); + + // Note that we don't get any info telling if the installation succeeded or failed + installableResource.Install(); + + ScrTextCollection.RefreshScrTexts(); + + if (!ScrTextCollection.IsPresent(installableResource.ExistingScrText)) + throw new Exception( + LocalizationService.GetLocalizedString( + PapiClient, + "%downloadResources_errorInstallResource_installationFailed%", + $"Resource cannot be found after attempted installation. Installation failed." + ) + ); + + SendDataUpdateEvent(DBL_RESOURCES, "DBL resources data updated"); + } + + /// + /// Try to uninstall DBL resource with specified DBL id + /// + private void UninstallDblResource(string DBLEntryUid) + { + FindResource( + DBLEntryUid, + LocalizationService.GetLocalizedString( + PapiClient, + "%downloadResources_errorUninstallResource_resourceNotFound%", + $"Resource not found on list of DBL resources." + ), + out var installableResource + ); + + if (!installableResource.Installed) + throw new Exception( + LocalizationService.GetLocalizedString( + PapiClient, + "%downloadResources_errorUninstallResource_resourceNotInstalled%", + $"Resource is not currently installed, so it can't be removed." + ) + ); + + var objectToBeDeleted = installableResource.ExistingScrText; + + var isPresent = ScrTextCollection.IsPresent(objectToBeDeleted); + if (!isPresent) + throw new Exception( + LocalizationService.GetLocalizedString( + PapiClient, + "%downloadResources_errorUninstallResource_localResourceNotFound%", + $"Resource cannot be located, so it can't be removed." + ) + ); + + // Note that we don't get any info telling if uninstalling succeeded or failed + ScrTextCollection.DeleteProject(objectToBeDeleted); + + ScrTextCollection.RefreshScrTexts(); + + isPresent = ScrTextCollection.IsPresent(objectToBeDeleted); + if (isPresent) + throw new Exception( + LocalizationService.GetLocalizedString( + PapiClient, + "%downloadResources_errorUninstallResource_localResourceStillPresent%", + $"Resource is still present. Removing failed." + ) + ); + + SendDataUpdateEvent(DBL_RESOURCES, "DBL resources data updated"); + } + + #endregion +} diff --git a/c-sharp/Projects/DigitalBibleLibrary/DblMigrationOperations.cs b/c-sharp/Projects/DigitalBibleLibrary/DblMigrationOperations.cs new file mode 100644 index 0000000000..4d2eaf6796 --- /dev/null +++ b/c-sharp/Projects/DigitalBibleLibrary/DblMigrationOperations.cs @@ -0,0 +1,36 @@ +using Paratext.Data; +using Paratext.Data.Archiving; +using Paratext.Data.Languages; + +namespace Paranext.DataProvider.Projects.DigitalBibleLibrary; + +public class DblMigrationOperations : IMigrationOperations +{ + /// + /// See `Paratext.Migration.PTMigration.Migrate` for steps involved in migrating data + /// + public UnsupportedReason MigrateProjectIfNeeded(ScrText scrText) + { + return scrText.NeedsMigration + ? UnsupportedReason.CannotUpgrade + : UnsupportedReason.Supported; + } + + /// + /// Adapted from `Paratext.Migration.MigrateLanguage` + /// + public LanguageId DetermineBestLangIdToUseForResource( + string ldmlLanguageId, + string dblLanguageId + ) + { + LanguageId ethnologueDblLanguageId = LanguageId.FromEthnologueCode(dblLanguageId); + if (string.IsNullOrEmpty(ldmlLanguageId)) + return ethnologueDblLanguageId; + + LanguageId ethnologueLdmlLanguageId = LanguageId.FromEthnologueCode(ldmlLanguageId); + if (ethnologueLdmlLanguageId.Code == ethnologueDblLanguageId.Code) + return ethnologueLdmlLanguageId; + return ethnologueDblLanguageId; + } +} diff --git a/c-sharp/Projects/DigitalBibleLibrary/DblProjectDeleter.cs b/c-sharp/Projects/DigitalBibleLibrary/DblProjectDeleter.cs new file mode 100644 index 0000000000..21af2a9b0b --- /dev/null +++ b/c-sharp/Projects/DigitalBibleLibrary/DblProjectDeleter.cs @@ -0,0 +1,20 @@ +using Paratext.Data; +using Paratext.Data.Archiving; +using Paratext.Data.ProjectComments; +using Paratext.Data.Repository; + +namespace Paranext.DataProvider.Projects.DigitalBibleLibrary; + +/// +/// Adapted from `Paratext.ProjectMenu.DeleteProjectForm.DeleteProject` +/// +public class DblProjectDeleter : IProjectDeleter +{ + public void DeleteProject(ScrText scrText) + { + CommentManager.RemoveCommentManager(scrText); + VersioningManager.RemoveVersionedText(scrText); + if (!scrText.Settings.IsMarbleResource) + ScrTextCollection.DeleteProject(scrText); + } +} diff --git a/cspell.json b/cspell.json index 02641c07ed..7f0eeeacb0 100644 --- a/cspell.json +++ b/cspell.json @@ -37,6 +37,7 @@ "biblionexus", "camelcase", "consts", + "deleter", "deuterocanon", "dockbox", "electronmon", diff --git a/src/extension-host/data/menu.data.json b/src/extension-host/data/menu.data.json index 9f104526f4..f5341c74b4 100644 --- a/src/extension-host/data/menu.data.json +++ b/src/extension-host/data/menu.data.json @@ -26,13 +26,6 @@ "platform.helpMisc": { "column": "platform.help", "order": 2 } }, "items": [ - { - "label": "%mainMenu_downloadInstallResources%", - "localizeNotes": "Application main menu > Project > Download/Install Resources", - "group": "platform.projectResources", - "order": 1, - "command": "platform.downloadAndInstallResources" - }, { "label": "%mainMenu_settings%", "localizeNotes": "Application main menu > Project > Settings", diff --git a/src/renderer/components/platform-bible-menu.data.ts b/src/renderer/components/platform-bible-menu.data.ts index ef0842632f..7b37b6f860 100644 --- a/src/renderer/components/platform-bible-menu.data.ts +++ b/src/renderer/components/platform-bible-menu.data.ts @@ -25,13 +25,6 @@ const supportAndDevelopmentMenuLayout: LocalizedMainMenu = { 'paratext.helpSubgroup': { menuItem: 'paratext.helpRoot', order: 1 }, }, items: [ - { - label: 'Download/Install Resources', - localizeNotes: 'Main application menu > Paratext column > Download/Install Resources', - group: 'paratext.sendReceive', - order: 1, - command: 'platform.downloadAndInstallResources', - }, { label: 'Open Project...', tooltip: 'Open project or resource text(s)',