Skip to content

Commit

Permalink
Merge branch 'develop' into feat/crdt-part-of-speech-object-in-senses
Browse files Browse the repository at this point in the history
# Conflicts:
#	backend/FwLite/LcmCrdt.Tests/DataModelSnapshotTests.VerifyDbModel.verified.txt
  • Loading branch information
hahn-kev committed Jan 14, 2025
2 parents e515f4a + f31af35 commit f761055
Show file tree
Hide file tree
Showing 53 changed files with 4,255 additions and 2,028 deletions.
55 changes: 51 additions & 4 deletions backend/FwLite/FwDataMiniLcmBridge/Api/FwDataMiniLcmApi.cs
Original file line number Diff line number Diff line change
Expand Up @@ -938,6 +938,30 @@ internal void InsertSense(ILexEntry lexEntry, ILexSense lexSense, BetweenPositio
lexEntry.SensesOS.Add(lexSense);
}

internal void InsertExampleSentence(ILexSense lexSense, ILexExampleSentence lexExample, BetweenPosition? between = null)
{
var previousExampleId = between?.Previous;
var nextExampleId = between?.Next;

var previousExample = previousExampleId.HasValue ? lexSense.ExamplesOS.FirstOrDefault(s => s.Guid == previousExampleId) : null;
if (previousExample is not null)
{
var insertI = lexSense.ExamplesOS.IndexOf(previousExample) + 1;
lexSense.ExamplesOS.Insert(insertI, lexExample);
return;
}

var nextExample = nextExampleId.HasValue ? lexSense.ExamplesOS.FirstOrDefault(s => s.Guid == nextExampleId) : null;
if (nextExample is not null)
{
var insertI = lexSense.ExamplesOS.IndexOf(nextExample);
lexSense.ExamplesOS.Insert(insertI, lexExample);
return;
}

lexSense.ExamplesOS.Add(lexExample);
}

private void ApplySenseToLexSense(Sense sense, ILexSense lexSense)
{
if (lexSense.MorphoSyntaxAnalysisRA.GetPartOfSpeech()?.Guid != sense.PartOfSpeechId)
Expand Down Expand Up @@ -1070,9 +1094,10 @@ public Task DeleteSense(Guid entryId, Guid senseId)
return Task.FromResult(lcmExampleSentence is null ? null : FromLexExampleSentence(senseId, lcmExampleSentence));
}

internal void CreateExampleSentence(ILexSense lexSense, ExampleSentence exampleSentence)
internal void CreateExampleSentence(ILexSense lexSense, ExampleSentence exampleSentence, BetweenPosition? between = null)
{
var lexExampleSentence = LexExampleSentenceFactory.Create(exampleSentence.Id, lexSense);
var lexExampleSentence = LexExampleSentenceFactory.Create(exampleSentence.Id);
InsertExampleSentence(lexSense, lexExampleSentence, between);
UpdateLcmMultiString(lexExampleSentence.Example, exampleSentence.Sentence);
var freeTranslationType = CmPossibilityRepository.GetObject(CmPossibilityTags.kguidTranFreeTranslation);
var translation = CmTranslationFactory.Create(lexExampleSentence, freeTranslationType);
Expand All @@ -1081,7 +1106,7 @@ internal void CreateExampleSentence(ILexSense lexSense, ExampleSentence exampleS
lexExampleSentence.Reference.get_WritingSystem(0));
}

public async Task<ExampleSentence> CreateExampleSentence(Guid entryId, Guid senseId, ExampleSentence exampleSentence)
public async Task<ExampleSentence> CreateExampleSentence(Guid entryId, Guid senseId, ExampleSentence exampleSentence, BetweenPosition? between = null)
{
if (exampleSentence.Id == default) exampleSentence.Id = Guid.NewGuid();
if (!SenseRepository.TryGetObject(senseId, out var lexSense))
Expand All @@ -1090,7 +1115,7 @@ public async Task<ExampleSentence> CreateExampleSentence(Guid entryId, Guid sens
UndoableUnitOfWorkHelper.DoUsingNewOrCurrentUOW("Create Example Sentence",
"Remove example sentence",
Cache.ServiceLocator.ActionHandler,
() => CreateExampleSentence(lexSense, exampleSentence));
() => CreateExampleSentence(lexSense, exampleSentence, between));
return FromLexExampleSentence(senseId, ExampleSentenceRepository.GetObject(exampleSentence.Id));
}

Expand Down Expand Up @@ -1127,6 +1152,28 @@ await Cache.DoUsingNewOrCurrentUOW("Update Example Sentence",
return await GetExampleSentence(entryId, senseId, after.Id) ?? throw new NullReferenceException("unable to find example sentence with id " + after.Id);
}

