diff --git a/backend/FwLite/FwLiteShared/Services/FwLiteProvider.cs b/backend/FwLite/FwLiteShared/Services/FwLiteProvider.cs index 9a2b69b58b..a24d7511e6 100644 --- a/backend/FwLite/FwLiteShared/Services/FwLiteProvider.cs +++ b/backend/FwLite/FwLiteShared/Services/FwLiteProvider.cs @@ -46,6 +46,7 @@ IServiceProvider services DotnetService.ProjectServicesProvider => typeof(ProjectServicesProvider), DotnetService.HistoryService => typeof(HistoryServiceJsInvokable), DotnetService.SyncService => typeof(SyncServiceJsInvokable), + DotnetService.MediaFilesService => typeof(MediaFilesServiceJsInvokable), DotnetService.AppLauncher => typeof(IAppLauncher), DotnetService.TroubleshootingService => typeof(ITroubleshootingService), DotnetService.TestingService => typeof(TestingService), @@ -103,6 +104,7 @@ public enum DotnetService ProjectServicesProvider, HistoryService, SyncService, + MediaFilesService, AppLauncher, TroubleshootingService, TestingService, diff --git a/backend/FwLite/FwLiteShared/Services/MediaFilesServiceJsInvokable.cs b/backend/FwLite/FwLiteShared/Services/MediaFilesServiceJsInvokable.cs new file mode 100644 index 0000000000..841ba94e39 --- /dev/null +++ b/backend/FwLite/FwLiteShared/Services/MediaFilesServiceJsInvokable.cs @@ -0,0 +1,57 @@ +using Microsoft.JSInterop; +using LcmCrdt.MediaServer; +using SIL.Harmony.Resource; +using MiniLcm.Media; + +namespace FwLiteShared.Services; + +public class MediaFilesServiceJsInvokable(LcmMediaService mediaService) +{ + [JSInvokable] + public async Task AllResources() + { + return await mediaService.AllResources(); + } + + [JSInvokable] + public async Task ResourcesPendingDownload() + { + return await mediaService.ResourcesPendingDownload(); + } + + [JSInvokable] + public async Task ResourcesPendingUpload() + { + return await mediaService.ResourcesPendingUpload(); + } + + [JSInvokable] + public async Task DownloadAllResources() + { + await mediaService.DownloadAllResources(); + } + + [JSInvokable] + public async Task UploadAllResources() + { + await mediaService.UploadAllResources(); + } + + [JSInvokable] + public async Task DownloadResources(IEnumerable resourceIds) + { + await mediaService.DownloadResources(resourceIds); + } + + [JSInvokable] + public async Task UploadResources(IEnumerable resourceIds) + { + await mediaService.UploadResources(resourceIds); + } + + [JSInvokable] + public async Task GetFileMetadata(Guid fileId) + { + return await mediaService.GetFileMetadata(fileId); + } +} diff --git a/backend/FwLite/FwLiteShared/Services/ProjectServicesProvider.cs b/backend/FwLite/FwLiteShared/Services/ProjectServicesProvider.cs index 71b6ee85f2..57a3608943 100644 --- a/backend/FwLite/FwLiteShared/Services/ProjectServicesProvider.cs +++ b/backend/FwLite/FwLiteShared/Services/ProjectServicesProvider.cs @@ -69,7 +69,8 @@ public Task OpenCrdtProject(string code) scope.Server = server; scope.SetCrdtServices( ActivatorUtilities.CreateInstance(scopedServices), - ActivatorUtilities.CreateInstance(scopedServices) + ActivatorUtilities.CreateInstance(scopedServices), + ActivatorUtilities.CreateInstance(scopedServices) ); _projectScopes.TryAdd(scope, scope); return scope; @@ -173,10 +174,12 @@ public ProjectScope(AsyncServiceScope serviceScope, public void SetCrdtServices( HistoryServiceJsInvokable historyService, - SyncServiceJsInvokable syncService) + SyncServiceJsInvokable syncService, + MediaFilesServiceJsInvokable mediaFilesService) { HistoryService = DotNetObjectReference.Create(historyService); SyncService = DotNetObjectReference.Create(syncService); + MediaFilesService = DotNetObjectReference.Create(mediaFilesService); } public ValueTask CleanupAsync() @@ -191,4 +194,5 @@ public ValueTask CleanupAsync() public DotNetObjectReference MiniLcm { get; set; } public DotNetObjectReference? HistoryService { get; set; } public DotNetObjectReference? SyncService { get; set; } + public DotNetObjectReference? MediaFilesService { get; set; } } diff --git a/backend/FwLite/FwLiteShared/TypeGen/ReinforcedFwLiteTypingConfig.cs b/backend/FwLite/FwLiteShared/TypeGen/ReinforcedFwLiteTypingConfig.cs index 77d482dab7..dd26521024 100644 --- a/backend/FwLite/FwLiteShared/TypeGen/ReinforcedFwLiteTypingConfig.cs +++ b/backend/FwLite/FwLiteShared/TypeGen/ReinforcedFwLiteTypingConfig.cs @@ -21,6 +21,7 @@ using SIL.Harmony; using SIL.Harmony.Core; using SIL.Harmony.Db; +using SIL.Harmony.Resource; using System.Runtime.CompilerServices; using FwLiteShared.AppUpdate; using FwLiteShared.Sync; @@ -84,7 +85,10 @@ private static void ConfigureMiniLcmTypes(ConfigurationBuilder builder) typeof(RichTextObjectData), typeof(MediaFile), - typeof(LcmFileMetadata) + typeof(LcmFileMetadata), + typeof(HarmonyResource), + typeof(RemoteResource), + typeof(LocalResource) ], exportBuilder => exportBuilder.WithPublicNonStaticProperties(exportBuilder => { @@ -112,6 +116,10 @@ private static void ConfigureMiniLcmTypes(ConfigurationBuilder builder) .FlattenHierarchy() .WithPublicProperties() .WithPublicMethods(b => b.AlwaysReturnPromise().OnlyJsInvokable()); + builder.ExportAsInterface() + // .WithPublicMethods(b => b.AlwaysReturnPromise().OnlyJsInvokable()); + .WithPublicMethods(); + // TODO: Does MediaFilesServiceJsInvokable need the AlwaysReturnPromise().OnlyJsInvokable() setup that MiniLcmJsInvokable needs? builder.ExportAsEnum().UseString(); builder.ExportAsInterfaces([ typeof(QueryOptions), diff --git a/backend/FwLite/LcmCrdt/MediaServer/IMediaServerClient.cs b/backend/FwLite/LcmCrdt/MediaServer/IMediaServerClient.cs index 10443aea5c..fe19e2a543 100644 --- a/backend/FwLite/LcmCrdt/MediaServer/IMediaServerClient.cs +++ b/backend/FwLite/LcmCrdt/MediaServer/IMediaServerClient.cs @@ -1,3 +1,4 @@ +using MiniLcm.Media; using Refit; namespace LcmCrdt.MediaServer; @@ -8,6 +9,9 @@ public interface IMediaServerClient [Get("/api/media/{fileId}")] Task DownloadFile(Guid fileId); + [Get("/api/media/metadata/{fileId}")] + Task GetFileMetadata(Guid fileId); + [Post("/api/media")] [Multipart] Task UploadFile(MultipartItem file, diff --git a/backend/FwLite/LcmCrdt/MediaServer/LcmMediaService.cs b/backend/FwLite/LcmCrdt/MediaServer/LcmMediaService.cs index d4cec3d0e0..a2588d3836 100644 --- a/backend/FwLite/LcmCrdt/MediaServer/LcmMediaService.cs +++ b/backend/FwLite/LcmCrdt/MediaServer/LcmMediaService.cs @@ -7,6 +7,7 @@ using LcmCrdt.RemoteSync; using Microsoft.Extensions.Logging; using MiniLcm.Media; +using System.Net.Http.Json; namespace LcmCrdt.MediaServer; @@ -24,6 +25,16 @@ public async Task AllResources() return await resourceService.AllResources(); } + public async Task ResourcesPendingDownload() + { + return await resourceService.ListResourcesPendingDownload(); + } + + public async Task ResourcesPendingUpload() + { + return await resourceService.ListResourcesPendingUpload(); + } + /// /// should only be used in fw-headless for files which already exist in the lexbox db /// @@ -42,6 +53,74 @@ public async Task DeleteResource(Guid fileId) await resourceService.DeleteResource(currentProjectService.ProjectData.ClientId, fileId); } + public async Task DownloadResourceIfNeeded(Guid fileId) + { + var localResource = await resourceService.GetLocalResource(fileId); + if (localResource is null) + { + var connectionStatus = await httpClientProvider.ConnectionStatus(); + if (connectionStatus == ConnectionStatus.Online) + { + return await resourceService.DownloadResource(fileId, this); + } + } + return localResource; + } + + public async Task DownloadAllResources() + { + var connectionStatus = await httpClientProvider.ConnectionStatus(); + if (connectionStatus == ConnectionStatus.Online) + { + var resources = await ResourcesPendingDownload(); + foreach (var resource in resources) + { + if (resource.RemoteId is null) continue; + await resourceService.DownloadResource(resource.Id, this); + } + } + // TODO: Gracefully handle other connection statuses, e.g. "not logged in" + } + + public async Task UploadAllResources() + { + await UploadResources((await ResourcesPendingUpload()).Select(r => r.Id)); + } + + public async Task DownloadResources(IEnumerable resourceIds) + { + foreach (var resourceId in resourceIds) + { + await DownloadResourceIfNeeded(resourceId); + } + } + + public async Task DownloadResources(IEnumerable resources) + { + foreach (var resource in resources) + { + await DownloadResourceIfNeeded(resource.Id); + } + } + + public async Task UploadResources(IEnumerable resourceIds) + { + var clientId = currentProjectService.ProjectData.ClientId; + foreach (var resourceId in resourceIds) + { + await resourceService.UploadPendingResource(resourceId, clientId, this); + } + } + + public async Task UploadResources(IEnumerable resources) + { + var clientId = currentProjectService.ProjectData.ClientId; + foreach (var resource in resources) + { + await resourceService.UploadPendingResource(resource, clientId, this); + } + } + /// /// return a stream for the file, if it's not cached locally, it will be downloaded /// @@ -50,25 +129,44 @@ public async Task DeleteResource(Guid fileId) /// public async Task GetFileStream(Guid fileId) { - var localResource = await resourceService.GetLocalResource(fileId); + var localResource = await DownloadResourceIfNeeded(fileId); if (localResource is null) { var connectionStatus = await httpClientProvider.ConnectionStatus(); if (connectionStatus == ConnectionStatus.Online) { - localResource = await resourceService.DownloadResource(fileId, this); + // Try again, maybe earlier failure was a blip + localResource = await DownloadResourceIfNeeded(fileId); } else { return new ReadFileResponse(ReadFileResult.Offline); } } - //todo, consider trying to download the file again, maybe the cache was cleared + if (localResource is null || !File.Exists(localResource.LocalPath)) + { + // One more attempt to download again, maybe the cache was cleared + localResource = await DownloadResourceIfNeeded(fileId); + // If still null then connection is offline or unreliable enough to consider as offline + if (localResource is null) return new ReadFileResponse(ReadFileResult.Offline); + } + // If still can't find local path then this is where we give up if (!File.Exists(localResource.LocalPath)) throw new FileNotFoundException("Unable to find the file with Id" + fileId, localResource.LocalPath); return new(File.OpenRead(localResource.LocalPath), Path.GetFileName(localResource.LocalPath)); } + public async Task GetFileMetadata(Guid fileId) + { + var mediaClient = await MediaServerClient(); + var metadata = await mediaClient.GetFileMetadata(fileId); + if (metadata is null) + { + throw new Exception($"Failed to retrieve metadata for file {fileId}"); + } + return metadata; + } + private async Task<(Stream? stream, string? filename)> RequestMediaFile(Guid fileId) { var mediaClient = await MediaServerClient(); diff --git a/frontend/viewer/src/DotnetProjectView.svelte b/frontend/viewer/src/DotnetProjectView.svelte index 51f2f84d97..3e372263ef 100644 --- a/frontend/viewer/src/DotnetProjectView.svelte +++ b/frontend/viewer/src/DotnetProjectView.svelte @@ -13,6 +13,7 @@ } from '$lib/dotnet-types/generated-types/FwLiteShared/Services/ISyncServiceJsInvokable'; import ProjectLoader from './ProjectLoader.svelte'; import {initProjectContext} from '$lib/project-context.svelte'; + import type {IMediaFilesServiceJsInvokable} from '$lib/dotnet-types/generated-types/FwLiteShared/Services/IMediaFilesServiceJsInvokable'; const projectServicesProvider = useProjectServicesProvider(); const projectContext = initProjectContext(); @@ -52,11 +53,16 @@ if (projectScope.syncService) { syncService = wrapInProxy(projectScope.syncService, DotnetService.SyncService); } + let mediaFilesService: IMediaFilesServiceJsInvokable | undefined = undefined; + if (projectScope.mediaFilesService) { + mediaFilesService = wrapInProxy(projectScope.mediaFilesService, DotnetService.MediaFilesService); + } const api = wrapInProxy(projectScope.miniLcm, DotnetService.MiniLcmApi); projectContext.setup({ api, historyService, syncService, + mediaFilesService, projectName, projectCode: code, projectType, diff --git a/frontend/viewer/src/lib/components/ui/checkbox/checkbox-group.svelte b/frontend/viewer/src/lib/components/ui/checkbox/checkbox-group.svelte new file mode 100644 index 0000000000..823ff5cebd --- /dev/null +++ b/frontend/viewer/src/lib/components/ui/checkbox/checkbox-group.svelte @@ -0,0 +1,10 @@ + + + diff --git a/frontend/viewer/src/lib/components/ui/checkbox/index.ts b/frontend/viewer/src/lib/components/ui/checkbox/index.ts index 30198775c8..b128c38cdb 100644 --- a/frontend/viewer/src/lib/components/ui/checkbox/index.ts +++ b/frontend/viewer/src/lib/components/ui/checkbox/index.ts @@ -1,5 +1,6 @@ import Root from './checkbox.svelte'; +import Group from './checkbox-group.svelte'; export { // - Root as Checkbox, Root + Root as Checkbox, Group as CheckboxGroup, Root }; diff --git a/frontend/viewer/src/lib/dotnet-types/generated-types/FwLiteShared/Services/DotnetService.ts b/frontend/viewer/src/lib/dotnet-types/generated-types/FwLiteShared/Services/DotnetService.ts index 53875f57b9..e3840f2f15 100644 --- a/frontend/viewer/src/lib/dotnet-types/generated-types/FwLiteShared/Services/DotnetService.ts +++ b/frontend/viewer/src/lib/dotnet-types/generated-types/FwLiteShared/Services/DotnetService.ts @@ -12,6 +12,7 @@ export enum DotnetService { ProjectServicesProvider = "ProjectServicesProvider", HistoryService = "HistoryService", SyncService = "SyncService", + MediaFilesService = "MediaFilesService", AppLauncher = "AppLauncher", TroubleshootingService = "TroubleshootingService", TestingService = "TestingService", diff --git a/frontend/viewer/src/lib/dotnet-types/generated-types/FwLiteShared/Services/IMediaFilesServiceJsInvokable.ts b/frontend/viewer/src/lib/dotnet-types/generated-types/FwLiteShared/Services/IMediaFilesServiceJsInvokable.ts new file mode 100644 index 0000000000..0f4fc775e2 --- /dev/null +++ b/frontend/viewer/src/lib/dotnet-types/generated-types/FwLiteShared/Services/IMediaFilesServiceJsInvokable.ts @@ -0,0 +1,22 @@ +/* eslint-disable */ +// This code was generated by a Reinforced.Typings tool. +// Changes to this file may cause incorrect behavior and will be lost if +// the code is regenerated. + +import type {IHarmonyResource} from '../../SIL/Harmony/Resource/IHarmonyResource'; +import type {IRemoteResource} from '../../SIL/Harmony/Resource/IRemoteResource'; +import type {ILocalResource} from '../../SIL/Harmony/Resource/ILocalResource'; +import type {ILcmFileMetadata} from '../../MiniLcm/Media/ILcmFileMetadata'; + +export interface IMediaFilesServiceJsInvokable +{ + allResources() : Promise; + resourcesPendingDownload() : Promise; + resourcesPendingUpload() : Promise; + downloadAllResources() : Promise; + uploadAllResources() : Promise; + downloadResources(resourceIds: string[]) : Promise; + uploadResources(resourceIds: string[]) : Promise; + getFileMetadata(fileId: string) : Promise; +} +/* eslint-enable */ diff --git a/frontend/viewer/src/lib/dotnet-types/generated-types/FwLiteShared/Services/IProjectScope.ts b/frontend/viewer/src/lib/dotnet-types/generated-types/FwLiteShared/Services/IProjectScope.ts index 7cd53912f9..a560a59238 100644 --- a/frontend/viewer/src/lib/dotnet-types/generated-types/FwLiteShared/Services/IProjectScope.ts +++ b/frontend/viewer/src/lib/dotnet-types/generated-types/FwLiteShared/Services/IProjectScope.ts @@ -16,5 +16,6 @@ export interface IProjectScope miniLcm: DotNet.DotNetObject; historyService?: DotNet.DotNetObject; syncService?: DotNet.DotNetObject; + mediaFilesService?: DotNet.DotNetObject; } /* eslint-enable */ diff --git a/frontend/viewer/src/lib/dotnet-types/generated-types/SIL/Harmony/Resource/IHarmonyResource.ts b/frontend/viewer/src/lib/dotnet-types/generated-types/SIL/Harmony/Resource/IHarmonyResource.ts new file mode 100644 index 0000000000..db368c8bdb --- /dev/null +++ b/frontend/viewer/src/lib/dotnet-types/generated-types/SIL/Harmony/Resource/IHarmonyResource.ts @@ -0,0 +1,14 @@ +/* eslint-disable */ +// This code was generated by a Reinforced.Typings tool. +// Changes to this file may cause incorrect behavior and will be lost if +// the code is regenerated. + +export interface IHarmonyResource +{ + id: string; + remoteId?: string; + localPath?: string; + local: boolean; + remote: boolean; +} +/* eslint-enable */ diff --git a/frontend/viewer/src/lib/dotnet-types/generated-types/SIL/Harmony/Resource/ILocalResource.ts b/frontend/viewer/src/lib/dotnet-types/generated-types/SIL/Harmony/Resource/ILocalResource.ts new file mode 100644 index 0000000000..3dade6d918 --- /dev/null +++ b/frontend/viewer/src/lib/dotnet-types/generated-types/SIL/Harmony/Resource/ILocalResource.ts @@ -0,0 +1,11 @@ +/* eslint-disable */ +// This code was generated by a Reinforced.Typings tool. +// Changes to this file may cause incorrect behavior and will be lost if +// the code is regenerated. + +export interface ILocalResource +{ + id: string; + localPath: string; +} +/* eslint-enable */ diff --git a/frontend/viewer/src/lib/project-context.svelte.ts b/frontend/viewer/src/lib/project-context.svelte.ts index 60c787e17c..5de3e474bb 100644 --- a/frontend/viewer/src/lib/project-context.svelte.ts +++ b/frontend/viewer/src/lib/project-context.svelte.ts @@ -6,6 +6,9 @@ import type { import type { ISyncServiceJsInvokable } from '$lib/dotnet-types/generated-types/FwLiteShared/Services/ISyncServiceJsInvokable'; +import type { + IMediaFilesServiceJsInvokable +} from '$lib/dotnet-types/generated-types/FwLiteShared/Services/IMediaFilesServiceJsInvokable'; import {resource, type ResourceOptions, type ResourceReturn} from 'runed'; import {SvelteMap} from 'svelte/reactivity'; import type {IProjectData} from '$lib/dotnet-types/generated-types/LcmCrdt/IProjectData'; @@ -18,6 +21,7 @@ interface ProjectContextSetup { api: IMiniLcmJsInvokable; historyService?: IHistoryServiceJsInvokable; syncService?: ISyncServiceJsInvokable; + mediaFilesService?: IMediaFilesServiceJsInvokable; projectName: string; projectCode: string; projectType?: 'crdt' | 'fwdata'; @@ -43,6 +47,7 @@ export class ProjectContext { #projectData = $state(); #historyService: IHistoryServiceJsInvokable | undefined = $state(undefined); #syncService: ISyncServiceJsInvokable | undefined = $state(undefined); + #mediaFilesService: IMediaFilesServiceJsInvokable | undefined = $state(undefined); #paratext = $state(false); #features = resource(() => this.#api, (api) => { if (!api) return Promise.resolve({} satisfies IMiniLcmFeatures); @@ -81,6 +86,9 @@ export class ProjectContext { public get syncService(): ISyncServiceJsInvokable | undefined { return this.#syncService; } + public get mediaFilesService(): IMediaFilesServiceJsInvokable | undefined { + return this.#mediaFilesService; + } public get inParatext(): boolean { return this.#paratext; } @@ -93,6 +101,7 @@ export class ProjectContext { this.#api = args.api; this.#historyService = args.historyService; this.#syncService = args.syncService; + this.#mediaFilesService = args.mediaFilesService; this.#projectName = args.projectName; this.#projectCode = args.projectCode; this.#projectType = args.projectType; diff --git a/frontend/viewer/src/lib/services/media-files-service.ts b/frontend/viewer/src/lib/services/media-files-service.ts new file mode 100644 index 0000000000..14765517c0 --- /dev/null +++ b/frontend/viewer/src/lib/services/media-files-service.ts @@ -0,0 +1,51 @@ +import type {IMediaFilesServiceJsInvokable} from '$lib/dotnet-types/generated-types/FwLiteShared/Services/IMediaFilesServiceJsInvokable'; +import type {IHarmonyResource} from '$lib/dotnet-types/generated-types/SIL/Harmony/Resource/IHarmonyResource'; +import type {ILocalResource} from '$lib/dotnet-types/generated-types/SIL/Harmony/Resource/ILocalResource'; +import type {IRemoteResource} from '$lib/dotnet-types/generated-types/SIL/Harmony/Resource/IRemoteResource'; +import {type ProjectContext, useProjectContext} from '$lib/project-context.svelte'; + +export function useMediaFilesService() { + const projectContext = useProjectContext(); + if (!projectContext.mediaFilesService) { + throw new Error('MediaFilesService not available in the current project context'); + } + return new MediaFilesService(projectContext); +} + +export class MediaFilesService { + #projectContext: ProjectContext; + get mediaFilesApi(): IMediaFilesServiceJsInvokable { + if (!this.#projectContext.mediaFilesService) { + throw new Error('MediaFilesService not available in the current project context'); + } + return this.#projectContext.mediaFilesService; + } + + constructor(projectContext: ProjectContext) { + this.#projectContext = projectContext; + } + allResources() { + return this.mediaFilesApi.allResources(); + } + resourcesPendingDownload() { + return this.mediaFilesApi.resourcesPendingDownload(); + } + resourcesPendingUpload() { + return this.mediaFilesApi.resourcesPendingUpload(); + } + downloadAllResources() { + return this.mediaFilesApi.downloadAllResources(); + } + uploadAllResources() { + return this.mediaFilesApi.uploadAllResources(); + } + downloadResources(resources: string[]) { + return this.mediaFilesApi.downloadResources(resources); + } + uploadResources(resources: string[]) { + return this.mediaFilesApi.uploadResources(resources); + } + getFileMetadata(id: string) { + return this.mediaFilesApi.getFileMetadata(id); + } +} diff --git a/frontend/viewer/src/lib/services/service-provider.ts b/frontend/viewer/src/lib/services/service-provider.ts index 2eca49731f..7ae9448050 100644 --- a/frontend/viewer/src/lib/services/service-provider.ts +++ b/frontend/viewer/src/lib/services/service-provider.ts @@ -16,6 +16,7 @@ import type {IJsEventListener} from '$lib/dotnet-types/generated-types/FwLiteSha import type {IFwEvent} from '$lib/dotnet-types/generated-types/FwLiteShared/Events/IFwEvent'; import type {IHistoryServiceJsInvokable} from '$lib/dotnet-types/generated-types/FwLiteShared/Services/IHistoryServiceJsInvokable'; import type {ISyncServiceJsInvokable} from '$lib/dotnet-types/generated-types/FwLiteShared/Services/ISyncServiceJsInvokable'; +import type {IMediaFilesServiceJsInvokable} from '$lib/dotnet-types/generated-types/FwLiteShared/Services/IMediaFilesServiceJsInvokable'; import {useProjectContext} from '../project-context.svelte'; export type ServiceKey = keyof LexboxServiceRegistry; @@ -28,6 +29,7 @@ export type LexboxServiceRegistry = { [DotnetService.ProjectServicesProvider]: IProjectServicesProvider, [DotnetService.HistoryService]: IHistoryServiceJsInvokable, [DotnetService.SyncService]: ISyncServiceJsInvokable, + [DotnetService.MediaFilesService]: IMediaFilesServiceJsInvokable, [DotnetService.AppLauncher]: IAppLauncher, [DotnetService.TroubleshootingService]: ITroubleshootingService, [DotnetService.TestingService]: ITestingService, diff --git a/frontend/viewer/src/project/MediaFilesDialog.svelte b/frontend/viewer/src/project/MediaFilesDialog.svelte new file mode 100644 index 0000000000..caddc84661 --- /dev/null +++ b/frontend/viewer/src/project/MediaFilesDialog.svelte @@ -0,0 +1,245 @@ + + + + + + + + {$t`Download Files`} + + {#if loadingDownload || loadingUpload} + + {:else} +
+ + +
+ +
+
+ {pendingDownloadCount ?? '?'} files to download +
+
+ +
+
+
    + + {#each remoteFiles as file, idx (idx)} +
  • + {#await service.getFileMetadata(file.id)} + ... + {:then metadata} + + {metadata.filename} + {/await} +
  • + {/each} +
    +
+
+ {#if selectedFilesToDownload?.length} +
+ +
+ {/if} +
+ +
+
+ {pendingUploadCount ?? '?'} files to upload +
+
+ +
+
+
    + + {#each localFiles as file, idx (idx)} +
  • + {#await service.getFileMetadata(file.id)} + ... + {:then metadata} + + {metadata.filename} + {/await} +
  • + {/each} +
    +
+
+ {#if selectedFilesToUpload?.length} +
+ +
+ {/if} +
+ ALL FILES +
+
+
    + {#each allFiles as file, idx (idx)} +
  • + {#await service.getFileMetadata(file.id)} + ... + {:then metadata} + {metadata.filename} + {/await} +
  • + {/each} +
+
+
+ {/if} +
+
diff --git a/frontend/viewer/src/project/ProjectSidebar.svelte b/frontend/viewer/src/project/ProjectSidebar.svelte index 0bb81649ba..2dabdcf9c4 100644 --- a/frontend/viewer/src/project/ProjectSidebar.svelte +++ b/frontend/viewer/src/project/ProjectSidebar.svelte @@ -16,6 +16,7 @@ import DevContent from '$lib/layout/DevContent.svelte'; import TroubleshootDialog from '$lib/troubleshoot/TroubleshootDialog.svelte'; import SyncDialog from './SyncDialog.svelte'; + import MediaFilesDialog from './MediaFilesDialog.svelte'; import {useFeatures} from '$lib/services/feature-service'; import {useProjectStats} from '$lib/project-stats'; import {formatNumber} from '$lib/components/ui/format'; @@ -52,6 +53,7 @@ const supportsTroubleshooting = useTroubleshootingService(); let troubleshootDialog = $state(); let syncDialog = $state(); + let mediaFilesDialog = $state(); {#snippet ViewButton(view: View, icon: IconClass, label: string, stat?: string)} @@ -103,8 +105,15 @@ {#if features.sync} + + mediaFilesDialog?.open()} class="justify-between"> +
+ + {$t`Media Files`} +
+
syncDialog?.open()} class="justify-between"> {#snippet tooltipContent()} {#if syncStatus === SyncStatus.Offline}