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)',