public Task MoveExampleSentence(Guid entryId, Guid senseId, Guid exampleSentenceId, BetweenPosition between)
{
if (!EntriesRepository.TryGetObject(entryId, out var lexEntry))
throw new InvalidOperationException("Entry not found");
if (!SenseRepository.TryGetObject(senseId, out var lexSense))
throw new InvalidOperationException("Sense not found");
if (!ExampleSentenceRepository.TryGetObject(exampleSentenceId, out var lexExample))
throw new InvalidOperationException("Example sentence not found");

ValidateOwnership(lexExample, entryId, senseId);

UndoableUnitOfWorkHelper.DoUsingNewOrCurrentUOW("Move Example sentence",
"Move Example sentence back",
Cache.ServiceLocator.ActionHandler,
() =>
{
// LibLCM treats an insert as a move if the example sentence is already on the sense
InsertExampleSentence(lexSense, lexExample, between);
});
return Task.CompletedTask;
}

public Task DeleteExampleSentence(Guid entryId, Guid senseId, Guid exampleSentenceId)
{
var lexExampleSentence = ExampleSentenceRepository.GetObject(exampleSentenceId);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -113,15 +113,9 @@ public override required Guid Id
}
}

public override IList<ExampleSentence> ExampleSentences
public override List<ExampleSentence> ExampleSentences
{
get =>
new UpdateListProxy<ExampleSentence>(
sentence => lexboxLcmApi.CreateExampleSentence(sense, sentence),
sentence => lexboxLcmApi.DeleteExampleSentence(sense.Owner.Guid, Id, sentence.Id),
i => new UpdateExampleSentenceProxy(sense.ExamplesOS[i], lexboxLcmApi),
sense.ExamplesOS.Count
);
get => throw new NotImplementedException();
set => throw new NotImplementedException();
}
}
1 change: 1 addition & 0 deletions backend/FwLite/FwLiteMaui/FwLiteMaui.csproj
Original file line number Diff line number Diff line change
Expand Up @@ -79,6 +79,7 @@
<PackageReference Include="Microsoft.Maui.Controls" Version="$(MauiVersion)" />
<PackageReference Include="Microsoft.Maui.Controls.Compatibility" Version="$(MauiVersion)" />
<PackageReference Include="Microsoft.AspNetCore.Components.WebView.Maui" Version="$(MauiVersion)"/>
<PackageReference Include="Microsoft.Toolkit.Uwp.Notifications" Version="7.1.3" />
<PackageReference Include="NReco.Logging.File" Version="1.2.1" />
<PackageReference Include="System.Collections" Version="4.3.0" />
<PackageReference Include="System.IO" Version="4.3.0" />
Expand Down
5 changes: 5 additions & 0 deletions backend/FwLite/FwLiteMaui/FwLiteMauiKernel.cs
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
using System.Diagnostics;
using System.Reflection;
using FwLiteShared;
using FwLiteShared.Auth;
using LcmCrdt;
Expand Down Expand Up @@ -78,6 +80,9 @@ public static void AddFwLiteMauiServices(this IServiceCollection services,
});

var defaultDataPath = IsPortableApp ? Directory.GetCurrentDirectory() : FileSystem.AppDataDirectory;
//when launching from a notification, the current directory may be C:\Windows\System32, so we'll use the path of the executable instead
if (defaultDataPath.StartsWith("C:\\Windows\\System32", StringComparison.OrdinalIgnoreCase))
defaultDataPath = Path.GetDirectoryName(Process.GetCurrentProcess().MainModule?.FileName ?? Assembly.GetExecutingAssembly().Location) ?? ".";
var baseDataPath = Path.GetFullPath(configuration.GetSection("FwLiteMaui").GetValue<string>("BaseDataDir") ??
defaultDataPath);
logging.AddFilter("FwLiteShared.Auth.LoggerAdapter", LogLevel.Warning);
Expand Down
111 changes: 107 additions & 4 deletions backend/FwLite/FwLiteMaui/Platforms/Windows/AppUpdateService.cs
Original file line number Diff line number Diff line change
@@ -1,8 +1,11 @@
using System.Buffers;
using System.Net.Http.Json;
using System.Text.Json;
using Windows.Management.Deployment;
using Windows.Networking.Connectivity;
using LexCore.Entities;
using Microsoft.Extensions.Logging;
using Microsoft.Toolkit.Uwp.Notifications;

namespace FwLiteMaui;

