From c7f51c79b8bbae8275ff63b8c9f5e86633dbf22a Mon Sep 17 00:00:00 2001 From: Lamparter <71598437+Lamparter@users.noreply.github.com> Date: Mon, 23 Mar 2026 21:39:50 +0000 Subject: [PATCH 01/10] Initial implementation of various models --- .../Models/Admin/AdminListPage.cs | 10 ++++++++++ .../Models/Admin/AdminSearchResult.cs | 8 ++++++++ .../Models/Admin/AdminStats.cs | 12 +++++++++++ .../Models/Admin/EntityType.cs | 10 ++++++++++ .../Models/Developer/DeveloperApp.cs | 16 +++++++++++++++ .../Models/Developer/OAuthGrant.cs | 15 ++++++++++++++ .../Models/Developer/TrustLevel.cs | 7 +++++++ .../Models/Timelapses/Comment.cs | 13 ++++++++++++ .../Models/Timelapses/DraftEdit.cs | 12 +++++++++++ .../Models/Timelapses/DraftTimelapse.cs | 20 +++++++++++++++++++ .../Models/Timelapses/EditKind.cs | 6 ++++++ .../Timelapses/Local/DraftPipelineState.cs | 19 ++++++++++++++++++ .../Models/Timelapses/Local/LocalDraft.cs | 17 ++++++++++++++++ .../Models/Timelapses/Local/LocalSession.cs | 11 ++++++++++ .../Models/Timelapses/Local/LocalThumbnail.cs | 13 ++++++++++++ .../Timelapses/Local/RemoteDraftSync.cs | 9 +++++++++ .../Models/Timelapses/Local/TusUploadState.cs | 12 +++++++++++ .../Models/Timelapses/Timelapse.cs | 17 ++++++++++++++++ .../Models/Timelapses/Visibility.cs | 12 +++++++++++ .../Models/User/Device.cs | 11 ++++++++++ .../Models/User/Local/DeviceKey.cs | 4 ++++ .../Models/User/Local/KeyRelayRequest.cs | 7 +++++++ .../Models/User/Local/KeyRelayResult.cs | 7 +++++++ .../Models/User/Myself.cs | 12 +++++++++++ .../Models/User/PermissionLevel.cs | 12 +++++++++++ .../Riverside.Elapsed.App/Models/User/User.cs | 18 +++++++++++++++++ 26 files changed, 310 insertions(+) create mode 100644 src/app/Riverside.Elapsed.App/Models/Admin/AdminListPage.cs create mode 100644 src/app/Riverside.Elapsed.App/Models/Admin/AdminSearchResult.cs create mode 100644 src/app/Riverside.Elapsed.App/Models/Admin/AdminStats.cs create mode 100644 src/app/Riverside.Elapsed.App/Models/Admin/EntityType.cs create mode 100644 src/app/Riverside.Elapsed.App/Models/Developer/DeveloperApp.cs create mode 100644 src/app/Riverside.Elapsed.App/Models/Developer/OAuthGrant.cs create mode 100644 src/app/Riverside.Elapsed.App/Models/Developer/TrustLevel.cs create mode 100644 src/app/Riverside.Elapsed.App/Models/Timelapses/Comment.cs create mode 100644 src/app/Riverside.Elapsed.App/Models/Timelapses/DraftEdit.cs create mode 100644 src/app/Riverside.Elapsed.App/Models/Timelapses/DraftTimelapse.cs create mode 100644 src/app/Riverside.Elapsed.App/Models/Timelapses/EditKind.cs create mode 100644 src/app/Riverside.Elapsed.App/Models/Timelapses/Local/DraftPipelineState.cs create mode 100644 src/app/Riverside.Elapsed.App/Models/Timelapses/Local/LocalDraft.cs create mode 100644 src/app/Riverside.Elapsed.App/Models/Timelapses/Local/LocalSession.cs create mode 100644 src/app/Riverside.Elapsed.App/Models/Timelapses/Local/LocalThumbnail.cs create mode 100644 src/app/Riverside.Elapsed.App/Models/Timelapses/Local/RemoteDraftSync.cs create mode 100644 src/app/Riverside.Elapsed.App/Models/Timelapses/Local/TusUploadState.cs create mode 100644 src/app/Riverside.Elapsed.App/Models/Timelapses/Timelapse.cs create mode 100644 src/app/Riverside.Elapsed.App/Models/Timelapses/Visibility.cs create mode 100644 src/app/Riverside.Elapsed.App/Models/User/Device.cs create mode 100644 src/app/Riverside.Elapsed.App/Models/User/Local/DeviceKey.cs create mode 100644 src/app/Riverside.Elapsed.App/Models/User/Local/KeyRelayRequest.cs create mode 100644 src/app/Riverside.Elapsed.App/Models/User/Local/KeyRelayResult.cs create mode 100644 src/app/Riverside.Elapsed.App/Models/User/Myself.cs create mode 100644 src/app/Riverside.Elapsed.App/Models/User/PermissionLevel.cs create mode 100644 src/app/Riverside.Elapsed.App/Models/User/User.cs diff --git a/src/app/Riverside.Elapsed.App/Models/Admin/AdminListPage.cs b/src/app/Riverside.Elapsed.App/Models/Admin/AdminListPage.cs new file mode 100644 index 0000000..4069530 --- /dev/null +++ b/src/app/Riverside.Elapsed.App/Models/Admin/AdminListPage.cs @@ -0,0 +1,10 @@ +namespace Riverside.Elapsed.App.Models.Admin; + +public class AdminListPage +{ + public EntityType Entity; + public IReadOnlyList Rows; + public long Total; + public long Page; + public long PageSize; +} diff --git a/src/app/Riverside.Elapsed.App/Models/Admin/AdminSearchResult.cs b/src/app/Riverside.Elapsed.App/Models/Admin/AdminSearchResult.cs new file mode 100644 index 0000000..408bbef --- /dev/null +++ b/src/app/Riverside.Elapsed.App/Models/Admin/AdminSearchResult.cs @@ -0,0 +1,8 @@ +namespace Riverside.Elapsed.App.Models.Admin; + +public class AdminSearchResult +{ + public EntityType Entity; + public string Id; + public string DisplayText; +} diff --git a/src/app/Riverside.Elapsed.App/Models/Admin/AdminStats.cs b/src/app/Riverside.Elapsed.App/Models/Admin/AdminStats.cs new file mode 100644 index 0000000..9c8d41a --- /dev/null +++ b/src/app/Riverside.Elapsed.App/Models/Admin/AdminStats.cs @@ -0,0 +1,12 @@ +using System; +using System.Collections.Generic; +using System.Text; + +namespace Riverside.Elapsed.App.Models.Admin; + +public class AdminStats +{ + public double TotalLoggedSeconds; + public long TotalProjects; + public long TotalUsers; +} diff --git a/src/app/Riverside.Elapsed.App/Models/Admin/EntityType.cs b/src/app/Riverside.Elapsed.App/Models/Admin/EntityType.cs new file mode 100644 index 0000000..cf04a2f --- /dev/null +++ b/src/app/Riverside.Elapsed.App/Models/Admin/EntityType.cs @@ -0,0 +1,10 @@ +namespace Riverside.Elapsed.App.Models.Admin; + +public enum EntityType +{ + User, + Timelapse, + Comment, + DraftTimelapse, + LegacyTimelapse, +} diff --git a/src/app/Riverside.Elapsed.App/Models/Developer/DeveloperApp.cs b/src/app/Riverside.Elapsed.App/Models/Developer/DeveloperApp.cs new file mode 100644 index 0000000..05734b1 --- /dev/null +++ b/src/app/Riverside.Elapsed.App/Models/Developer/DeveloperApp.cs @@ -0,0 +1,16 @@ +namespace Riverside.Elapsed.App.Models.Developer; + +public class DeveloperApp +{ + public Guid AppId; + public string Name; + public string Description; + public Uri HomepageUrl; + public Uri? IconUrl; + public IReadOnlyList RedirectUrls; + public IReadOnlyList Scopes; + public TrustLevel TrustLevel; + public string ClientId; + public DateTimeOffset CreatedAt; + public (string, string, string)? CreatedBy; +} diff --git a/src/app/Riverside.Elapsed.App/Models/Developer/OAuthGrant.cs b/src/app/Riverside.Elapsed.App/Models/Developer/OAuthGrant.cs new file mode 100644 index 0000000..0d3c585 --- /dev/null +++ b/src/app/Riverside.Elapsed.App/Models/Developer/OAuthGrant.cs @@ -0,0 +1,15 @@ +using System; +using System.Collections.Generic; +using System.Text; + +namespace Riverside.Elapsed.App.Models.Developer; + +public class OAuthGrant +{ + public string GrantId; + public string ServiceClientId; + public string ServiceName; + public IReadOnlyList Scopes; + public DateTimeOffset CreatedAt; + public DateTimeOffset? LastUsedAt; +} diff --git a/src/app/Riverside.Elapsed.App/Models/Developer/TrustLevel.cs b/src/app/Riverside.Elapsed.App/Models/Developer/TrustLevel.cs new file mode 100644 index 0000000..ca17445 --- /dev/null +++ b/src/app/Riverside.Elapsed.App/Models/Developer/TrustLevel.cs @@ -0,0 +1,7 @@ +namespace Riverside.Elapsed.App.Models.Developer; + +public enum TrustLevel +{ + Untrusted, + Trusted, +} diff --git a/src/app/Riverside.Elapsed.App/Models/Timelapses/Comment.cs b/src/app/Riverside.Elapsed.App/Models/Timelapses/Comment.cs new file mode 100644 index 0000000..1db96b0 --- /dev/null +++ b/src/app/Riverside.Elapsed.App/Models/Timelapses/Comment.cs @@ -0,0 +1,13 @@ +using System; +using System.Collections.Generic; +using System.Text; + +namespace Riverside.Elapsed.App.Models.Timelapses; + +public class Comment +{ + public string CommentId; + public string Content; + public User.User Author; + public DateTimeOffset CreatedAt; +} diff --git a/src/app/Riverside.Elapsed.App/Models/Timelapses/DraftEdit.cs b/src/app/Riverside.Elapsed.App/Models/Timelapses/DraftEdit.cs new file mode 100644 index 0000000..a351938 --- /dev/null +++ b/src/app/Riverside.Elapsed.App/Models/Timelapses/DraftEdit.cs @@ -0,0 +1,12 @@ +using System; +using System.Collections.Generic; +using System.Text; + +namespace Riverside.Elapsed.App.Models.Timelapses; + +public class DraftEdit +{ + public double BeginSeconds; + public double EndSeconds; + public EditKind Kind; +} diff --git a/src/app/Riverside.Elapsed.App/Models/Timelapses/DraftTimelapse.cs b/src/app/Riverside.Elapsed.App/Models/Timelapses/DraftTimelapse.cs new file mode 100644 index 0000000..81e0a8a --- /dev/null +++ b/src/app/Riverside.Elapsed.App/Models/Timelapses/DraftTimelapse.cs @@ -0,0 +1,20 @@ +using System; +using System.Collections.Generic; +using System.Text; + +namespace Riverside.Elapsed.App.Models.Timelapses; + +public class DraftTimelapse +{ + public string DraftTimelapseId; + public string Name; + public string Description; + public DateTimeOffset CreatedAt; + public User.User Owner; + public Guid DeviceId; + public byte[] IvHex; + public Uri PreviewThumbnailUrl; + public IReadOnlyList Sessions; + public IReadOnlyList EditList; + public string? AssociatedTimelapseId; +} diff --git a/src/app/Riverside.Elapsed.App/Models/Timelapses/EditKind.cs b/src/app/Riverside.Elapsed.App/Models/Timelapses/EditKind.cs new file mode 100644 index 0000000..17684b9 --- /dev/null +++ b/src/app/Riverside.Elapsed.App/Models/Timelapses/EditKind.cs @@ -0,0 +1,6 @@ +namespace Riverside.Elapsed.App.Models.Timelapses; + +public enum EditKind +{ + Cut, +} diff --git a/src/app/Riverside.Elapsed.App/Models/Timelapses/Local/DraftPipelineState.cs b/src/app/Riverside.Elapsed.App/Models/Timelapses/Local/DraftPipelineState.cs new file mode 100644 index 0000000..3630538 --- /dev/null +++ b/src/app/Riverside.Elapsed.App/Models/Timelapses/Local/DraftPipelineState.cs @@ -0,0 +1,19 @@ +namespace Riverside.Elapsed.App.Models.Timelapses.Local; + +public class DraftPipelineState +{ + public enum Phase + { + LocalOnly, + CreatingRemoteDraft, + Encrypting, + Uploading, + ReadyToPublish, + Publishing, + Published, + Error, + } + + public double Progress; + public string? LastError; +} diff --git a/src/app/Riverside.Elapsed.App/Models/Timelapses/Local/LocalDraft.cs b/src/app/Riverside.Elapsed.App/Models/Timelapses/Local/LocalDraft.cs new file mode 100644 index 0000000..283cb70 --- /dev/null +++ b/src/app/Riverside.Elapsed.App/Models/Timelapses/Local/LocalDraft.cs @@ -0,0 +1,17 @@ +namespace Riverside.Elapsed.App.Models.Timelapses.Local; + +public class LocalDraft +{ + public Guid LocalDraftId; + public string Name; + public string Description; + public DateTimeOffset CreatedAt; + public DateTimeOffset LastModifiedAt; + public Guid DeviceId; + public IReadOnlyList Snapshots; // milliseconds since epoch + public List EditList; + public List Sessions; + public LocalThumbnail Thumbnail; + public RemoteDraftSync? Remote; + public DraftPipelineState State; +} diff --git a/src/app/Riverside.Elapsed.App/Models/Timelapses/Local/LocalSession.cs b/src/app/Riverside.Elapsed.App/Models/Timelapses/Local/LocalSession.cs new file mode 100644 index 0000000..0049be1 --- /dev/null +++ b/src/app/Riverside.Elapsed.App/Models/Timelapses/Local/LocalSession.cs @@ -0,0 +1,11 @@ +namespace Riverside.Elapsed.App.Models.Timelapses.Local; + +public class LocalSession +{ + public Guid LocalSessionId; + public string FilePath; + public long FileSizeBytes; + // RecordedAtStart, RecordedAtEnd + //public SessionEncryptionState Encryption; + public TusUploadState Upload; +} diff --git a/src/app/Riverside.Elapsed.App/Models/Timelapses/Local/LocalThumbnail.cs b/src/app/Riverside.Elapsed.App/Models/Timelapses/Local/LocalThumbnail.cs new file mode 100644 index 0000000..ca1487a --- /dev/null +++ b/src/app/Riverside.Elapsed.App/Models/Timelapses/Local/LocalThumbnail.cs @@ -0,0 +1,13 @@ +using System; +using System.Collections.Generic; +using System.Text; + +namespace Riverside.Elapsed.App.Models.Timelapses.Local; + +public class LocalThumbnail +{ + public string FilePath; + public long FileSizeBytes; + //public ThumbnailEncryptionState Encryption; + public TusUploadState Upload; +} diff --git a/src/app/Riverside.Elapsed.App/Models/Timelapses/Local/RemoteDraftSync.cs b/src/app/Riverside.Elapsed.App/Models/Timelapses/Local/RemoteDraftSync.cs new file mode 100644 index 0000000..e806c3b --- /dev/null +++ b/src/app/Riverside.Elapsed.App/Models/Timelapses/Local/RemoteDraftSync.cs @@ -0,0 +1,9 @@ +namespace Riverside.Elapsed.App.Models.Timelapses.Local; + +public class RemoteDraftSync +{ + public string DraftTimelapseId; + public byte[] IvHex; + public List SessionUploadTokens; + public string ThumbnailUploadToken; +} diff --git a/src/app/Riverside.Elapsed.App/Models/Timelapses/Local/TusUploadState.cs b/src/app/Riverside.Elapsed.App/Models/Timelapses/Local/TusUploadState.cs new file mode 100644 index 0000000..c3edbbd --- /dev/null +++ b/src/app/Riverside.Elapsed.App/Models/Timelapses/Local/TusUploadState.cs @@ -0,0 +1,12 @@ +namespace Riverside.Elapsed.App.Models.Timelapses.Local; + +public class TusUploadState +{ + public string? UploadUrl; + public long BytesUploaded; + public long BytesTotal; + public bool IsComplete; + public string? LastError; + public DateTimeOffset StartedAt; + public DateTimeOffset? CompletedAt; +} diff --git a/src/app/Riverside.Elapsed.App/Models/Timelapses/Timelapse.cs b/src/app/Riverside.Elapsed.App/Models/Timelapses/Timelapse.cs new file mode 100644 index 0000000..dbabc75 --- /dev/null +++ b/src/app/Riverside.Elapsed.App/Models/Timelapses/Timelapse.cs @@ -0,0 +1,17 @@ +namespace Riverside.Elapsed.App.Models.Timelapses; + +public class Timelapse +{ + public string TimelapseId; + public string Name; + public string Description; + public Visibility Visibility; + public DateTimeOffset CreatedAt; + public User.User Owner; + public IReadOnlyList Comments; + public Uri? PlaybackUrl; + public Uri? ThumbnailUrl; + public double DurationSeconds; + public string? HackatimeProject; + public string? SourceDraftId; +} diff --git a/src/app/Riverside.Elapsed.App/Models/Timelapses/Visibility.cs b/src/app/Riverside.Elapsed.App/Models/Timelapses/Visibility.cs new file mode 100644 index 0000000..67f2f7b --- /dev/null +++ b/src/app/Riverside.Elapsed.App/Models/Timelapses/Visibility.cs @@ -0,0 +1,12 @@ +using System; +using System.Collections.Generic; +using System.Text; + +namespace Riverside.Elapsed.App.Models.Timelapses; + +public enum Visibility +{ + Unlisted, + Public, + FailedProcessing, +} diff --git a/src/app/Riverside.Elapsed.App/Models/User/Device.cs b/src/app/Riverside.Elapsed.App/Models/User/Device.cs new file mode 100644 index 0000000..150bba9 --- /dev/null +++ b/src/app/Riverside.Elapsed.App/Models/User/Device.cs @@ -0,0 +1,11 @@ +using System; +using System.Collections.Generic; +using System.Text; + +namespace Riverside.Elapsed.App.Models.User; + +public class Device +{ + public Guid DeviceId; + public string Name; +} diff --git a/src/app/Riverside.Elapsed.App/Models/User/Local/DeviceKey.cs b/src/app/Riverside.Elapsed.App/Models/User/Local/DeviceKey.cs new file mode 100644 index 0000000..cb1a162 --- /dev/null +++ b/src/app/Riverside.Elapsed.App/Models/User/Local/DeviceKey.cs @@ -0,0 +1,4 @@ +namespace Riverside.Elapsed.App.Models.User.Local; + +[ImplicitKeys(IsEnabled = false)] +public record DeviceKey(Guid DeviceId, byte[] Key, DateTimeOffset CreatedAt); diff --git a/src/app/Riverside.Elapsed.App/Models/User/Local/KeyRelayRequest.cs b/src/app/Riverside.Elapsed.App/Models/User/Local/KeyRelayRequest.cs new file mode 100644 index 0000000..bcc94ec --- /dev/null +++ b/src/app/Riverside.Elapsed.App/Models/User/Local/KeyRelayRequest.cs @@ -0,0 +1,7 @@ +namespace Riverside.Elapsed.App.Models.User.Local; + +public class KeyRelayRequest +{ + public Guid ExchangeId; + public Guid CallingDeviceId; +} diff --git a/src/app/Riverside.Elapsed.App/Models/User/Local/KeyRelayResult.cs b/src/app/Riverside.Elapsed.App/Models/User/Local/KeyRelayResult.cs new file mode 100644 index 0000000..4aefe70 --- /dev/null +++ b/src/app/Riverside.Elapsed.App/Models/User/Local/KeyRelayResult.cs @@ -0,0 +1,7 @@ +namespace Riverside.Elapsed.App.Models.User.Local; + +public class KeyRelayResult +{ + public Guid DeviceId; + public byte[] DeviceKey; +} diff --git a/src/app/Riverside.Elapsed.App/Models/User/Myself.cs b/src/app/Riverside.Elapsed.App/Models/User/Myself.cs new file mode 100644 index 0000000..97ccf81 --- /dev/null +++ b/src/app/Riverside.Elapsed.App/Models/User/Myself.cs @@ -0,0 +1,12 @@ +using System; +using System.Collections.Generic; +using System.Text; + +namespace Riverside.Elapsed.App.Models.User; + +public sealed class Myself : User +{ + public IReadOnlyList Devices; + public bool NeedsReauth; + public PermissionLevel PermissionLevel; +} diff --git a/src/app/Riverside.Elapsed.App/Models/User/PermissionLevel.cs b/src/app/Riverside.Elapsed.App/Models/User/PermissionLevel.cs new file mode 100644 index 0000000..1f3f747 --- /dev/null +++ b/src/app/Riverside.Elapsed.App/Models/User/PermissionLevel.cs @@ -0,0 +1,12 @@ +using System; +using System.Collections.Generic; +using System.Text; + +namespace Riverside.Elapsed.App.Models.User; + +public enum PermissionLevel +{ + User, + Admin, + Root, +} diff --git a/src/app/Riverside.Elapsed.App/Models/User/User.cs b/src/app/Riverside.Elapsed.App/Models/User/User.cs new file mode 100644 index 0000000..fae8b81 --- /dev/null +++ b/src/app/Riverside.Elapsed.App/Models/User/User.cs @@ -0,0 +1,18 @@ +using System; +using System.Collections.Generic; +using System.Text; + +namespace Riverside.Elapsed.App.Models.User; + +public class User +{ + public string UserId; + public DateTimeOffset CreatedAt; + public string Handle; + public string DisplayName; + public Uri ProfilePictureUrl; + public string Bio; + public IReadOnlyList Urls; + public string? HackatimeId; + public string? SlackId; +} From 905d7023e240b4d69042e1e60bf4dacfc3d7a6ff Mon Sep 17 00:00:00 2001 From: Lamparter <71598437+Lamparter@users.noreply.github.com> Date: Wed, 25 Mar 2026 22:21:55 +0000 Subject: [PATCH 02/10] Improve strong typing and MVVM compatibility in models --- .../Riverside.Elapsed.App/Models/Admin/AdminListPage.cs | 4 +++- .../Riverside.Elapsed.App/Models/Developer/DeveloperApp.cs | 4 ++-- .../Models/Timelapses/CursorPage{T}.cs | 7 +++++++ .../Models/Timelapses/DraftTimelapse.cs | 2 +- .../Models/Timelapses/Local/DraftPipelineState.cs | 1 + .../Models/Timelapses/Local/RemoteDraftSync.cs | 2 +- 6 files changed, 15 insertions(+), 5 deletions(-) create mode 100644 src/app/Riverside.Elapsed.App/Models/Timelapses/CursorPage{T}.cs diff --git a/src/app/Riverside.Elapsed.App/Models/Admin/AdminListPage.cs b/src/app/Riverside.Elapsed.App/Models/Admin/AdminListPage.cs index 4069530..da4a0bf 100644 --- a/src/app/Riverside.Elapsed.App/Models/Admin/AdminListPage.cs +++ b/src/app/Riverside.Elapsed.App/Models/Admin/AdminListPage.cs @@ -1,9 +1,11 @@ +using System.Text.Json; + namespace Riverside.Elapsed.App.Models.Admin; public class AdminListPage { public EntityType Entity; - public IReadOnlyList Rows; + public IReadOnlyList Rows; public long Total; public long Page; public long PageSize; diff --git a/src/app/Riverside.Elapsed.App/Models/Developer/DeveloperApp.cs b/src/app/Riverside.Elapsed.App/Models/Developer/DeveloperApp.cs index 05734b1..b63166a 100644 --- a/src/app/Riverside.Elapsed.App/Models/Developer/DeveloperApp.cs +++ b/src/app/Riverside.Elapsed.App/Models/Developer/DeveloperApp.cs @@ -7,10 +7,10 @@ public class DeveloperApp public string Description; public Uri HomepageUrl; public Uri? IconUrl; - public IReadOnlyList RedirectUrls; + public IReadOnlyList RedirectUris; public IReadOnlyList Scopes; public TrustLevel TrustLevel; public string ClientId; public DateTimeOffset CreatedAt; - public (string, string, string)? CreatedBy; + public User.User? CreatedBy; } diff --git a/src/app/Riverside.Elapsed.App/Models/Timelapses/CursorPage{T}.cs b/src/app/Riverside.Elapsed.App/Models/Timelapses/CursorPage{T}.cs new file mode 100644 index 0000000..b59a0f9 --- /dev/null +++ b/src/app/Riverside.Elapsed.App/Models/Timelapses/CursorPage{T}.cs @@ -0,0 +1,7 @@ +namespace Riverside.Elapsed.App.Models.Timelapses; + +public class CursorPage // infinite scroll +{ + public IReadOnlyList Items; + public string? NextCursor; +} diff --git a/src/app/Riverside.Elapsed.App/Models/Timelapses/DraftTimelapse.cs b/src/app/Riverside.Elapsed.App/Models/Timelapses/DraftTimelapse.cs index 81e0a8a..9e88f37 100644 --- a/src/app/Riverside.Elapsed.App/Models/Timelapses/DraftTimelapse.cs +++ b/src/app/Riverside.Elapsed.App/Models/Timelapses/DraftTimelapse.cs @@ -12,7 +12,7 @@ public class DraftTimelapse public DateTimeOffset CreatedAt; public User.User Owner; public Guid DeviceId; - public byte[] IvHex; + public byte[] IvBytes; // IvHex public Uri PreviewThumbnailUrl; public IReadOnlyList Sessions; public IReadOnlyList EditList; diff --git a/src/app/Riverside.Elapsed.App/Models/Timelapses/Local/DraftPipelineState.cs b/src/app/Riverside.Elapsed.App/Models/Timelapses/Local/DraftPipelineState.cs index 3630538..6bfd8aa 100644 --- a/src/app/Riverside.Elapsed.App/Models/Timelapses/Local/DraftPipelineState.cs +++ b/src/app/Riverside.Elapsed.App/Models/Timelapses/Local/DraftPipelineState.cs @@ -14,6 +14,7 @@ public enum Phase Error, } + public Phase CurrentPhase; public double Progress; public string? LastError; } diff --git a/src/app/Riverside.Elapsed.App/Models/Timelapses/Local/RemoteDraftSync.cs b/src/app/Riverside.Elapsed.App/Models/Timelapses/Local/RemoteDraftSync.cs index e806c3b..77f8ee7 100644 --- a/src/app/Riverside.Elapsed.App/Models/Timelapses/Local/RemoteDraftSync.cs +++ b/src/app/Riverside.Elapsed.App/Models/Timelapses/Local/RemoteDraftSync.cs @@ -3,7 +3,7 @@ namespace Riverside.Elapsed.App.Models.Timelapses.Local; public class RemoteDraftSync { public string DraftTimelapseId; - public byte[] IvHex; + public byte[] IvBytes; // IvHex public List SessionUploadTokens; public string ThumbnailUploadToken; } From 1a2a119a4ff5d4d2643ae9ea64d21180f7fc3b66 Mon Sep 17 00:00:00 2001 From: Lamparter <71598437+Lamparter@users.noreply.github.com> Date: Wed, 25 Mar 2026 23:10:05 +0000 Subject: [PATCH 03/10] Improve JSON serialisation --- src/app/Riverside.Elapsed.App/Constants.cs | 18 +++++++++++ .../Json/TimeSpanSecondsJsonConverter.cs | 31 +++++++++++++++++++ .../Converters/Json/UriJsonConverter.cs | 24 ++++++++++++++ .../Models/Timelapses/DraftTimelapse.cs | 2 +- .../Timelapses/Local/DraftPipelineState.cs | 6 ++-- .../Models/Timelapses/Local/LocalDraft.cs | 30 ++++++++++-------- .../Timelapses/Local/RemoteDraftSync.cs | 2 +- 7 files changed, 95 insertions(+), 18 deletions(-) create mode 100644 src/app/Riverside.Elapsed.App/Constants.cs create mode 100644 src/app/Riverside.Elapsed.App/Converters/Json/TimeSpanSecondsJsonConverter.cs create mode 100644 src/app/Riverside.Elapsed.App/Converters/Json/UriJsonConverter.cs diff --git a/src/app/Riverside.Elapsed.App/Constants.cs b/src/app/Riverside.Elapsed.App/Constants.cs new file mode 100644 index 0000000..dbd3889 --- /dev/null +++ b/src/app/Riverside.Elapsed.App/Constants.cs @@ -0,0 +1,18 @@ +using System.Text.Json; +using System.Text.Json.Serialization; + +namespace Riverside.Elapsed.App; + +public static class Constants +{ + public static JsonSerializerOptions SerializerOptions = new() + { + WriteIndented = true, + PropertyNameCaseInsensitive = true, + PropertyNamingPolicy = JsonNamingPolicy.CamelCase, + DictionaryKeyPolicy = JsonNamingPolicy.CamelCase, + DefaultIgnoreCondition = JsonIgnoreCondition.WhenWritingNull, + IgnoreReadOnlyProperties = true, + ReferenceHandler = ReferenceHandler.IgnoreCycles, + }; +} diff --git a/src/app/Riverside.Elapsed.App/Converters/Json/TimeSpanSecondsJsonConverter.cs b/src/app/Riverside.Elapsed.App/Converters/Json/TimeSpanSecondsJsonConverter.cs new file mode 100644 index 0000000..28688a4 --- /dev/null +++ b/src/app/Riverside.Elapsed.App/Converters/Json/TimeSpanSecondsJsonConverter.cs @@ -0,0 +1,31 @@ +using System; +using System.Collections.Generic; +using System.Globalization; +using System.Text; +using System.Text.Json; +using System.Text.Json.Serialization; + +namespace Riverside.Elapsed.App.Converters.Json; + +public sealed class TimeSpanSecondsJsonConverter : JsonConverter +{ + public override TimeSpan Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options) + { + if (reader.TokenType == JsonTokenType.Number && reader.TryGetDouble(out var seconds)) + return TimeSpan.FromSeconds(seconds); + + if (reader.TokenType == JsonTokenType.String) + { + var s = reader.GetString(); + if (double.TryParse(s, NumberStyles.Float, CultureInfo.InvariantCulture, out var sec)) + return TimeSpan.FromMilliseconds(sec); + } + + throw new JsonException("Invalid timespan value."); + } + + public override void Write(Utf8JsonWriter writer, TimeSpan value, JsonSerializerOptions options) + { + writer.WriteNumberValue(value.TotalSeconds); + } +} diff --git a/src/app/Riverside.Elapsed.App/Converters/Json/UriJsonConverter.cs b/src/app/Riverside.Elapsed.App/Converters/Json/UriJsonConverter.cs new file mode 100644 index 0000000..cd78900 --- /dev/null +++ b/src/app/Riverside.Elapsed.App/Converters/Json/UriJsonConverter.cs @@ -0,0 +1,24 @@ +using System; +using System.Collections.Generic; +using System.Text; +using System.Text.Json; +using System.Text.Json.Serialization; + +namespace Riverside.Elapsed.App.Converters.Json; + +public sealed class UriJsonConverter : JsonConverter +{ + public override Uri? Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options) + { + var s = reader.GetString(); + if (string.IsNullOrWhiteSpace(s)) + return new Uri("about:blank"); + + return new Uri(s, UriKind.Absolute); + } + + public override void Write(Utf8JsonWriter writer, Uri value, JsonSerializerOptions options) + { + writer.WriteStringValue(value.ToString()); + } +} diff --git a/src/app/Riverside.Elapsed.App/Models/Timelapses/DraftTimelapse.cs b/src/app/Riverside.Elapsed.App/Models/Timelapses/DraftTimelapse.cs index 9e88f37..2b8d625 100644 --- a/src/app/Riverside.Elapsed.App/Models/Timelapses/DraftTimelapse.cs +++ b/src/app/Riverside.Elapsed.App/Models/Timelapses/DraftTimelapse.cs @@ -12,7 +12,7 @@ public class DraftTimelapse public DateTimeOffset CreatedAt; public User.User Owner; public Guid DeviceId; - public byte[] IvBytes; // IvHex + public string IvHex; public Uri PreviewThumbnailUrl; public IReadOnlyList Sessions; public IReadOnlyList EditList; diff --git a/src/app/Riverside.Elapsed.App/Models/Timelapses/Local/DraftPipelineState.cs b/src/app/Riverside.Elapsed.App/Models/Timelapses/Local/DraftPipelineState.cs index 6bfd8aa..ece9bda 100644 --- a/src/app/Riverside.Elapsed.App/Models/Timelapses/Local/DraftPipelineState.cs +++ b/src/app/Riverside.Elapsed.App/Models/Timelapses/Local/DraftPipelineState.cs @@ -14,7 +14,7 @@ public enum Phase Error, } - public Phase CurrentPhase; - public double Progress; - public string? LastError; + public Phase CurrentPhase { get; init; } = Phase.LocalOnly; + public double Progress { get; init; } + public string? LastError { get; init; } } diff --git a/src/app/Riverside.Elapsed.App/Models/Timelapses/Local/LocalDraft.cs b/src/app/Riverside.Elapsed.App/Models/Timelapses/Local/LocalDraft.cs index 283cb70..3170d41 100644 --- a/src/app/Riverside.Elapsed.App/Models/Timelapses/Local/LocalDraft.cs +++ b/src/app/Riverside.Elapsed.App/Models/Timelapses/Local/LocalDraft.cs @@ -1,17 +1,21 @@ namespace Riverside.Elapsed.App.Models.Timelapses.Local; -public class LocalDraft +public sealed record LocalDraft { - public Guid LocalDraftId; - public string Name; - public string Description; - public DateTimeOffset CreatedAt; - public DateTimeOffset LastModifiedAt; - public Guid DeviceId; - public IReadOnlyList Snapshots; // milliseconds since epoch - public List EditList; - public List Sessions; - public LocalThumbnail Thumbnail; - public RemoteDraftSync? Remote; - public DraftPipelineState State; + public Guid LocalDraftId { get; init; } + public string Name { get; init; } = ""; + public string Description { get; init; } = ""; + public DateTimeOffset CreatedAt { get; init; } + public DateTimeOffset LastModifiedAt { get; init; } + + public Guid DeviceId { get; init; } + + public IReadOnlyList Snapshots { get; init; } = []; // milliseconds since epoch + public List EditList { get; init; } = []; + + public List Sessions { get; init; } = []; + public LocalThumbnail Thumbnail { get; init; } = new(); + + public RemoteDraftSync? Remote { get; init; } + public DraftPipelineState State { get; init; } = new(); } diff --git a/src/app/Riverside.Elapsed.App/Models/Timelapses/Local/RemoteDraftSync.cs b/src/app/Riverside.Elapsed.App/Models/Timelapses/Local/RemoteDraftSync.cs index 77f8ee7..28c5021 100644 --- a/src/app/Riverside.Elapsed.App/Models/Timelapses/Local/RemoteDraftSync.cs +++ b/src/app/Riverside.Elapsed.App/Models/Timelapses/Local/RemoteDraftSync.cs @@ -3,7 +3,7 @@ namespace Riverside.Elapsed.App.Models.Timelapses.Local; public class RemoteDraftSync { public string DraftTimelapseId; - public byte[] IvBytes; // IvHex + public string IvHex; public List SessionUploadTokens; public string ThumbnailUploadToken; } From dd87103a536faa68341f95ce11e2e18b02ac5b29 Mon Sep 17 00:00:00 2001 From: Lamparter <71598437+Lamparter@users.noreply.github.com> Date: Thu, 26 Mar 2026 17:51:42 +0000 Subject: [PATCH 04/10] Customise JSON serialisation settings --- src/app/Riverside.Elapsed.App/Constants.cs | 32 +++++++++++++------ .../Converters/Json/UriJsonConverter.cs | 3 -- .../Riverside.Elapsed.App/App.xaml.cs | 4 +++ 3 files changed, 27 insertions(+), 12 deletions(-) diff --git a/src/app/Riverside.Elapsed.App/Constants.cs b/src/app/Riverside.Elapsed.App/Constants.cs index dbd3889..11ff02e 100644 --- a/src/app/Riverside.Elapsed.App/Constants.cs +++ b/src/app/Riverside.Elapsed.App/Constants.cs @@ -1,18 +1,32 @@ using System.Text.Json; using System.Text.Json.Serialization; +using Riverside.Elapsed.App.Converters.Json; namespace Riverside.Elapsed.App; public static class Constants { - public static JsonSerializerOptions SerializerOptions = new() + public static readonly JsonSerializerOptions SerializerOptions = GetJsonSerializerOptions(); + + public static JsonSerializerOptions GetJsonSerializerOptions() { - WriteIndented = true, - PropertyNameCaseInsensitive = true, - PropertyNamingPolicy = JsonNamingPolicy.CamelCase, - DictionaryKeyPolicy = JsonNamingPolicy.CamelCase, - DefaultIgnoreCondition = JsonIgnoreCondition.WhenWritingNull, - IgnoreReadOnlyProperties = true, - ReferenceHandler = ReferenceHandler.IgnoreCycles, - }; + var options = new JsonSerializerOptions + { + WriteIndented = true, + PropertyNameCaseInsensitive = true, + PropertyNamingPolicy = JsonNamingPolicy.CamelCase, + DictionaryKeyPolicy = JsonNamingPolicy.CamelCase, + DefaultIgnoreCondition = JsonIgnoreCondition.WhenWritingNull, + IgnoreReadOnlyProperties = true, + NumberHandling = JsonNumberHandling.Strict, + //ReferenceHandler = ReferenceHandler.IgnoreCycles, + }; + + options.Converters.Add(new JsonStringEnumConverter(JsonNamingPolicy.CamelCase)); + //options.Converters.Add(new JsonStringEnumConverter(JsonNamingPolicy.CamelCase, true)); + //options.Converters.Add(new TimeSpanSecondsJsonConverter()); + //options.Converters.Add(new UriJsonConverter()); + + return options; + } } diff --git a/src/app/Riverside.Elapsed.App/Converters/Json/UriJsonConverter.cs b/src/app/Riverside.Elapsed.App/Converters/Json/UriJsonConverter.cs index cd78900..16c156e 100644 --- a/src/app/Riverside.Elapsed.App/Converters/Json/UriJsonConverter.cs +++ b/src/app/Riverside.Elapsed.App/Converters/Json/UriJsonConverter.cs @@ -1,6 +1,3 @@ -using System; -using System.Collections.Generic; -using System.Text; using System.Text.Json; using System.Text.Json.Serialization; diff --git a/src/platforms/Riverside.Elapsed.App/App.xaml.cs b/src/platforms/Riverside.Elapsed.App/App.xaml.cs index 0eb6c76..f3f7fb3 100644 --- a/src/platforms/Riverside.Elapsed.App/App.xaml.cs +++ b/src/platforms/Riverside.Elapsed.App/App.xaml.cs @@ -87,6 +87,10 @@ protected async override void OnLaunched(LaunchActivatedEventArgs args) //services.AddSingleton(); }) .UseNavigation(RegisterRoutes) + .UseSerialization(serialization => + { + serialization.AddSingleton(Constants.SerializerOptions); + }) ); MainWindow = builder.Window; From 508685b864f5279fd21a5e527c64f67ae817755c Mon Sep 17 00:00:00 2001 From: Lamparter <71598437+Lamparter@users.noreply.github.com> Date: Thu, 26 Mar 2026 23:27:11 +0000 Subject: [PATCH 05/10] Refactor existing models and partially introduce JSON store model --- .../Models/Primitives/IUploadable.cs | 15 +++++ .../Models/Timelapses/Local/LocalDraft.cs | 22 ++++++- .../Timelapses/Local/LocalDraftIndex.cs | 6 ++ .../Timelapses/Local/LocalDraftIndexItem.cs | 11 ++++ .../Models/Timelapses/Local/LocalSession.cs | 17 +++-- .../Models/Timelapses/Local/LocalThumbnail.cs | 12 ++-- .../Timelapses/Local/RemoteDraftSync.cs | 6 +- .../Models/Timelapses/Local/TusUploadState.cs | 16 +++-- .../DraftsServiceCollectionExtensions.cs | 12 ++++ .../Services/Drafts/ILocalDraftRepository.cs | 12 ++++ .../Services/Drafts/LocalDraftRepository.cs | 57 ++++++++++++++++ .../Services/Storage/ILocalJsonStore.cs | 9 +++ .../Services/Storage/LocalJsonStore.cs | 66 +++++++++++++++++++ .../Riverside.Elapsed.App/App.xaml.cs | 2 + 14 files changed, 238 insertions(+), 25 deletions(-) create mode 100644 src/app/Riverside.Elapsed.App/Models/Primitives/IUploadable.cs create mode 100644 src/app/Riverside.Elapsed.App/Models/Timelapses/Local/LocalDraftIndex.cs create mode 100644 src/app/Riverside.Elapsed.App/Models/Timelapses/Local/LocalDraftIndexItem.cs create mode 100644 src/app/Riverside.Elapsed.App/Services/Drafts/DraftsServiceCollectionExtensions.cs create mode 100644 src/app/Riverside.Elapsed.App/Services/Drafts/ILocalDraftRepository.cs create mode 100644 src/app/Riverside.Elapsed.App/Services/Drafts/LocalDraftRepository.cs create mode 100644 src/app/Riverside.Elapsed.App/Services/Storage/ILocalJsonStore.cs create mode 100644 src/app/Riverside.Elapsed.App/Services/Storage/LocalJsonStore.cs diff --git a/src/app/Riverside.Elapsed.App/Models/Primitives/IUploadable.cs b/src/app/Riverside.Elapsed.App/Models/Primitives/IUploadable.cs new file mode 100644 index 0000000..78c236b --- /dev/null +++ b/src/app/Riverside.Elapsed.App/Models/Primitives/IUploadable.cs @@ -0,0 +1,15 @@ +using Riverside.Elapsed.App.Models.Timelapses.Local; + +namespace Riverside.Elapsed.App.Models.Primitives; + +/// +/// Represents a locally-stored file that can be uploaded to the Lapse server online. +/// +public interface IUploadable +{ + string FilePath { get; init; } + long FileSizeBytes { get; init; } + + string? UploadToken { get; init; } + TusUploadState Upload { get; init; } +} diff --git a/src/app/Riverside.Elapsed.App/Models/Timelapses/Local/LocalDraft.cs b/src/app/Riverside.Elapsed.App/Models/Timelapses/Local/LocalDraft.cs index 3170d41..e46cb5e 100644 --- a/src/app/Riverside.Elapsed.App/Models/Timelapses/Local/LocalDraft.cs +++ b/src/app/Riverside.Elapsed.App/Models/Timelapses/Local/LocalDraft.cs @@ -3,8 +3,8 @@ namespace Riverside.Elapsed.App.Models.Timelapses.Local; public sealed record LocalDraft { public Guid LocalDraftId { get; init; } - public string Name { get; init; } = ""; - public string Description { get; init; } = ""; + public string Name { get; init; } = string.Empty; + public string Description { get; init; } = string.Empty; public DateTimeOffset CreatedAt { get; init; } public DateTimeOffset LastModifiedAt { get; init; } @@ -18,4 +18,22 @@ public sealed record LocalDraft public RemoteDraftSync? Remote { get; init; } public DraftPipelineState State { get; init; } = new(); + + public static LocalDraft Create(Guid deviceId, DateTimeOffset now) + { + return new LocalDraft + { + LocalDraftId = Guid.NewGuid(), + DeviceId = deviceId, + CreatedAt = now, + Name = string.Empty, + Description = string.Empty, + Snapshots = [], + EditList = [], + Sessions = [], + Thumbnail = new LocalThumbnail(), + State = new DraftPipelineState(), + Remote = null, + }; + } } diff --git a/src/app/Riverside.Elapsed.App/Models/Timelapses/Local/LocalDraftIndex.cs b/src/app/Riverside.Elapsed.App/Models/Timelapses/Local/LocalDraftIndex.cs new file mode 100644 index 0000000..82f8e54 --- /dev/null +++ b/src/app/Riverside.Elapsed.App/Models/Timelapses/Local/LocalDraftIndex.cs @@ -0,0 +1,6 @@ +namespace Riverside.Elapsed.App.Models.Timelapses.Local; + +public sealed record LocalDraftIndex +{ + public IReadOnlyList Drafts { get; init; } = []; +} diff --git a/src/app/Riverside.Elapsed.App/Models/Timelapses/Local/LocalDraftIndexItem.cs b/src/app/Riverside.Elapsed.App/Models/Timelapses/Local/LocalDraftIndexItem.cs new file mode 100644 index 0000000..cf735a0 --- /dev/null +++ b/src/app/Riverside.Elapsed.App/Models/Timelapses/Local/LocalDraftIndexItem.cs @@ -0,0 +1,11 @@ +namespace Riverside.Elapsed.App.Models.Timelapses.Local; + +public sealed record LocalDraftIndexItem +{ + public Guid LocalDraftId { get; init; } + public string Name { get; init; } = string.Empty; + public DateTimeOffset LastModifiedAt { get; init; } + + public bool HasRemoteDraft { get; init; } + public string? RemoteDraftTimelapseId { get; init; } +} diff --git a/src/app/Riverside.Elapsed.App/Models/Timelapses/Local/LocalSession.cs b/src/app/Riverside.Elapsed.App/Models/Timelapses/Local/LocalSession.cs index 0049be1..5b4f06f 100644 --- a/src/app/Riverside.Elapsed.App/Models/Timelapses/Local/LocalSession.cs +++ b/src/app/Riverside.Elapsed.App/Models/Timelapses/Local/LocalSession.cs @@ -1,11 +1,14 @@ +using Riverside.Elapsed.App.Models.Primitives; + namespace Riverside.Elapsed.App.Models.Timelapses.Local; -public class LocalSession +public sealed record LocalSession : IUploadable { - public Guid LocalSessionId; - public string FilePath; - public long FileSizeBytes; - // RecordedAtStart, RecordedAtEnd - //public SessionEncryptionState Encryption; - public TusUploadState Upload; + public Guid LocalSessionId { get; init; } + + public string FilePath { get; init; } = string.Empty; + public long FileSizeBytes { get; init; } + + public string? UploadToken { get; init; } + public TusUploadState Upload { get; init; } = new(); } diff --git a/src/app/Riverside.Elapsed.App/Models/Timelapses/Local/LocalThumbnail.cs b/src/app/Riverside.Elapsed.App/Models/Timelapses/Local/LocalThumbnail.cs index ca1487a..f927bf3 100644 --- a/src/app/Riverside.Elapsed.App/Models/Timelapses/Local/LocalThumbnail.cs +++ b/src/app/Riverside.Elapsed.App/Models/Timelapses/Local/LocalThumbnail.cs @@ -1,13 +1,15 @@ using System; using System.Collections.Generic; using System.Text; +using Riverside.Elapsed.App.Models.Primitives; namespace Riverside.Elapsed.App.Models.Timelapses.Local; -public class LocalThumbnail +public sealed record LocalThumbnail : IUploadable { - public string FilePath; - public long FileSizeBytes; - //public ThumbnailEncryptionState Encryption; - public TusUploadState Upload; + public string FilePath { get; init; } = string.Empty; + public long FileSizeBytes { get; init; } + + public string? UploadToken { get; init; } + public TusUploadState Upload { get; init; } = new(); } diff --git a/src/app/Riverside.Elapsed.App/Models/Timelapses/Local/RemoteDraftSync.cs b/src/app/Riverside.Elapsed.App/Models/Timelapses/Local/RemoteDraftSync.cs index 28c5021..1159688 100644 --- a/src/app/Riverside.Elapsed.App/Models/Timelapses/Local/RemoteDraftSync.cs +++ b/src/app/Riverside.Elapsed.App/Models/Timelapses/Local/RemoteDraftSync.cs @@ -2,8 +2,6 @@ namespace Riverside.Elapsed.App.Models.Timelapses.Local; public class RemoteDraftSync { - public string DraftTimelapseId; - public string IvHex; - public List SessionUploadTokens; - public string ThumbnailUploadToken; + public string DraftTimelapseId { get; init; } = string.Empty; + public string IvHex { get; init; } = string.Empty; // draft IV as hex string (not byte[] because json serialiser will get confused) } diff --git a/src/app/Riverside.Elapsed.App/Models/Timelapses/Local/TusUploadState.cs b/src/app/Riverside.Elapsed.App/Models/Timelapses/Local/TusUploadState.cs index c3edbbd..3daafc3 100644 --- a/src/app/Riverside.Elapsed.App/Models/Timelapses/Local/TusUploadState.cs +++ b/src/app/Riverside.Elapsed.App/Models/Timelapses/Local/TusUploadState.cs @@ -2,11 +2,13 @@ namespace Riverside.Elapsed.App.Models.Timelapses.Local; public class TusUploadState { - public string? UploadUrl; - public long BytesUploaded; - public long BytesTotal; - public bool IsComplete; - public string? LastError; - public DateTimeOffset StartedAt; - public DateTimeOffset? CompletedAt; + public string? UploadUrl { get; init; } + + public long BytesUploaded { get; init; } + public long BytesTotal { get; init; } + public bool IsComplete { get; init; } + public string? LastError { get; init; } + + public DateTimeOffset StartedAt { get; init; } + public DateTimeOffset? CompletedAt { get; init; } } diff --git a/src/app/Riverside.Elapsed.App/Services/Drafts/DraftsServiceCollectionExtensions.cs b/src/app/Riverside.Elapsed.App/Services/Drafts/DraftsServiceCollectionExtensions.cs new file mode 100644 index 0000000..0400925 --- /dev/null +++ b/src/app/Riverside.Elapsed.App/Services/Drafts/DraftsServiceCollectionExtensions.cs @@ -0,0 +1,12 @@ +using Riverside.Elapsed.App.Services.Drafts; + +namespace Riverside.Elapsed.App.Extensions; + +public static class DraftsServiceCollectionExtensions +{ + public static IServiceCollection AddDrafts(this IServiceCollection services) + { + services.AddSingleton(); + return services; + } +} diff --git a/src/app/Riverside.Elapsed.App/Services/Drafts/ILocalDraftRepository.cs b/src/app/Riverside.Elapsed.App/Services/Drafts/ILocalDraftRepository.cs new file mode 100644 index 0000000..6f2580e --- /dev/null +++ b/src/app/Riverside.Elapsed.App/Services/Drafts/ILocalDraftRepository.cs @@ -0,0 +1,12 @@ +using Riverside.Elapsed.App.Models.Timelapses.Local; + +namespace Riverside.Elapsed.App.Services.Drafts; + +public interface ILocalDraftRepository +{ + Task GetIndexAsync(CancellationToken ct = default); + Task GetDraftAsync(Guid localDraftId, CancellationToken ct = default); + + Task SaveDraftAsync(LocalDraft draft, CancellationToken ct = default); + Task DeleteDraftAsync(Guid localDraftId, CancellationToken ct = default); +} diff --git a/src/app/Riverside.Elapsed.App/Services/Drafts/LocalDraftRepository.cs b/src/app/Riverside.Elapsed.App/Services/Drafts/LocalDraftRepository.cs new file mode 100644 index 0000000..55c1a18 --- /dev/null +++ b/src/app/Riverside.Elapsed.App/Services/Drafts/LocalDraftRepository.cs @@ -0,0 +1,57 @@ +using Riverside.Elapsed.App.Models.Timelapses.Local; +using Riverside.Elapsed.App.Services.Storage; + +namespace Riverside.Elapsed.App.Services.Drafts; + +public sealed class LocalDraftRepository(ILocalJsonStore store) : ILocalDraftRepository +{ + private const string DraftsDir = "drafts"; + private static readonly string IndexPath = Path.Combine(DraftsDir, "index.json"); + + private static string DraftPath(Guid id) => Path.Combine(DraftsDir, $"{id:D}.json"); + + public async Task GetIndexAsync(CancellationToken ct = default) + { + return await store.ReadAsync(IndexPath, ct).ConfigureAwait(false) + ?? new LocalDraftIndex(); + } + + public Task GetDraftAsync(Guid localDraftId, CancellationToken ct = default) + => store.ReadAsync(DraftPath(localDraftId), ct); + + public async Task SaveDraftAsync(LocalDraft draft, CancellationToken ct = default) + { + await store.WriteAsync(DraftPath(draft.LocalDraftId), draft, ct).ConfigureAwait(false); // persist draft contents + + var index = await GetIndexAsync(ct).ConfigureAwait(false); + var newItem = new LocalDraftIndexItem + { + LocalDraftId = draft.LocalDraftId, + Name = draft.Name, + LastModifiedAt = draft.LastModifiedAt, + HasRemoteDraft = draft.Remote is not null, + RemoteDraftTimelapseId = draft.Remote?.DraftTimelapseId, + }; + + var updated = index.Drafts // replace existing & sort newest first + .Where(x => x.LocalDraftId != draft.LocalDraftId) + .Append(newItem) + .OrderByDescending(x => x.LastModifiedAt) + .ToArray(); + + await store.WriteAsync(IndexPath, index with { Drafts = updated }, ct).ConfigureAwait(false); + } + + public async Task DeleteDraftAsync(Guid localDraftId, CancellationToken ct = default) + { + await store.DeleteAsync(DraftPath(localDraftId), ct).ConfigureAwait(false); + + var index = await GetIndexAsync(ct).ConfigureAwait(false); + var updated = index.Drafts.Where(x => x.LocalDraftId != localDraftId).ToArray(); + + if (updated.Length == index.Drafts.Count) + return; + + await store.WriteAsync(IndexPath, index with { Drafts = updated }, ct).ConfigureAwait(false); + } +} diff --git a/src/app/Riverside.Elapsed.App/Services/Storage/ILocalJsonStore.cs b/src/app/Riverside.Elapsed.App/Services/Storage/ILocalJsonStore.cs new file mode 100644 index 0000000..e3fb216 --- /dev/null +++ b/src/app/Riverside.Elapsed.App/Services/Storage/ILocalJsonStore.cs @@ -0,0 +1,9 @@ +namespace Riverside.Elapsed.App.Services.Storage; + +public interface ILocalJsonStore +{ + Task ReadAsync(string relativePath, CancellationToken ct = default); + Task WriteAsync(string relativePath, T value, CancellationToken ct = default); + Task ExistsAsync(string relativePath, CancellationToken ct = default); + Task DeleteAsync(string relativePath, CancellationToken ct = default); +} diff --git a/src/app/Riverside.Elapsed.App/Services/Storage/LocalJsonStore.cs b/src/app/Riverside.Elapsed.App/Services/Storage/LocalJsonStore.cs new file mode 100644 index 0000000..a5dc78f --- /dev/null +++ b/src/app/Riverside.Elapsed.App/Services/Storage/LocalJsonStore.cs @@ -0,0 +1,66 @@ +namespace Riverside.Elapsed.App.Services.Storage; + +public sealed class LocalJsonStore(ISerializer serializer) : ILocalJsonStore +{ + // TODO: inject something better later + private static string BasePath + => Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.LocalApplicationData), "Riverside", "Elapsed"); + + private static string FullPath(string relativePath) + => Path.Combine(BasePath, relativePath.Replace('\\', Path.DirectorySeparatorChar).Replace('/', Path.DirectorySeparatorChar)); + + public Task ExistsAsync(string relativePath, CancellationToken ct = default) + => Task.FromResult(File.Exists(FullPath(relativePath))); + + public Task DeleteAsync(string relativePath, CancellationToken ct = default) + { + var path = FullPath(relativePath); + if (File.Exists(path)) + File.Delete(path); + return Task.CompletedTask; + } + + public async Task ReadAsync(string relativePath, CancellationToken ct = default) + { + var path = FullPath(relativePath); + if (!File.Exists(path)) + return default; + + await using var stream = File.OpenRead(path); + return (T?)serializer.FromStream(stream, typeof(T)); + } + + public async Task WriteAsync(string relativePath, T value, CancellationToken ct = default) + { + var path = FullPath(relativePath); + Directory.CreateDirectory(Path.GetDirectoryName(path)!); + + var tmp = path + ".tmp"; + var bak = path + ".bak"; + + await using (var stream = File.Create(tmp)) + { + serializer.ToStream(stream, value!, typeof(T)); + await stream.FlushAsync(ct).ConfigureAwait(false); + } + + // TODO: ensure compatibility with other systems + if (File.Exists(path)) + { + try + { + File.Replace(tmp, path, bak, ignoreMetadataErrors: true); + if (File.Exists(bak)) + { + File.Delete(bak); + } + return; + } + catch (PlatformNotSupportedException) { } + catch (IOException) { } + File.Delete(path); + } + + File.Move(tmp, path); + } +} diff --git a/src/platforms/Riverside.Elapsed.App/App.xaml.cs b/src/platforms/Riverside.Elapsed.App/App.xaml.cs index f3f7fb3..2711cfe 100644 --- a/src/platforms/Riverside.Elapsed.App/App.xaml.cs +++ b/src/platforms/Riverside.Elapsed.App/App.xaml.cs @@ -2,6 +2,7 @@ using Uno.Resizetizer; using Riverside.Elapsed; using Riverside.Elapsed.App.ViewModels; +using Riverside.Elapsed.App.Extensions; namespace Riverside.Elapsed.App; @@ -85,6 +86,7 @@ protected async override void OnLaunched(LaunchActivatedEventArgs args) { // TODO: Register your services //services.AddSingleton(); + services.AddDrafts(); }) .UseNavigation(RegisterRoutes) .UseSerialization(serialization => From ccdd0fd98c0dd6b62da1fd517d7a8ba5c12f79d5 Mon Sep 17 00:00:00 2001 From: Lamparter <71598437+Lamparter@users.noreply.github.com> Date: Fri, 27 Mar 2026 18:10:56 +0000 Subject: [PATCH 06/10] Undo unwanted configuration change --- Elapsed.slnx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Elapsed.slnx b/Elapsed.slnx index 73e594b..fa6229e 100644 --- a/Elapsed.slnx +++ b/Elapsed.slnx @@ -1,6 +1,6 @@ - + From 2534784a3a00bdd3fd82fb506ccdf8315b8bd4e1 Mon Sep 17 00:00:00 2001 From: Lamparter <71598437+Lamparter@users.noreply.github.com> Date: Fri, 27 Mar 2026 22:51:44 +0000 Subject: [PATCH 07/10] Add primitive draft view models --- .../Timelapses/Drafts/DraftDetailArgs.cs | 3 ++ .../Drafts/DraftListItemViewModel.cs | 19 +++++++++ .../Timelapses/Drafts/DraftViewModel.cs | 39 +++++++++++++++++++ 3 files changed, 61 insertions(+) create mode 100644 src/app/Riverside.Elapsed.App/ViewModels/Timelapses/Drafts/DraftDetailArgs.cs create mode 100644 src/app/Riverside.Elapsed.App/ViewModels/Timelapses/Drafts/DraftListItemViewModel.cs create mode 100644 src/app/Riverside.Elapsed.App/ViewModels/Timelapses/Drafts/DraftViewModel.cs diff --git a/src/app/Riverside.Elapsed.App/ViewModels/Timelapses/Drafts/DraftDetailArgs.cs b/src/app/Riverside.Elapsed.App/ViewModels/Timelapses/Drafts/DraftDetailArgs.cs new file mode 100644 index 0000000..9f2e5c7 --- /dev/null +++ b/src/app/Riverside.Elapsed.App/ViewModels/Timelapses/Drafts/DraftDetailArgs.cs @@ -0,0 +1,3 @@ +namespace Riverside.Elapsed.App.ViewModels.Timelapses.Drafts; + +public sealed record DraftDetailArgs(Guid LocalDraftId); diff --git a/src/app/Riverside.Elapsed.App/ViewModels/Timelapses/Drafts/DraftListItemViewModel.cs b/src/app/Riverside.Elapsed.App/ViewModels/Timelapses/Drafts/DraftListItemViewModel.cs new file mode 100644 index 0000000..affb155 --- /dev/null +++ b/src/app/Riverside.Elapsed.App/ViewModels/Timelapses/Drafts/DraftListItemViewModel.cs @@ -0,0 +1,19 @@ +namespace Riverside.Elapsed.App.ViewModels.Timelapses.Drafts; + +public sealed partial class DraftListItemViewModel +{ + public Guid LocalDraftId { get; } + public string Name { get; } + public DateTimeOffset LastModifiedAt { get; } + public bool HasRemoteDraft { get; } + public string? RemoteDraftTimelapseId { get; } + + public DraftListItemViewModel(Guid localDraftId, string name, DateTimeOffset lastModifiedAt, bool hasRemoteDraft, string? remoteDraftTimelapseId) + { + LocalDraftId = localDraftId; + Name = name; + LastModifiedAt = lastModifiedAt; + HasRemoteDraft = hasRemoteDraft; + RemoteDraftTimelapseId = remoteDraftTimelapseId; + } +} diff --git a/src/app/Riverside.Elapsed.App/ViewModels/Timelapses/Drafts/DraftViewModel.cs b/src/app/Riverside.Elapsed.App/ViewModels/Timelapses/Drafts/DraftViewModel.cs new file mode 100644 index 0000000..2350bda --- /dev/null +++ b/src/app/Riverside.Elapsed.App/ViewModels/Timelapses/Drafts/DraftViewModel.cs @@ -0,0 +1,39 @@ +using System.Collections.ObjectModel; +using Riverside.Elapsed.App.Services.Drafts; + +namespace Riverside.Elapsed.App.ViewModels.Timelapses.Drafts; + +public sealed partial class DraftViewModel : ObservableObject +{ + private readonly ILocalDraftRepository _drafts; + private readonly INavigator _navigator; + + [ObservableProperty] + private bool _isLoading; + + [ObservableProperty] + private string? _errorMessage; + + public ObservableCollection Items { get; } + + public IAsyncRelayCommand RefreshCommand { get; } + public IAsyncRelayCommand CreateNewDraftCommand { get; } + public IAsyncRelayCommand DeleteDraftCommand { get; } + public IAsyncRelayCommand OpenDraftCommand { get; } + + public DraftsViewModel(ILocalDraftRepository drafts) + { + _drafts = drafts; + } + + public async Task RefreshAsync() + { + try + { + ErrorMessage = null; + IsLoading = true; + + var index = await _drafts.GetIndexAsync().ConfigureAwait(false); + } + } +} From 205613e7bf43cf93fbd1dcf7c9e63318466a0aa4 Mon Sep 17 00:00:00 2001 From: Lamparter <71598437+Lamparter@users.noreply.github.com> Date: Mon, 13 Apr 2026 17:16:22 +0100 Subject: [PATCH 08/10] Add new view models --- .../Drafts/DraftDetailsViewModel.cs | 163 ++++++++++++++++++ .../{DraftDetailArgs.cs => DraftListItem.cs} | 2 +- .../Timelapses/Drafts/DraftViewModel.cs | 39 ----- .../Timelapses/Drafts/DraftsViewModel.cs | 134 ++++++++++++++ .../Riverside.Elapsed.App/App.xaml.cs | 15 +- 5 files changed, 309 insertions(+), 44 deletions(-) create mode 100644 src/app/Riverside.Elapsed.App/ViewModels/Timelapses/Drafts/DraftDetailsViewModel.cs rename src/app/Riverside.Elapsed.App/ViewModels/Timelapses/Drafts/{DraftDetailArgs.cs => DraftListItem.cs} (52%) delete mode 100644 src/app/Riverside.Elapsed.App/ViewModels/Timelapses/Drafts/DraftViewModel.cs create mode 100644 src/app/Riverside.Elapsed.App/ViewModels/Timelapses/Drafts/DraftsViewModel.cs diff --git a/src/app/Riverside.Elapsed.App/ViewModels/Timelapses/Drafts/DraftDetailsViewModel.cs b/src/app/Riverside.Elapsed.App/ViewModels/Timelapses/Drafts/DraftDetailsViewModel.cs new file mode 100644 index 0000000..e799301 --- /dev/null +++ b/src/app/Riverside.Elapsed.App/ViewModels/Timelapses/Drafts/DraftDetailsViewModel.cs @@ -0,0 +1,163 @@ +using Riverside.Elapsed.App.Models.Timelapses.Local; +using Riverside.Elapsed.App.Services.Drafts; + +namespace Riverside.Elapsed.App.ViewModels.Timelapses.Drafts; + +public sealed partial class DraftDetailsViewModel : ObservableObject +{ + private readonly ILocalDraftRepository _drafts; + private readonly INavigator _navigator; + + private CancellationTokenSource? _autosaveCts; + + [ObservableProperty] + private bool _isLoading; + + [ObservableProperty] + private string? _errorMessage; + + [ObservableProperty] + private LocalDraft? _draft; + + public DraftListItem Args { get; private set; } + + public string Name + { + get => Draft?.Name ?? ""; + set + { + if (Draft is null) + return; + if (Draft.Name == value) + return; + + Draft = Draft with + { + Name = value, + LastModifiedAt = DateTimeOffset.UtcNow, + }; + + OnPropertyChanged(nameof(Name)); + TriggerAutosave(); + SaveCommand.NotifyCanExecuteChanged(); + } + } + + public string Description + { + get => Draft?.Description ?? ""; + set + { + if (Draft is null) + return; + if (Draft.Description == value) + return; + + Draft = Draft with + { + Description = value, + LastModifiedAt = DateTimeOffset.UtcNow, + }; + + OnPropertyChanged(nameof(Description)); + TriggerAutosave(); + SaveCommand.NotifyCanExecuteChanged(); + } + } + + public IAsyncRelayCommand SaveCommand { get; } + public IAsyncRelayCommand ReloadCommand { get; } + + public DraftDetailsViewModel(ILocalDraftRepository drafts, INavigator navigator) + { + _drafts = drafts; + _navigator = navigator; + + SaveCommand = new AsyncRelayCommand(SaveAsync, CanSave); + ReloadCommand = new AsyncRelayCommand(ReloadAsync); + } + + public async Task OnNavigatedToAsync(DraftListItem args) + { + Args = args; + await ReloadAsync(); + } + + private async Task ReloadAsync() + { + if (Args is null) + { + ErrorMessage = "Missing navigation arguments."; + return; + } + + try + { + ErrorMessage = null; + IsLoading = true; + + var loaded = await _drafts.GetDraftAsync(Args.LocalDraftId); + if (loaded is null) + { + ErrorMessage = "Draft not found (it may have been deleted)."; + Draft = null; + return; + } + + Draft = loaded; + + OnPropertyChanged(nameof(Name)); + OnPropertyChanged(nameof(Description)); + SaveCommand.NotifyCanExecuteChanged(); + } + catch (Exception ex) + { + ErrorMessage = ex.Message; + } + finally + { + IsLoading = false; + } + } + + private bool CanSave() + => Draft is not null; + + private async Task SaveAsync() + { + if (Draft is null) + return; + + try + { + ErrorMessage = null; + await _drafts.SaveDraftAsync(Draft); + } + catch (Exception ex) + { + ErrorMessage = ex.Message; + } + } + + private void TriggerAutosave() + { + _autosaveCts?.Cancel(); + _autosaveCts?.Dispose(); + + _autosaveCts = new(); + var token = _autosaveCts.Token; + + _ = Task.Run(async () => + { + try + { + await Task.Delay(TimeSpan.FromMilliseconds(500), token); + if (token.IsCancellationRequested) + return; + + await SaveAsync(); + } + catch (OperationCanceledException) { } + }, token); + } +} diff --git a/src/app/Riverside.Elapsed.App/ViewModels/Timelapses/Drafts/DraftDetailArgs.cs b/src/app/Riverside.Elapsed.App/ViewModels/Timelapses/Drafts/DraftListItem.cs similarity index 52% rename from src/app/Riverside.Elapsed.App/ViewModels/Timelapses/Drafts/DraftDetailArgs.cs rename to src/app/Riverside.Elapsed.App/ViewModels/Timelapses/Drafts/DraftListItem.cs index 9f2e5c7..77ef317 100644 --- a/src/app/Riverside.Elapsed.App/ViewModels/Timelapses/Drafts/DraftDetailArgs.cs +++ b/src/app/Riverside.Elapsed.App/ViewModels/Timelapses/Drafts/DraftListItem.cs @@ -1,3 +1,3 @@ namespace Riverside.Elapsed.App.ViewModels.Timelapses.Drafts; -public sealed record DraftDetailArgs(Guid LocalDraftId); +public sealed record DraftListItem(Guid LocalDraftId); diff --git a/src/app/Riverside.Elapsed.App/ViewModels/Timelapses/Drafts/DraftViewModel.cs b/src/app/Riverside.Elapsed.App/ViewModels/Timelapses/Drafts/DraftViewModel.cs deleted file mode 100644 index 2350bda..0000000 --- a/src/app/Riverside.Elapsed.App/ViewModels/Timelapses/Drafts/DraftViewModel.cs +++ /dev/null @@ -1,39 +0,0 @@ -using System.Collections.ObjectModel; -using Riverside.Elapsed.App.Services.Drafts; - -namespace Riverside.Elapsed.App.ViewModels.Timelapses.Drafts; - -public sealed partial class DraftViewModel : ObservableObject -{ - private readonly ILocalDraftRepository _drafts; - private readonly INavigator _navigator; - - [ObservableProperty] - private bool _isLoading; - - [ObservableProperty] - private string? _errorMessage; - - public ObservableCollection Items { get; } - - public IAsyncRelayCommand RefreshCommand { get; } - public IAsyncRelayCommand CreateNewDraftCommand { get; } - public IAsyncRelayCommand DeleteDraftCommand { get; } - public IAsyncRelayCommand OpenDraftCommand { get; } - - public DraftsViewModel(ILocalDraftRepository drafts) - { - _drafts = drafts; - } - - public async Task RefreshAsync() - { - try - { - ErrorMessage = null; - IsLoading = true; - - var index = await _drafts.GetIndexAsync().ConfigureAwait(false); - } - } -} diff --git a/src/app/Riverside.Elapsed.App/ViewModels/Timelapses/Drafts/DraftsViewModel.cs b/src/app/Riverside.Elapsed.App/ViewModels/Timelapses/Drafts/DraftsViewModel.cs new file mode 100644 index 0000000..5b0e6bf --- /dev/null +++ b/src/app/Riverside.Elapsed.App/ViewModels/Timelapses/Drafts/DraftsViewModel.cs @@ -0,0 +1,134 @@ +using System.Collections.ObjectModel; +using Riverside.Elapsed.App.Models.Timelapses.Local; +using Riverside.Elapsed.App.Services.Drafts; + +namespace Riverside.Elapsed.App.ViewModels.Timelapses.Drafts; + +public sealed partial class DraftsViewModel : ObservableObject +{ + private readonly ILocalDraftRepository _drafts; + private readonly INavigator _navigator; + private readonly IDispatcher _dispatcher; + + [ObservableProperty] + private bool _isLoading; + + [ObservableProperty] + private string? _errorMessage; + + public ObservableCollection Items { get; } + + public IAsyncRelayCommand RefreshCommand { get; } + public IAsyncRelayCommand CreateNewDraftCommand { get; } + public IAsyncRelayCommand DeleteDraftCommand { get; } + public IAsyncRelayCommand OpenDraftCommand { get; } + + public DraftsViewModel(ILocalDraftRepository drafts, INavigator navigator, IDispatcher dispatcher) + { + _drafts = drafts; + _navigator = navigator; + _dispatcher = dispatcher; + + Items = []; + + RefreshCommand = new AsyncRelayCommand(RefreshAsync); + CreateNewDraftCommand = new AsyncRelayCommand(CreateNewDraftAsync); + DeleteDraftCommand = new AsyncRelayCommand(DeleteDraftAsync); + OpenDraftCommand = new AsyncRelayCommand(OpenDraftAsync); + } + + public async Task RefreshAsync() + { + try + { + ErrorMessage = null; + IsLoading = true; + + var index = await _drafts.GetIndexAsync(); + + await _dispatcher.ExecuteAsync(() => + { + Items.Clear(); + foreach (var d in index.Drafts) + { + Items.Add(new( + d.LocalDraftId, + string.IsNullOrWhiteSpace(d.Name) ? "Untitled draft" : d.Name, // TODO: Localise + d.LastModifiedAt, + d.HasRemoteDraft, + d.RemoteDraftTimelapseId)); + } + }); + } + catch (Exception ex) + { + ErrorMessage = ex.Message; + } + finally + { + IsLoading = false; + } + } + + public async Task CreateNewDraftAsync() + { + try + { + ErrorMessage = null; + + var deviceId = Guid.Empty; // TODO: Replace with local device registration store + var now = DateTimeOffset.UtcNow; + var draft = new LocalDraft + { + LocalDraftId = Guid.NewGuid(), + CreatedAt = now, + LastModifiedAt = now, + Name = string.Empty, + Description = string.Empty, + Snapshots = [], + EditList = [], + Sessions = [], + Thumbnail = new(), + Remote = null, + State = new(), + }; + + await _drafts.SaveDraftAsync(draft); + await RefreshAsync(); + await OpenDraftAsync(draft.LocalDraftId); + } + catch (Exception ex) + { + ErrorMessage = ex.Message; + } + } + + private async Task DeleteDraftAsync(Guid localDraftId) + { + try + { + ErrorMessage = null; + + await _drafts.DeleteDraftAsync(localDraftId); + await _dispatcher.ExecuteAsync(() => + { + var item = Items.FirstOrDefault(x => x.LocalDraftId == localDraftId); + if (item is not null) + { + Items.Remove(item); + } + }); + } + catch (Exception ex) + { + ErrorMessage = ex.Message; + } + } + + private Task OpenDraftAsync(Guid localDraftId) + { + return _navigator.NavigateViewModelAsync( + this, + data: new DraftItem(localDraftId)); + } +} diff --git a/src/platforms/Riverside.Elapsed.App/App.xaml.cs b/src/platforms/Riverside.Elapsed.App/App.xaml.cs index 2711cfe..8ca50de 100644 --- a/src/platforms/Riverside.Elapsed.App/App.xaml.cs +++ b/src/platforms/Riverside.Elapsed.App/App.xaml.cs @@ -3,6 +3,7 @@ using Riverside.Elapsed; using Riverside.Elapsed.App.ViewModels; using Riverside.Elapsed.App.Extensions; +using Riverside.Elapsed.App.ViewModels.Timelapses.Drafts; namespace Riverside.Elapsed.App; @@ -123,16 +124,22 @@ private static void RegisterRoutes(IViewRegistry views, IRouteRegistry routes) new ViewMap(ViewModel: typeof(ShellViewModel)), new ViewMap(), new ViewMap(), - new DataViewMap() + new DataViewMap(), + + new ViewMap(), + new DataViewMap(), ); routes.Register( new RouteMap("", View: views.FindByViewModel(), Nested: [ - new ("Login", View: views.FindByViewModel()), - new ("Main", View: views.FindByViewModel(), IsDefault:true), - new ("Second", View: views.FindByViewModel()), + new("Login", View: views.FindByViewModel()), + new("Main", View: views.FindByViewModel(), IsDefault:true), + new("Second", View: views.FindByViewModel()), + + new("Drafts", View: views.FindByViewModel()), + new("Draft", View: views.FindByViewModel()), ] ) ); From 7820e5dae7bcf9f156e35f7b7eedc983a3ecf466 Mon Sep 17 00:00:00 2001 From: Lamparter <71598437+Lamparter@users.noreply.github.com> Date: Sun, 3 May 2026 23:42:43 +0100 Subject: [PATCH 09/10] Revert "Undo unwanted configuration change" This reverts commit ccdd0fd98c0dd6b62da1fd517d7a8ba5c12f79d5. --- Elapsed.slnx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Elapsed.slnx b/Elapsed.slnx index fa6229e..73e594b 100644 --- a/Elapsed.slnx +++ b/Elapsed.slnx @@ -1,6 +1,6 @@ - + From d6847b70a3c35cfe0b973768530e484dc02439e2 Mon Sep 17 00:00:00 2001 From: Lamparter <71598437+Lamparter@users.noreply.github.com> Date: Sun, 3 May 2026 23:43:50 +0100 Subject: [PATCH 10/10] Move files --- src/{app => platforms}/Riverside.Elapsed.App/Constants.cs | 0 .../Converters/Json/TimeSpanSecondsJsonConverter.cs | 0 .../Riverside.Elapsed.App/Converters/Json/UriJsonConverter.cs | 0 .../Riverside.Elapsed.App/Models/Admin/AdminListPage.cs | 0 .../Riverside.Elapsed.App/Models/Admin/AdminSearchResult.cs | 0 .../Riverside.Elapsed.App/Models/Admin/AdminStats.cs | 0 .../Riverside.Elapsed.App/Models/Admin/EntityType.cs | 0 .../Riverside.Elapsed.App/Models/Developer/DeveloperApp.cs | 0 .../Riverside.Elapsed.App/Models/Developer/OAuthGrant.cs | 0 .../Riverside.Elapsed.App/Models/Developer/TrustLevel.cs | 0 .../Riverside.Elapsed.App/Models/Primitives/IUploadable.cs | 0 .../Riverside.Elapsed.App/Models/Timelapses/Comment.cs | 0 .../Riverside.Elapsed.App/Models/Timelapses/CursorPage{T}.cs | 0 .../Riverside.Elapsed.App/Models/Timelapses/DraftEdit.cs | 0 .../Riverside.Elapsed.App/Models/Timelapses/DraftTimelapse.cs | 0 .../Riverside.Elapsed.App/Models/Timelapses/EditKind.cs | 0 .../Models/Timelapses/Local/DraftPipelineState.cs | 0 .../Riverside.Elapsed.App/Models/Timelapses/Local/LocalDraft.cs | 0 .../Models/Timelapses/Local/LocalDraftIndex.cs | 0 .../Models/Timelapses/Local/LocalDraftIndexItem.cs | 0 .../Riverside.Elapsed.App/Models/Timelapses/Local/LocalSession.cs | 0 .../Models/Timelapses/Local/LocalThumbnail.cs | 0 .../Models/Timelapses/Local/RemoteDraftSync.cs | 0 .../Models/Timelapses/Local/TusUploadState.cs | 0 .../Riverside.Elapsed.App/Models/Timelapses/Timelapse.cs | 0 .../Riverside.Elapsed.App/Models/Timelapses/Visibility.cs | 0 .../Riverside.Elapsed.App/Models/User/Device.cs | 0 .../Riverside.Elapsed.App/Models/User/Local/DeviceKey.cs | 0 .../Riverside.Elapsed.App/Models/User/Local/KeyRelayRequest.cs | 0 .../Riverside.Elapsed.App/Models/User/Local/KeyRelayResult.cs | 0 .../Riverside.Elapsed.App/Models/User/Myself.cs | 0 .../Riverside.Elapsed.App/Models/User/PermissionLevel.cs | 0 src/{app => platforms}/Riverside.Elapsed.App/Models/User/User.cs | 0 .../Services/Drafts/DraftsServiceCollectionExtensions.cs | 0 .../Services/Drafts/ILocalDraftRepository.cs | 0 .../Riverside.Elapsed.App/Services/Drafts/LocalDraftRepository.cs | 0 .../Riverside.Elapsed.App/Services/Storage/ILocalJsonStore.cs | 0 .../Riverside.Elapsed.App/Services/Storage/LocalJsonStore.cs | 0 .../ViewModels/Timelapses/Drafts/DraftDetailsViewModel.cs | 0 .../ViewModels/Timelapses/Drafts/DraftListItem.cs | 0 .../ViewModels/Timelapses/Drafts/DraftListItemViewModel.cs | 0 .../ViewModels/Timelapses/Drafts/DraftsViewModel.cs | 0 42 files changed, 0 insertions(+), 0 deletions(-) rename src/{app => platforms}/Riverside.Elapsed.App/Constants.cs (100%) rename src/{app => platforms}/Riverside.Elapsed.App/Converters/Json/TimeSpanSecondsJsonConverter.cs (100%) rename src/{app => platforms}/Riverside.Elapsed.App/Converters/Json/UriJsonConverter.cs (100%) rename src/{app => platforms}/Riverside.Elapsed.App/Models/Admin/AdminListPage.cs (100%) rename src/{app => platforms}/Riverside.Elapsed.App/Models/Admin/AdminSearchResult.cs (100%) rename src/{app => platforms}/Riverside.Elapsed.App/Models/Admin/AdminStats.cs (100%) rename src/{app => platforms}/Riverside.Elapsed.App/Models/Admin/EntityType.cs (100%) rename src/{app => platforms}/Riverside.Elapsed.App/Models/Developer/DeveloperApp.cs (100%) rename src/{app => platforms}/Riverside.Elapsed.App/Models/Developer/OAuthGrant.cs (100%) rename src/{app => platforms}/Riverside.Elapsed.App/Models/Developer/TrustLevel.cs (100%) rename src/{app => platforms}/Riverside.Elapsed.App/Models/Primitives/IUploadable.cs (100%) rename src/{app => platforms}/Riverside.Elapsed.App/Models/Timelapses/Comment.cs (100%) rename src/{app => platforms}/Riverside.Elapsed.App/Models/Timelapses/CursorPage{T}.cs (100%) rename src/{app => platforms}/Riverside.Elapsed.App/Models/Timelapses/DraftEdit.cs (100%) rename src/{app => platforms}/Riverside.Elapsed.App/Models/Timelapses/DraftTimelapse.cs (100%) rename src/{app => platforms}/Riverside.Elapsed.App/Models/Timelapses/EditKind.cs (100%) rename src/{app => platforms}/Riverside.Elapsed.App/Models/Timelapses/Local/DraftPipelineState.cs (100%) rename src/{app => platforms}/Riverside.Elapsed.App/Models/Timelapses/Local/LocalDraft.cs (100%) rename src/{app => platforms}/Riverside.Elapsed.App/Models/Timelapses/Local/LocalDraftIndex.cs (100%) rename src/{app => platforms}/Riverside.Elapsed.App/Models/Timelapses/Local/LocalDraftIndexItem.cs (100%) rename src/{app => platforms}/Riverside.Elapsed.App/Models/Timelapses/Local/LocalSession.cs (100%) rename src/{app => platforms}/Riverside.Elapsed.App/Models/Timelapses/Local/LocalThumbnail.cs (100%) rename src/{app => platforms}/Riverside.Elapsed.App/Models/Timelapses/Local/RemoteDraftSync.cs (100%) rename src/{app => platforms}/Riverside.Elapsed.App/Models/Timelapses/Local/TusUploadState.cs (100%) rename src/{app => platforms}/Riverside.Elapsed.App/Models/Timelapses/Timelapse.cs (100%) rename src/{app => platforms}/Riverside.Elapsed.App/Models/Timelapses/Visibility.cs (100%) rename src/{app => platforms}/Riverside.Elapsed.App/Models/User/Device.cs (100%) rename src/{app => platforms}/Riverside.Elapsed.App/Models/User/Local/DeviceKey.cs (100%) rename src/{app => platforms}/Riverside.Elapsed.App/Models/User/Local/KeyRelayRequest.cs (100%) rename src/{app => platforms}/Riverside.Elapsed.App/Models/User/Local/KeyRelayResult.cs (100%) rename src/{app => platforms}/Riverside.Elapsed.App/Models/User/Myself.cs (100%) rename src/{app => platforms}/Riverside.Elapsed.App/Models/User/PermissionLevel.cs (100%) rename src/{app => platforms}/Riverside.Elapsed.App/Models/User/User.cs (100%) rename src/{app => platforms}/Riverside.Elapsed.App/Services/Drafts/DraftsServiceCollectionExtensions.cs (100%) rename src/{app => platforms}/Riverside.Elapsed.App/Services/Drafts/ILocalDraftRepository.cs (100%) rename src/{app => platforms}/Riverside.Elapsed.App/Services/Drafts/LocalDraftRepository.cs (100%) rename src/{app => platforms}/Riverside.Elapsed.App/Services/Storage/ILocalJsonStore.cs (100%) rename src/{app => platforms}/Riverside.Elapsed.App/Services/Storage/LocalJsonStore.cs (100%) rename src/{app => platforms}/Riverside.Elapsed.App/ViewModels/Timelapses/Drafts/DraftDetailsViewModel.cs (100%) rename src/{app => platforms}/Riverside.Elapsed.App/ViewModels/Timelapses/Drafts/DraftListItem.cs (100%) rename src/{app => platforms}/Riverside.Elapsed.App/ViewModels/Timelapses/Drafts/DraftListItemViewModel.cs (100%) rename src/{app => platforms}/Riverside.Elapsed.App/ViewModels/Timelapses/Drafts/DraftsViewModel.cs (100%) diff --git a/src/app/Riverside.Elapsed.App/Constants.cs b/src/platforms/Riverside.Elapsed.App/Constants.cs similarity index 100% rename from src/app/Riverside.Elapsed.App/Constants.cs rename to src/platforms/Riverside.Elapsed.App/Constants.cs diff --git a/src/app/Riverside.Elapsed.App/Converters/Json/TimeSpanSecondsJsonConverter.cs b/src/platforms/Riverside.Elapsed.App/Converters/Json/TimeSpanSecondsJsonConverter.cs similarity index 100% rename from src/app/Riverside.Elapsed.App/Converters/Json/TimeSpanSecondsJsonConverter.cs rename to src/platforms/Riverside.Elapsed.App/Converters/Json/TimeSpanSecondsJsonConverter.cs diff --git a/src/app/Riverside.Elapsed.App/Converters/Json/UriJsonConverter.cs b/src/platforms/Riverside.Elapsed.App/Converters/Json/UriJsonConverter.cs similarity index 100% rename from src/app/Riverside.Elapsed.App/Converters/Json/UriJsonConverter.cs rename to src/platforms/Riverside.Elapsed.App/Converters/Json/UriJsonConverter.cs diff --git a/src/app/Riverside.Elapsed.App/Models/Admin/AdminListPage.cs b/src/platforms/Riverside.Elapsed.App/Models/Admin/AdminListPage.cs similarity index 100% rename from src/app/Riverside.Elapsed.App/Models/Admin/AdminListPage.cs rename to src/platforms/Riverside.Elapsed.App/Models/Admin/AdminListPage.cs diff --git a/src/app/Riverside.Elapsed.App/Models/Admin/AdminSearchResult.cs b/src/platforms/Riverside.Elapsed.App/Models/Admin/AdminSearchResult.cs similarity index 100% rename from src/app/Riverside.Elapsed.App/Models/Admin/AdminSearchResult.cs rename to src/platforms/Riverside.Elapsed.App/Models/Admin/AdminSearchResult.cs diff --git a/src/app/Riverside.Elapsed.App/Models/Admin/AdminStats.cs b/src/platforms/Riverside.Elapsed.App/Models/Admin/AdminStats.cs similarity index 100% rename from src/app/Riverside.Elapsed.App/Models/Admin/AdminStats.cs rename to src/platforms/Riverside.Elapsed.App/Models/Admin/AdminStats.cs diff --git a/src/app/Riverside.Elapsed.App/Models/Admin/EntityType.cs b/src/platforms/Riverside.Elapsed.App/Models/Admin/EntityType.cs similarity index 100% rename from src/app/Riverside.Elapsed.App/Models/Admin/EntityType.cs rename to src/platforms/Riverside.Elapsed.App/Models/Admin/EntityType.cs diff --git a/src/app/Riverside.Elapsed.App/Models/Developer/DeveloperApp.cs b/src/platforms/Riverside.Elapsed.App/Models/Developer/DeveloperApp.cs similarity index 100% rename from src/app/Riverside.Elapsed.App/Models/Developer/DeveloperApp.cs rename to src/platforms/Riverside.Elapsed.App/Models/Developer/DeveloperApp.cs diff --git a/src/app/Riverside.Elapsed.App/Models/Developer/OAuthGrant.cs b/src/platforms/Riverside.Elapsed.App/Models/Developer/OAuthGrant.cs similarity index 100% rename from src/app/Riverside.Elapsed.App/Models/Developer/OAuthGrant.cs rename to src/platforms/Riverside.Elapsed.App/Models/Developer/OAuthGrant.cs diff --git a/src/app/Riverside.Elapsed.App/Models/Developer/TrustLevel.cs b/src/platforms/Riverside.Elapsed.App/Models/Developer/TrustLevel.cs similarity index 100% rename from src/app/Riverside.Elapsed.App/Models/Developer/TrustLevel.cs rename to src/platforms/Riverside.Elapsed.App/Models/Developer/TrustLevel.cs diff --git a/src/app/Riverside.Elapsed.App/Models/Primitives/IUploadable.cs b/src/platforms/Riverside.Elapsed.App/Models/Primitives/IUploadable.cs similarity index 100% rename from src/app/Riverside.Elapsed.App/Models/Primitives/IUploadable.cs rename to src/platforms/Riverside.Elapsed.App/Models/Primitives/IUploadable.cs diff --git a/src/app/Riverside.Elapsed.App/Models/Timelapses/Comment.cs b/src/platforms/Riverside.Elapsed.App/Models/Timelapses/Comment.cs similarity index 100% rename from src/app/Riverside.Elapsed.App/Models/Timelapses/Comment.cs rename to src/platforms/Riverside.Elapsed.App/Models/Timelapses/Comment.cs diff --git a/src/app/Riverside.Elapsed.App/Models/Timelapses/CursorPage{T}.cs b/src/platforms/Riverside.Elapsed.App/Models/Timelapses/CursorPage{T}.cs similarity index 100% rename from src/app/Riverside.Elapsed.App/Models/Timelapses/CursorPage{T}.cs rename to src/platforms/Riverside.Elapsed.App/Models/Timelapses/CursorPage{T}.cs diff --git a/src/app/Riverside.Elapsed.App/Models/Timelapses/DraftEdit.cs b/src/platforms/Riverside.Elapsed.App/Models/Timelapses/DraftEdit.cs similarity index 100% rename from src/app/Riverside.Elapsed.App/Models/Timelapses/DraftEdit.cs rename to src/platforms/Riverside.Elapsed.App/Models/Timelapses/DraftEdit.cs diff --git a/src/app/Riverside.Elapsed.App/Models/Timelapses/DraftTimelapse.cs b/src/platforms/Riverside.Elapsed.App/Models/Timelapses/DraftTimelapse.cs similarity index 100% rename from src/app/Riverside.Elapsed.App/Models/Timelapses/DraftTimelapse.cs rename to src/platforms/Riverside.Elapsed.App/Models/Timelapses/DraftTimelapse.cs diff --git a/src/app/Riverside.Elapsed.App/Models/Timelapses/EditKind.cs b/src/platforms/Riverside.Elapsed.App/Models/Timelapses/EditKind.cs similarity index 100% rename from src/app/Riverside.Elapsed.App/Models/Timelapses/EditKind.cs rename to src/platforms/Riverside.Elapsed.App/Models/Timelapses/EditKind.cs diff --git a/src/app/Riverside.Elapsed.App/Models/Timelapses/Local/DraftPipelineState.cs b/src/platforms/Riverside.Elapsed.App/Models/Timelapses/Local/DraftPipelineState.cs similarity index 100% rename from src/app/Riverside.Elapsed.App/Models/Timelapses/Local/DraftPipelineState.cs rename to src/platforms/Riverside.Elapsed.App/Models/Timelapses/Local/DraftPipelineState.cs diff --git a/src/app/Riverside.Elapsed.App/Models/Timelapses/Local/LocalDraft.cs b/src/platforms/Riverside.Elapsed.App/Models/Timelapses/Local/LocalDraft.cs similarity index 100% rename from src/app/Riverside.Elapsed.App/Models/Timelapses/Local/LocalDraft.cs rename to src/platforms/Riverside.Elapsed.App/Models/Timelapses/Local/LocalDraft.cs diff --git a/src/app/Riverside.Elapsed.App/Models/Timelapses/Local/LocalDraftIndex.cs b/src/platforms/Riverside.Elapsed.App/Models/Timelapses/Local/LocalDraftIndex.cs similarity index 100% rename from src/app/Riverside.Elapsed.App/Models/Timelapses/Local/LocalDraftIndex.cs rename to src/platforms/Riverside.Elapsed.App/Models/Timelapses/Local/LocalDraftIndex.cs diff --git a/src/app/Riverside.Elapsed.App/Models/Timelapses/Local/LocalDraftIndexItem.cs b/src/platforms/Riverside.Elapsed.App/Models/Timelapses/Local/LocalDraftIndexItem.cs similarity index 100% rename from src/app/Riverside.Elapsed.App/Models/Timelapses/Local/LocalDraftIndexItem.cs rename to src/platforms/Riverside.Elapsed.App/Models/Timelapses/Local/LocalDraftIndexItem.cs diff --git a/src/app/Riverside.Elapsed.App/Models/Timelapses/Local/LocalSession.cs b/src/platforms/Riverside.Elapsed.App/Models/Timelapses/Local/LocalSession.cs similarity index 100% rename from src/app/Riverside.Elapsed.App/Models/Timelapses/Local/LocalSession.cs rename to src/platforms/Riverside.Elapsed.App/Models/Timelapses/Local/LocalSession.cs diff --git a/src/app/Riverside.Elapsed.App/Models/Timelapses/Local/LocalThumbnail.cs b/src/platforms/Riverside.Elapsed.App/Models/Timelapses/Local/LocalThumbnail.cs similarity index 100% rename from src/app/Riverside.Elapsed.App/Models/Timelapses/Local/LocalThumbnail.cs rename to src/platforms/Riverside.Elapsed.App/Models/Timelapses/Local/LocalThumbnail.cs diff --git a/src/app/Riverside.Elapsed.App/Models/Timelapses/Local/RemoteDraftSync.cs b/src/platforms/Riverside.Elapsed.App/Models/Timelapses/Local/RemoteDraftSync.cs similarity index 100% rename from src/app/Riverside.Elapsed.App/Models/Timelapses/Local/RemoteDraftSync.cs rename to src/platforms/Riverside.Elapsed.App/Models/Timelapses/Local/RemoteDraftSync.cs diff --git a/src/app/Riverside.Elapsed.App/Models/Timelapses/Local/TusUploadState.cs b/src/platforms/Riverside.Elapsed.App/Models/Timelapses/Local/TusUploadState.cs similarity index 100% rename from src/app/Riverside.Elapsed.App/Models/Timelapses/Local/TusUploadState.cs rename to src/platforms/Riverside.Elapsed.App/Models/Timelapses/Local/TusUploadState.cs diff --git a/src/app/Riverside.Elapsed.App/Models/Timelapses/Timelapse.cs b/src/platforms/Riverside.Elapsed.App/Models/Timelapses/Timelapse.cs similarity index 100% rename from src/app/Riverside.Elapsed.App/Models/Timelapses/Timelapse.cs rename to src/platforms/Riverside.Elapsed.App/Models/Timelapses/Timelapse.cs diff --git a/src/app/Riverside.Elapsed.App/Models/Timelapses/Visibility.cs b/src/platforms/Riverside.Elapsed.App/Models/Timelapses/Visibility.cs similarity index 100% rename from src/app/Riverside.Elapsed.App/Models/Timelapses/Visibility.cs rename to src/platforms/Riverside.Elapsed.App/Models/Timelapses/Visibility.cs diff --git a/src/app/Riverside.Elapsed.App/Models/User/Device.cs b/src/platforms/Riverside.Elapsed.App/Models/User/Device.cs similarity index 100% rename from src/app/Riverside.Elapsed.App/Models/User/Device.cs rename to src/platforms/Riverside.Elapsed.App/Models/User/Device.cs diff --git a/src/app/Riverside.Elapsed.App/Models/User/Local/DeviceKey.cs b/src/platforms/Riverside.Elapsed.App/Models/User/Local/DeviceKey.cs similarity index 100% rename from src/app/Riverside.Elapsed.App/Models/User/Local/DeviceKey.cs rename to src/platforms/Riverside.Elapsed.App/Models/User/Local/DeviceKey.cs diff --git a/src/app/Riverside.Elapsed.App/Models/User/Local/KeyRelayRequest.cs b/src/platforms/Riverside.Elapsed.App/Models/User/Local/KeyRelayRequest.cs similarity index 100% rename from src/app/Riverside.Elapsed.App/Models/User/Local/KeyRelayRequest.cs rename to src/platforms/Riverside.Elapsed.App/Models/User/Local/KeyRelayRequest.cs diff --git a/src/app/Riverside.Elapsed.App/Models/User/Local/KeyRelayResult.cs b/src/platforms/Riverside.Elapsed.App/Models/User/Local/KeyRelayResult.cs similarity index 100% rename from src/app/Riverside.Elapsed.App/Models/User/Local/KeyRelayResult.cs rename to src/platforms/Riverside.Elapsed.App/Models/User/Local/KeyRelayResult.cs diff --git a/src/app/Riverside.Elapsed.App/Models/User/Myself.cs b/src/platforms/Riverside.Elapsed.App/Models/User/Myself.cs similarity index 100% rename from src/app/Riverside.Elapsed.App/Models/User/Myself.cs rename to src/platforms/Riverside.Elapsed.App/Models/User/Myself.cs diff --git a/src/app/Riverside.Elapsed.App/Models/User/PermissionLevel.cs b/src/platforms/Riverside.Elapsed.App/Models/User/PermissionLevel.cs similarity index 100% rename from src/app/Riverside.Elapsed.App/Models/User/PermissionLevel.cs rename to src/platforms/Riverside.Elapsed.App/Models/User/PermissionLevel.cs diff --git a/src/app/Riverside.Elapsed.App/Models/User/User.cs b/src/platforms/Riverside.Elapsed.App/Models/User/User.cs similarity index 100% rename from src/app/Riverside.Elapsed.App/Models/User/User.cs rename to src/platforms/Riverside.Elapsed.App/Models/User/User.cs diff --git a/src/app/Riverside.Elapsed.App/Services/Drafts/DraftsServiceCollectionExtensions.cs b/src/platforms/Riverside.Elapsed.App/Services/Drafts/DraftsServiceCollectionExtensions.cs similarity index 100% rename from src/app/Riverside.Elapsed.App/Services/Drafts/DraftsServiceCollectionExtensions.cs rename to src/platforms/Riverside.Elapsed.App/Services/Drafts/DraftsServiceCollectionExtensions.cs diff --git a/src/app/Riverside.Elapsed.App/Services/Drafts/ILocalDraftRepository.cs b/src/platforms/Riverside.Elapsed.App/Services/Drafts/ILocalDraftRepository.cs similarity index 100% rename from src/app/Riverside.Elapsed.App/Services/Drafts/ILocalDraftRepository.cs rename to src/platforms/Riverside.Elapsed.App/Services/Drafts/ILocalDraftRepository.cs diff --git a/src/app/Riverside.Elapsed.App/Services/Drafts/LocalDraftRepository.cs b/src/platforms/Riverside.Elapsed.App/Services/Drafts/LocalDraftRepository.cs similarity index 100% rename from src/app/Riverside.Elapsed.App/Services/Drafts/LocalDraftRepository.cs rename to src/platforms/Riverside.Elapsed.App/Services/Drafts/LocalDraftRepository.cs diff --git a/src/app/Riverside.Elapsed.App/Services/Storage/ILocalJsonStore.cs b/src/platforms/Riverside.Elapsed.App/Services/Storage/ILocalJsonStore.cs similarity index 100% rename from src/app/Riverside.Elapsed.App/Services/Storage/ILocalJsonStore.cs rename to src/platforms/Riverside.Elapsed.App/Services/Storage/ILocalJsonStore.cs diff --git a/src/app/Riverside.Elapsed.App/Services/Storage/LocalJsonStore.cs b/src/platforms/Riverside.Elapsed.App/Services/Storage/LocalJsonStore.cs similarity index 100% rename from src/app/Riverside.Elapsed.App/Services/Storage/LocalJsonStore.cs rename to src/platforms/Riverside.Elapsed.App/Services/Storage/LocalJsonStore.cs diff --git a/src/app/Riverside.Elapsed.App/ViewModels/Timelapses/Drafts/DraftDetailsViewModel.cs b/src/platforms/Riverside.Elapsed.App/ViewModels/Timelapses/Drafts/DraftDetailsViewModel.cs similarity index 100% rename from src/app/Riverside.Elapsed.App/ViewModels/Timelapses/Drafts/DraftDetailsViewModel.cs rename to src/platforms/Riverside.Elapsed.App/ViewModels/Timelapses/Drafts/DraftDetailsViewModel.cs diff --git a/src/app/Riverside.Elapsed.App/ViewModels/Timelapses/Drafts/DraftListItem.cs b/src/platforms/Riverside.Elapsed.App/ViewModels/Timelapses/Drafts/DraftListItem.cs similarity index 100% rename from src/app/Riverside.Elapsed.App/ViewModels/Timelapses/Drafts/DraftListItem.cs rename to src/platforms/Riverside.Elapsed.App/ViewModels/Timelapses/Drafts/DraftListItem.cs diff --git a/src/app/Riverside.Elapsed.App/ViewModels/Timelapses/Drafts/DraftListItemViewModel.cs b/src/platforms/Riverside.Elapsed.App/ViewModels/Timelapses/Drafts/DraftListItemViewModel.cs similarity index 100% rename from src/app/Riverside.Elapsed.App/ViewModels/Timelapses/Drafts/DraftListItemViewModel.cs rename to src/platforms/Riverside.Elapsed.App/ViewModels/Timelapses/Drafts/DraftListItemViewModel.cs diff --git a/src/app/Riverside.Elapsed.App/ViewModels/Timelapses/Drafts/DraftsViewModel.cs b/src/platforms/Riverside.Elapsed.App/ViewModels/Timelapses/Drafts/DraftsViewModel.cs similarity index 100% rename from src/app/Riverside.Elapsed.App/ViewModels/Timelapses/Drafts/DraftsViewModel.cs rename to src/platforms/Riverside.Elapsed.App/ViewModels/Timelapses/Drafts/DraftsViewModel.cs