Expand All @@ -16,6 +19,10 @@ public class AppUpdateService(
private const string FwliteUpdateUrlEnvVar = "FWLITE_UPDATE_URL";
private const string ForceUpdateCheckEnvVar = "FWLITE_FORCE_UPDATE_CHECK";
private const string PreventUpdateCheckEnvVar = "FWLITE_PREVENT_UPDATE";
private const string NotificationIdKey = "notificationId";
private const string ActionKey = "action";
private const string ResultRefKey = "resultRef";
private static readonly Dictionary<string, TaskCompletionSource<string?>> NotificationCompletionSources = new();

private static readonly SearchValues<string> ValidPositiveEnvVarValues =
SearchValues.Create(["1", "true", "yes"], StringComparison.OrdinalIgnoreCase);
Expand All @@ -25,6 +32,16 @@ public class AppUpdateService(

public void Initialize(IServiceProvider services)
{
ToastNotificationManagerCompat.OnActivated += toastArgs =>
{
ToastArguments args = ToastArguments.Parse(toastArgs.Argument);
HandleNotificationAction(args.Get(ActionKey), args.Get(NotificationIdKey), args);
};
if (ToastNotificationManagerCompat.WasCurrentProcessToastActivated())
{
//don't check for updates if the user already clicked on a notification
return;
}
_ = Task.Run(TryUpdate);
}

Expand All @@ -39,19 +56,90 @@ private async Task TryUpdate()
if (!ShouldCheckForUpdate()) return;
var response = await ShouldUpdate();
if (!response.Update) return;

if (ShouldPromptBeforeUpdate() && !await RequestPermissionToUpdate(response.Release))
{
return;
}
await ApplyUpdate(response.Release);
}

private async Task ApplyUpdate(FwLiteRelease latestRelease)
private async Task Test()
{
logger.LogInformation("Testing update notifications");
var fwLiteRelease = new FwLiteRelease("1.0.0.0", "https://test.com");
if (!await RequestPermissionToUpdate(fwLiteRelease))
{
logger.LogInformation("User declined update");
return;
}

await ApplyUpdate(fwLiteRelease);
}

private void ShowUpdateAvailableNotification(FwLiteRelease latestRelease)
{
new ToastContentBuilder().AddText("FieldWorks Lite Update Available").AddText($"Version {latestRelease.Version} will be installed after FieldWorks Lite is closed").Show();
}

private async Task<bool> RequestPermissionToUpdate(FwLiteRelease latestRelease)
{
var notificationId = $"update-{Guid.NewGuid()}";
var tcs = new TaskCompletionSource<string?>();
NotificationCompletionSources.Add(notificationId, tcs);
new ToastContentBuilder()
.AddText("FieldWorks Lite Update")
.AddText("A new version of FieldWorks Lite is available")
.AddText($"Version {latestRelease.Version} would you like to download and install this update?")
.AddArgument(NotificationIdKey, notificationId)
.AddButton(new ToastButton()
.SetContent("Download & Install")
.AddArgument(ActionKey, "download")
.AddArgument("release", JsonSerializer.Serialize(latestRelease)))
.AddArgument(ResultRefKey, "release")
.Show(toast =>
{
toast.Tag = "update";
});
var taskResult = await tcs.Task;
return taskResult != null;
}

private void HandleNotificationAction(string action, string notificationId, ToastArguments args)
{
var result = args.Get(args.Get(ResultRefKey));
if (!NotificationCompletionSources.TryGetValue(notificationId, out var tcs))
{
if (action == "download")
{
var release = JsonSerializer.Deserialize<FwLiteRelease>(result);
if (release == null)
{
logger.LogError("Invalid release {Release} for notification {NotificationId}", result, notificationId);
return;
}
_ = Task.Run(() => ApplyUpdate(release, true));
}
else
{
logger.LogError("Unknown action {Action} for notification {NotificationId}", action, notificationId);
}
return;
}

tcs.SetResult(result);
NotificationCompletionSources.Remove(notificationId);
}

private async Task ApplyUpdate(FwLiteRelease latestRelease, bool quitOnUpdate = false)
{
logger.LogInformation("New version available: {Version}, Current version: {CurrentVersion}", latestRelease.Version, AppVersion.Version);
logger.LogInformation("Installing new version: {Version}, Current version: {CurrentVersion}", latestRelease.Version, AppVersion.Version);
var packageManager = new PackageManager();
var asyncOperation = packageManager.AddPackageByUriAsync(new Uri(latestRelease.Url),
new AddPackageOptions()
{
DeferRegistrationWhenPackagesAreInUse = true,
ForceUpdateFromAnyVersion = true
ForceUpdateFromAnyVersion = true,
ForceAppShutdown = quitOnUpdate
});
asyncOperation.Progress = (info, progressInfo) =>
{
Expand All @@ -70,6 +158,7 @@ private async Task ApplyUpdate(FwLiteRelease latestRelease)
}

logger.LogInformation("Update downloaded, will install on next restart");
ShowUpdateAvailableNotification(latestRelease);
}

private async Task<ShouldUpdateResponse> ShouldUpdate()
Expand Down Expand Up @@ -124,4 +213,18 @@ private bool ShouldCheckForUpdate()
preferences.Set(LastUpdateCheck, DateTime.UtcNow);
return true;
}

private bool ShouldPromptBeforeUpdate()
{
return IsOnMeteredConnection();
}

private bool IsOnMeteredConnection()
{
var profile = NetworkInformation.GetInternetConnectionProfile();
if (profile == null) return false;
var cost = profile.GetConnectionCost();
return cost.NetworkCostType != NetworkCostType.Unrestricted;

}
}
25 changes: 23 additions & 2 deletions backend/FwLite/FwLiteMaui/Platforms/Windows/Package.appxmanifest
Original file line number Diff line number Diff line change
Expand Up @@ -2,10 +2,14 @@
<Package
xmlns="http://schemas.microsoft.com/appx/manifest/foundation/windows10"
xmlns:uap="http://schemas.microsoft.com/appx/manifest/uap/windows10"
xmlns:com="http://schemas.microsoft.com/appx/manifest/com/windows10"
xmlns:desktop="http://schemas.microsoft.com/appx/manifest/desktop/windows10"
xmlns:rescap="http://schemas.microsoft.com/appx/manifest/foundation/windows10/restrictedcapabilities"
IgnorableNamespaces="uap rescap">
IgnorableNamespaces="uap rescap com desktop">

<Identity Name="FwLiteMaui"
<!-- Name is the unique identifier for this app, do not change it, otherwise it will get installed as a different app and not replace the existing one-->
<!-- We may want to use a different name to let users have multiple release channels, eg prod and beta-->
<Identity Name="FwLiteDesktop"
Publisher="CN=&quot;Summer Institute of Linguistics, Inc.&quot;, O=&quot;Summer Institute of Linguistics, Inc.&quot;, L=Dallas, S=Texas, C=US"
Version="0.0.0.0" />
<Properties>
Expand Down Expand Up @@ -37,6 +41,23 @@
<uap:DefaultTile Square71x71Logo="$placeholder$.png" Wide310x150Logo="$placeholder$.png" Square310x310Logo="$placeholder$.png" />
<uap:SplashScreen Image="$placeholder$.png" />
</uap:VisualElements>
<Extensions>

<!--Specify which CLSID to activate when toast clicked-->
<desktop:Extension Category="windows.toastNotificationActivation">
<desktop:ToastNotificationActivation ToastActivatorCLSID="49f2053c-31cc-4eb9-8a65-84491e543d56"/>
</desktop:Extension>

<!--Register COM CLSID LocalServer32 registry key-->
<com:Extension Category="windows.comServer">
<com:ComServer>
<com:ExeServer Executable="$targetnametoken$.exe" Arguments="-ToastActivated"
DisplayName="Toast activator">
<com:Class Id="49f2053c-31cc-4eb9-8a65-84491e543d56" DisplayName="Toast activator"/>
</com:ExeServer>
</com:ComServer>
</com:Extension>
</Extensions>
</Application>
</Applications>

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -9,9 +9,9 @@ public static class WindowsKernel
public static void AddFwLiteWindows(this IServiceCollection services, IHostEnvironment environment)
{
if (!RuntimeInformation.IsOSPlatform(OSPlatform.Windows)) return;
services.AddSingleton<IMauiInitializeService, AppUpdateService>();
if (!FwLiteMauiKernel.IsPortableApp)
{
services.AddSingleton<IMauiInitializeService, AppUpdateService>();
services.AddSingleton<IMauiInitializeService, WindowsShortcutService>();
}
services.Configure<FwLiteConfig>(config =>
Expand Down
4 changes: 3 additions & 1 deletion backend/FwLite/FwLiteProjectSync.Tests/EntrySyncTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -43,7 +43,9 @@ public async Task CanSyncRandomEntries()
var actual = await _fixture.CrdtApi.GetEntry(after.Id);
actual.Should().NotBeNull();
actual.Should().BeEquivalentTo(after, options => options
.For(e => e.Senses).Exclude(s => s.Order));
.For(e => e.Senses).Exclude(s => s.Order)
.For(e => e.Senses).For(s => s.ExampleSentences).Exclude(s => s.Order)
);
}

[Fact]
Expand Down
3 changes: 2 additions & 1 deletion backend/FwLite/FwLiteProjectSync.Tests/Sena3SyncTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -53,7 +53,8 @@ private void ShouldAllBeEquivalentTo(Dictionary<Guid, Entry> crdtEntries, Dictio
options => options
.For(e => e.Components).Exclude(c => c.Id)
.For(e => e.ComplexForms).Exclude(c => c.Id)
.For(e => e.Senses).Exclude(s => s.Order),
.For(e => e.Senses).Exclude(s => s.Order)
.For(e => e.Senses).For(s => s.ExampleSentences).Exclude(s => s.Order),
$"CRDT entry {crdtEntry.Id} was synced with FwData");
}
}
Expand Down
Loading

0 comments on commit f761055

Please sign in to comment.