Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
41 commits
Select commit Hold shift + click to select a range
d872aad
move MediaUri into a media folder
hahn-kev Jul 21, 2025
d38032d
add a no-op save file implementation
hahn-kev Jul 21, 2025
1306f96
setup typegen
hahn-kev Jul 21, 2025
f2b5a96
ensure blobs are correctly transformed into JSStreamReferences
hahn-kev Jul 21, 2025
a40a59b
change save to not expect a media uri, and type IJsStreamReference
hahn-kev Jul 21, 2025
19829d6
handle saving files locally when using FwData
hahn-kev Jul 21, 2025
c86d7e2
update the local media path cache with newly created files
hahn-kev Jul 21, 2025
166f96b
store recordings as a file with a generated name
hahn-kev Jul 21, 2025
e1932d7
prevent reporting an infinite duration which can crash the browser du…
hahn-kev Jul 21, 2025
05098cd
generate a filename based on the time and guess the extension by mime…
hahn-kev Jul 21, 2025
1b24ab2
save files locally using harmony resources
hahn-kev Jul 21, 2025
d3078ec
show an error for files which are too big
hahn-kev Jul 21, 2025
1a76966
handle file saving and upload to lexbox
hahn-kev Jul 21, 2025
cb45b25
expose a media files gql endpoint
hahn-kev Jul 21, 2025
49b2cd5
attempt to upload pending media files on sync
hahn-kev Jul 22, 2025
ae32663
update harmony
hahn-kev Jul 22, 2025
bcac0b5
fix ambiguity
hahn-kev Jul 22, 2025
0125051
fix crash searching when an entry has a null lexeme form
hahn-kev Jul 22, 2025
195122c
protect against overwriting a file which already exists, delete a cre…
hahn-kev Jul 23, 2025
61e6ef9
correct error in file size
hahn-kev Jul 23, 2025
731b28b
make style consistent
hahn-kev Jul 23, 2025
b868963
handle file not found better in FwData
hahn-kev Jul 23, 2025
dd90c61
write media tests base
hahn-kev Jul 23, 2025
40352b0
implement media tests for Fwdata and harmony, and handle file path is…
hahn-kev Jul 23, 2025
26d2bc4
properly handle duplicate filenames by returning a message that the f…
hahn-kev Jul 23, 2025
79af587
remove unused parameter
hahn-kev Jul 23, 2025
fb5671f
change MediaAdapter to work off full file paths
hahn-kev Jul 23, 2025
3807cb7
record workaround reason
hahn-kev Jul 23, 2025
6fa062b
use `is true` to avoid issues with different return types in the future
hahn-kev Jul 23, 2025
ccd4d39
fix failing test
hahn-kev Jul 23, 2025
3186449
fix lint issue
hahn-kev Jul 23, 2025
149971f
disable warning about custom element props
hahn-kev Jul 23, 2025
107c849
remove unused var
hahn-kev Jul 23, 2025
463b81d
fix test failure due to getting uri before the file existed, ensure m…
hahn-kev Jul 23, 2025
0e4a5eb
create SafeLength extension method for streams as CanSeek does not in…
hahn-kev Jul 23, 2025
de7fa0d
enable custom elements for storybook
hahn-kev Jul 24, 2025
8cdaff0
setup permission manager for android blazor web client
hahn-kev Jul 29, 2025
99df7d8
add origin of permission code
hahn-kev Jul 29, 2025
ca99d72
setup permission requests on windows maui
hahn-kev Jul 29, 2025
afa919d
Merge branch 'develop' into save-audio
hahn-kev Jul 29, 2025
a82cf76
remove unused variable
hahn-kev Jul 29, 2025
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
16 changes: 9 additions & 7 deletions backend/FwHeadless/LexboxFwDataMediaAdapter.cs
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,9 @@
using LexCore.Exceptions;
using Microsoft.Extensions.Options;
using MiniLcm;
using MiniLcm.Media;
using SIL.LCModel;
using MediaFile = LexCore.Entities.MediaFile;

namespace FwHeadless;

Expand All @@ -13,17 +15,17 @@ public class LexboxFwDataMediaAdapter(IOptions<FwHeadlessConfig> config, MediaFi
{
public MediaUri MediaUriFromPath(string path, LcmCache cache)
{
var fullPath = Path.Join(cache.LangProject.LinkedFilesRootDir, path);
if (!File.Exists(fullPath)) return MediaUri.NotFound;
return MediaUriForMediaFile(mediaFileService.FindMediaFile(config.Value.LexboxProjectId(cache), fullPath));
if (!Path.IsPathRooted(path)) throw new ArgumentException("Path must be absolute, " + path, nameof(path));
if (!File.Exists(path)) return MediaUri.NotFound;
return MediaUriForMediaFile(mediaFileService.FindMediaFile(config.Value.LexboxProjectId(cache), path));
}

public string PathFromMediaUri(MediaUri mediaUri, LcmCache cache)
public string? PathFromMediaUri(MediaUri mediaUri, LcmCache cache)
{
var mediaFile = mediaFileService.FindMediaFile(mediaUri.FileId) ??
throw new NotFoundException($"Unable to find file {mediaUri.FileId}.", nameof(MediaFile));
var mediaFile = mediaFileService.FindMediaFile(mediaUri.FileId);
if (mediaFile is null) return null;
var fullFilePath = Path.Join(cache.ProjectId.ProjectFolder, mediaFile.Filename);
return Path.GetRelativePath(cache.LangProject.LinkedFilesRootDir, fullFilePath);
return fullFilePath;
}

private MediaUri MediaUriForMediaFile(MediaFile mediaFile)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,8 @@ public static IServiceCollection AddTestFwDataBridge(this IServiceCollection ser
{
services.AddFwDataBridge();
services.TryAddSingleton<IConfiguration>(_ => new ConfigurationRoot([]));
//this path is typically not used for projects (they're in memory) but it is used for media
services.Configure<FwDataBridgeConfig>(config => config.ProjectsFolder = Path.GetFullPath(Path.Combine(".", "fw-test-projects")));
if (mockProjectLoader)
{
services.AddSingleton<MockFwProjectLoader>();
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -11,11 +11,14 @@ public class ProjectLoaderFixture : IDisposable
private readonly ServiceProvider _serviceProvider;
private readonly IOptions<FwDataBridgeConfig> _config;
public MockFwProjectLoader MockFwProjectLoader { get; }
public IServiceProvider Services => _serviceProvider;

public ProjectLoaderFixture()
{
//todo make mock of IProjectLoader so we can load from test projects
var provider = new ServiceCollection().AddTestFwDataBridge().BuildServiceProvider();
var provider = new ServiceCollection()
.AddTestFwDataBridge()
.BuildServiceProvider();
_serviceProvider = provider;
_fwDataFactory = provider.GetRequiredService<FwDataFactory>();
MockFwProjectLoader = provider.GetRequiredService<MockFwProjectLoader>();
Expand Down
12 changes: 8 additions & 4 deletions backend/FwLite/FwDataMiniLcmBridge.Tests/MediaFileTests.cs
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
using FwDataMiniLcmBridge.Api;
using FwDataMiniLcmBridge.Media;
using FwDataMiniLcmBridge.Tests.Fixtures;
using Microsoft.Extensions.DependencyInjection;
using MiniLcm.Media;
using MiniLcm.Models;
using SIL.LCModel.Infrastructure;

Expand All @@ -11,9 +13,11 @@ public class MediaFileTests : IAsyncLifetime
{
private readonly FwDataMiniLcmApi _api;
private readonly WritingSystemId _audioWs = "en-Zxxx-x-audio";
private IMediaAdapter _mediaAdapter;

public MediaFileTests(ProjectLoaderFixture fixture)
{
_mediaAdapter = fixture.Services.GetRequiredService<IMediaAdapter>();
_api = fixture.NewProjectApi("media-file-test", "en", "en");
}

Expand Down Expand Up @@ -60,10 +64,10 @@ private async Task<Guid> AddFileDirectly(string fileName, string? contents, bool

private async Task<Guid> StoreFileContentsAsync(string fileName, string? contents)
{
var fwFilePath = Path.Combine(FwDataMiniLcmApi.AudioVisualFolder, fileName);
var filePath = Path.Combine(_api.Cache.LangProject.LinkedFilesRootDir, fwFilePath);
var filePath = Path.Combine(_api.Cache.LangProject.LinkedFilesRootDir, FwDataMiniLcmApi.AudioVisualFolder, fileName);
await File.WriteAllTextAsync(filePath, contents);
return LocalMediaAdapter.NewGuidV5(fwFilePath);
//using media adapter to ensure it's cache is updated with the new file
return _mediaAdapter.MediaUriFromPath(filePath, _api.Cache).FileId;
}

private string GetFwAudioValue(Guid id)
Expand All @@ -78,7 +82,7 @@ private string GetFwAudioValue(Guid id)
public async Task GetEntry_MapsFilePathsFromAudioWs()
{
var fileName = "MapsAFileReferenceIntoAMediaUri.txt";
var fileGuid = LocalMediaAdapter.NewGuidV5(Path.Combine(FwDataMiniLcmApi.AudioVisualFolder, fileName));
var fileGuid = LocalMediaAdapter.NewGuidV5(Path.Combine(_api.Cache.LangProject.LinkedFilesRootDir, FwDataMiniLcmApi.AudioVisualFolder, fileName));
var entryId = await AddFileDirectly(fileName, "test");

var entry = await _api.GetEntry(entryId);
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
using FwDataMiniLcmBridge.Api;
using FwDataMiniLcmBridge.Tests.Fixtures;

namespace FwDataMiniLcmBridge.Tests.MiniLcmTests;

[Collection(ProjectLoaderFixture.Name)]
public class MediaTests : MediaTestsBase
{
private readonly ProjectLoaderFixture _fixture;

public MediaTests(ProjectLoaderFixture fixture)
{
_fixture = fixture;
}

protected override Task<IMiniLcmApi> NewApi()
{
return Task.FromResult<IMiniLcmApi>(_fixture.NewProjectApi("media-test", "en", "en"));
}

public override async Task InitializeAsync()
{
await base.InitializeAsync();
var projectFolder = ((FwDataMiniLcmApi)Api).Cache.LangProject.LinkedFilesRootDir;
Directory.CreateDirectory(projectFolder);
}

public override async Task DisposeAsync()
{
var projectFolder = ((FwDataMiniLcmApi)Api).Cache.ProjectId.ProjectFolder;
if (Directory.Exists(projectFolder)) Directory.Delete(projectFolder, true);
await base.DisposeAsync();
}
}
63 changes: 57 additions & 6 deletions backend/FwLite/FwDataMiniLcmBridge/Api/FwDataMiniLcmApi.cs
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@
using Microsoft.Extensions.Options;
using MiniLcm;
using MiniLcm.Exceptions;
using MiniLcm.Media;
using MiniLcm.Models;
using MiniLcm.SyncHelpers;
using MiniLcm.Validators;
Expand Down Expand Up @@ -148,7 +149,7 @@
}
}

public async Task<WritingSystem> CreateWritingSystem(WritingSystem writingSystem)

Check warning on line 152 in backend/FwLite/FwDataMiniLcmBridge/Api/FwDataMiniLcmApi.cs

View workflow job for this annotation

GitHub Actions / Build FW Lite and run tests

This async method lacks 'await' operators and will run synchronously. Consider using the 'await' operator to await non-blocking API calls, or 'await Task.Run(...)' to do CPU-bound work on a background thread.

Check warning on line 152 in backend/FwLite/FwDataMiniLcmBridge/Api/FwDataMiniLcmApi.cs

View workflow job for this annotation

GitHub Actions / Build API / publish-api

This async method lacks 'await' operators and will run synchronously. Consider using the 'await' operator to await non-blocking API calls, or 'await Task.Run(...)' to do CPU-bound work on a background thread.

Check warning on line 152 in backend/FwLite/FwDataMiniLcmBridge/Api/FwDataMiniLcmApi.cs

View workflow job for this annotation

GitHub Actions / Build FwHeadless / publish-fw-headless

This async method lacks 'await' operators and will run synchronously. Consider using the 'await' operator to await non-blocking API calls, or 'await Task.Run(...)' to do CPU-bound work on a background thread.

Check warning on line 152 in backend/FwLite/FwDataMiniLcmBridge/Api/FwDataMiniLcmApi.cs

View workflow job for this annotation

GitHub Actions / GHA integration tests / dotnet

This async method lacks 'await' operators and will run synchronously. Consider using the 'await' operator to await non-blocking API calls, or 'await Task.Run(...)' to do CPU-bound work on a background thread.

Check warning on line 152 in backend/FwLite/FwDataMiniLcmBridge/Api/FwDataMiniLcmApi.cs

View workflow job for this annotation

GitHub Actions / Publish FW Lite app for Mac

This async method lacks 'await' operators and will run synchronously. Consider using the 'await' operator to await non-blocking API calls, or 'await Task.Run(...)' to do CPU-bound work on a background thread.

Check warning on line 152 in backend/FwLite/FwDataMiniLcmBridge/Api/FwDataMiniLcmApi.cs

View workflow job for this annotation

GitHub Actions / Publish FW Lite app for Mac

This async method lacks 'await' operators and will run synchronously. Consider using the 'await' operator to await non-blocking API calls, or 'await Task.Run(...)' to do CPU-bound work on a background thread.

Check warning on line 152 in backend/FwLite/FwDataMiniLcmBridge/Api/FwDataMiniLcmApi.cs

View workflow job for this annotation

GitHub Actions / Publish FW Lite app for Linux

This async method lacks 'await' operators and will run synchronously. Consider using the 'await' operator to await non-blocking API calls, or 'await Task.Run(...)' to do CPU-bound work on a background thread.

Check warning on line 152 in backend/FwLite/FwDataMiniLcmBridge/Api/FwDataMiniLcmApi.cs

View workflow job for this annotation

GitHub Actions / Publish FW Lite app for Windows

This async method lacks 'await' operators and will run synchronously. Consider using the 'await' operator to await non-blocking API calls, or 'await Task.Run(...)' to do CPU-bound work on a background thread.
{
var type = writingSystem.Type;
var exitingWs = type == WritingSystemType.Analysis ? Cache.ServiceLocator.WritingSystems.AnalysisWritingSystems : Cache.ServiceLocator.WritingSystems.VernacularWritingSystems;
Expand Down Expand Up @@ -235,7 +236,7 @@
? FromLcmPartOfSpeech(partOfSpeech) : null);
}

public async Task<PartOfSpeech> CreatePartOfSpeech(PartOfSpeech partOfSpeech)

Check warning on line 239 in backend/FwLite/FwDataMiniLcmBridge/Api/FwDataMiniLcmApi.cs

View workflow job for this annotation

GitHub Actions / Build FW Lite and run tests

This async method lacks 'await' operators and will run synchronously. Consider using the 'await' operator to await non-blocking API calls, or 'await Task.Run(...)' to do CPU-bound work on a background thread.

Check warning on line 239 in backend/FwLite/FwDataMiniLcmBridge/Api/FwDataMiniLcmApi.cs

View workflow job for this annotation

GitHub Actions / Build API / publish-api

This async method lacks 'await' operators and will run synchronously. Consider using the 'await' operator to await non-blocking API calls, or 'await Task.Run(...)' to do CPU-bound work on a background thread.

Check warning on line 239 in backend/FwLite/FwDataMiniLcmBridge/Api/FwDataMiniLcmApi.cs

View workflow job for this annotation

GitHub Actions / Build FwHeadless / publish-fw-headless

This async method lacks 'await' operators and will run synchronously. Consider using the 'await' operator to await non-blocking API calls, or 'await Task.Run(...)' to do CPU-bound work on a background thread.

Check warning on line 239 in backend/FwLite/FwDataMiniLcmBridge/Api/FwDataMiniLcmApi.cs

View workflow job for this annotation

GitHub Actions / GHA integration tests / dotnet

This async method lacks 'await' operators and will run synchronously. Consider using the 'await' operator to await non-blocking API calls, or 'await Task.Run(...)' to do CPU-bound work on a background thread.

Check warning on line 239 in backend/FwLite/FwDataMiniLcmBridge/Api/FwDataMiniLcmApi.cs

View workflow job for this annotation

GitHub Actions / Publish FW Lite app for Mac

This async method lacks 'await' operators and will run synchronously. Consider using the 'await' operator to await non-blocking API calls, or 'await Task.Run(...)' to do CPU-bound work on a background thread.

Check warning on line 239 in backend/FwLite/FwDataMiniLcmBridge/Api/FwDataMiniLcmApi.cs

View workflow job for this annotation

GitHub Actions / Publish FW Lite app for Mac

This async method lacks 'await' operators and will run synchronously. Consider using the 'await' operator to await non-blocking API calls, or 'await Task.Run(...)' to do CPU-bound work on a background thread.

Check warning on line 239 in backend/FwLite/FwDataMiniLcmBridge/Api/FwDataMiniLcmApi.cs

View workflow job for this annotation

GitHub Actions / Publish FW Lite app for Linux

This async method lacks 'await' operators and will run synchronously. Consider using the 'await' operator to await non-blocking API calls, or 'await Task.Run(...)' to do CPU-bound work on a background thread.

Check warning on line 239 in backend/FwLite/FwDataMiniLcmBridge/Api/FwDataMiniLcmApi.cs

View workflow job for this annotation

GitHub Actions / Publish FW Lite app for Windows

This async method lacks 'await' operators and will run synchronously. Consider using the 'await' operator to await non-blocking API calls, or 'await Task.Run(...)' to do CPU-bound work on a background thread.
{
IPartOfSpeech? lcmPartOfSpeech = null;
if (partOfSpeech.Id == default) partOfSpeech.Id = Guid.NewGuid();
Expand Down Expand Up @@ -437,7 +438,7 @@
return new ComplexFormType() { Id = t.Guid, Name = FromLcmMultiString(t.Name) };
}

public async Task<ComplexFormType> CreateComplexFormType(ComplexFormType complexFormType)

Check warning on line 441 in backend/FwLite/FwDataMiniLcmBridge/Api/FwDataMiniLcmApi.cs

View workflow job for this annotation

GitHub Actions / Build FW Lite and run tests

This async method lacks 'await' operators and will run synchronously. Consider using the 'await' operator to await non-blocking API calls, or 'await Task.Run(...)' to do CPU-bound work on a background thread.

Check warning on line 441 in backend/FwLite/FwDataMiniLcmBridge/Api/FwDataMiniLcmApi.cs

View workflow job for this annotation

GitHub Actions / Build API / publish-api

This async method lacks 'await' operators and will run synchronously. Consider using the 'await' operator to await non-blocking API calls, or 'await Task.Run(...)' to do CPU-bound work on a background thread.

Check warning on line 441 in backend/FwLite/FwDataMiniLcmBridge/Api/FwDataMiniLcmApi.cs

View workflow job for this annotation

GitHub Actions / Build FwHeadless / publish-fw-headless

This async method lacks 'await' operators and will run synchronously. Consider using the 'await' operator to await non-blocking API calls, or 'await Task.Run(...)' to do CPU-bound work on a background thread.

Check warning on line 441 in backend/FwLite/FwDataMiniLcmBridge/Api/FwDataMiniLcmApi.cs

View workflow job for this annotation

GitHub Actions / GHA integration tests / dotnet

This async method lacks 'await' operators and will run synchronously. Consider using the 'await' operator to await non-blocking API calls, or 'await Task.Run(...)' to do CPU-bound work on a background thread.

Check warning on line 441 in backend/FwLite/FwDataMiniLcmBridge/Api/FwDataMiniLcmApi.cs

View workflow job for this annotation

GitHub Actions / Publish FW Lite app for Mac

This async method lacks 'await' operators and will run synchronously. Consider using the 'await' operator to await non-blocking API calls, or 'await Task.Run(...)' to do CPU-bound work on a background thread.

Check warning on line 441 in backend/FwLite/FwDataMiniLcmBridge/Api/FwDataMiniLcmApi.cs

View workflow job for this annotation

GitHub Actions / Publish FW Lite app for Mac

This async method lacks 'await' operators and will run synchronously. Consider using the 'await' operator to await non-blocking API calls, or 'await Task.Run(...)' to do CPU-bound work on a background thread.

Check warning on line 441 in backend/FwLite/FwDataMiniLcmBridge/Api/FwDataMiniLcmApi.cs

View workflow job for this annotation

GitHub Actions / Publish FW Lite app for Linux

This async method lacks 'await' operators and will run synchronously. Consider using the 'await' operator to await non-blocking API calls, or 'await Task.Run(...)' to do CPU-bound work on a background thread.

Check warning on line 441 in backend/FwLite/FwDataMiniLcmBridge/Api/FwDataMiniLcmApi.cs

View workflow job for this annotation

GitHub Actions / Publish FW Lite app for Windows

This async method lacks 'await' operators and will run synchronously. Consider using the 'await' operator to await non-blocking API calls, or 'await Task.Run(...)' to do CPU-bound work on a background thread.
{
if (complexFormType.Id == default) complexFormType.Id = Guid.NewGuid();
UndoableUnitOfWorkHelper.DoUsingNewOrCurrentUOW("Create complex form type",
Expand Down Expand Up @@ -718,14 +719,17 @@
//rooted media paths aren't supported
if (Path.IsPathRooted(tsString))
throw new ArgumentException("Media path must be relative", nameof(tsString));
return mediaAdapter.MediaUriFromPath(Path.Combine(AudioVisualFolder, tsString), Cache).ToString();
var fullFilePath = Path.Join(Cache.LangProject.LinkedFilesRootDir, AudioVisualFolder, tsString);
return mediaAdapter.MediaUriFromPath(fullFilePath, Cache).ToString();
}

internal string FromMediaUri(string mediaUri)
internal string FromMediaUri(string mediaUriString)
{
//path includes `AudioVisual` currently
var path = mediaAdapter.PathFromMediaUri(new MediaUri(mediaUri), Cache);
return Path.GetRelativePath(AudioVisualFolder, path);
MediaUri mediaUri = new MediaUri(mediaUriString);
var path = mediaAdapter.PathFromMediaUri(mediaUri, Cache);
if (path is null) throw new NotFoundException($"Unable to find file {mediaUri.FileId}.", nameof(MediaFile));
return Path.GetRelativePath(Path.Join(Cache.LangProject.LinkedFilesRootDir, AudioVisualFolder), path);
}

internal RichString? ToRichString(ITsString? tsString)
Expand Down Expand Up @@ -817,7 +821,7 @@
{
if (string.IsNullOrEmpty(query)) return null;
return entry => entry.CitationForm.SearchValue(query) ||
entry.LexemeFormOA.Form.SearchValue(query) ||
entry.LexemeFormOA?.Form.SearchValue(query) is true ||
entry.AllSenses.Any(s => s.Gloss.SearchValue(query));
}

Expand Down Expand Up @@ -1295,7 +1299,7 @@
return Task.FromResult(lcmSense is null ? null : FromLexSense(lcmSense));
}

public async Task<Sense> CreateSense(Guid entryId, Sense sense, BetweenPosition? between = null)

Check warning on line 1302 in backend/FwLite/FwDataMiniLcmBridge/Api/FwDataMiniLcmApi.cs

View workflow job for this annotation

GitHub Actions / Build FW Lite and run tests

This async method lacks 'await' operators and will run synchronously. Consider using the 'await' operator to await non-blocking API calls, or 'await Task.Run(...)' to do CPU-bound work on a background thread.

Check warning on line 1302 in backend/FwLite/FwDataMiniLcmBridge/Api/FwDataMiniLcmApi.cs

View workflow job for this annotation

GitHub Actions / Build API / publish-api

This async method lacks 'await' operators and will run synchronously. Consider using the 'await' operator to await non-blocking API calls, or 'await Task.Run(...)' to do CPU-bound work on a background thread.

Check warning on line 1302 in backend/FwLite/FwDataMiniLcmBridge/Api/FwDataMiniLcmApi.cs

View workflow job for this annotation

GitHub Actions / Build FwHeadless / publish-fw-headless

This async method lacks 'await' operators and will run synchronously. Consider using the 'await' operator to await non-blocking API calls, or 'await Task.Run(...)' to do CPU-bound work on a background thread.

Check warning on line 1302 in backend/FwLite/FwDataMiniLcmBridge/Api/FwDataMiniLcmApi.cs

View workflow job for this annotation

GitHub Actions / GHA integration tests / dotnet

This async method lacks 'await' operators and will run synchronously. Consider using the 'await' operator to await non-blocking API calls, or 'await Task.Run(...)' to do CPU-bound work on a background thread.

Check warning on line 1302 in backend/FwLite/FwDataMiniLcmBridge/Api/FwDataMiniLcmApi.cs

View workflow job for this annotation

GitHub Actions / Publish FW Lite app for Mac

This async method lacks 'await' operators and will run synchronously. Consider using the 'await' operator to await non-blocking API calls, or 'await Task.Run(...)' to do CPU-bound work on a background thread.

Check warning on line 1302 in backend/FwLite/FwDataMiniLcmBridge/Api/FwDataMiniLcmApi.cs

View workflow job for this annotation

GitHub Actions / Publish FW Lite app for Mac

This async method lacks 'await' operators and will run synchronously. Consider using the 'await' operator to await non-blocking API calls, or 'await Task.Run(...)' to do CPU-bound work on a background thread.

Check warning on line 1302 in backend/FwLite/FwDataMiniLcmBridge/Api/FwDataMiniLcmApi.cs

View workflow job for this annotation

GitHub Actions / Publish FW Lite app for Linux

This async method lacks 'await' operators and will run synchronously. Consider using the 'await' operator to await non-blocking API calls, or 'await Task.Run(...)' to do CPU-bound work on a background thread.

Check warning on line 1302 in backend/FwLite/FwDataMiniLcmBridge/Api/FwDataMiniLcmApi.cs

View workflow job for this annotation

GitHub Actions / Publish FW Lite app for Windows

This async method lacks 'await' operators and will run synchronously. Consider using the 'await' operator to await non-blocking API calls, or 'await Task.Run(...)' to do CPU-bound work on a background thread.
{
if (sense.Id == default) sense.Id = Guid.NewGuid();
if (!EntriesRepository.TryGetObject(entryId, out var lexEntry))
Expand Down Expand Up @@ -1440,7 +1444,7 @@
return CmTranslationFactory.Create(parent, freeTranslationType);
}

public async Task<ExampleSentence> CreateExampleSentence(Guid entryId, Guid senseId, ExampleSentence exampleSentence, BetweenPosition? between = null)

Check warning on line 1447 in backend/FwLite/FwDataMiniLcmBridge/Api/FwDataMiniLcmApi.cs

View workflow job for this annotation

GitHub Actions / Build FW Lite and run tests

This async method lacks 'await' operators and will run synchronously. Consider using the 'await' operator to await non-blocking API calls, or 'await Task.Run(...)' to do CPU-bound work on a background thread.

Check warning on line 1447 in backend/FwLite/FwDataMiniLcmBridge/Api/FwDataMiniLcmApi.cs

View workflow job for this annotation

GitHub Actions / Build API / publish-api

This async method lacks 'await' operators and will run synchronously. Consider using the 'await' operator to await non-blocking API calls, or 'await Task.Run(...)' to do CPU-bound work on a background thread.

Check warning on line 1447 in backend/FwLite/FwDataMiniLcmBridge/Api/FwDataMiniLcmApi.cs

View workflow job for this annotation

GitHub Actions / Build FwHeadless / publish-fw-headless

This async method lacks 'await' operators and will run synchronously. Consider using the 'await' operator to await non-blocking API calls, or 'await Task.Run(...)' to do CPU-bound work on a background thread.

Check warning on line 1447 in backend/FwLite/FwDataMiniLcmBridge/Api/FwDataMiniLcmApi.cs

View workflow job for this annotation

GitHub Actions / GHA integration tests / dotnet

This async method lacks 'await' operators and will run synchronously. Consider using the 'await' operator to await non-blocking API calls, or 'await Task.Run(...)' to do CPU-bound work on a background thread.

Check warning on line 1447 in backend/FwLite/FwDataMiniLcmBridge/Api/FwDataMiniLcmApi.cs

View workflow job for this annotation

GitHub Actions / Publish FW Lite app for Mac

This async method lacks 'await' operators and will run synchronously. Consider using the 'await' operator to await non-blocking API calls, or 'await Task.Run(...)' to do CPU-bound work on a background thread.

Check warning on line 1447 in backend/FwLite/FwDataMiniLcmBridge/Api/FwDataMiniLcmApi.cs

View workflow job for this annotation

GitHub Actions / Publish FW Lite app for Mac

This async method lacks 'await' operators and will run synchronously. Consider using the 'await' operator to await non-blocking API calls, or 'await Task.Run(...)' to do CPU-bound work on a background thread.

Check warning on line 1447 in backend/FwLite/FwDataMiniLcmBridge/Api/FwDataMiniLcmApi.cs

View workflow job for this annotation

GitHub Actions / Publish FW Lite app for Linux

This async method lacks 'await' operators and will run synchronously. Consider using the 'await' operator to await non-blocking API calls, or 'await Task.Run(...)' to do CPU-bound work on a background thread.

Check warning on line 1447 in backend/FwLite/FwDataMiniLcmBridge/Api/FwDataMiniLcmApi.cs

View workflow job for this annotation

GitHub Actions / Publish FW Lite app for Windows

This async method lacks 'await' operators and will run synchronously. Consider using the 'await' operator to await non-blocking API calls, or 'await Task.Run(...)' to do CPU-bound work on a background thread.
{
if (exampleSentence.Id == default) exampleSentence.Id = Guid.NewGuid();
if (!SenseRepository.TryGetObject(senseId, out var lexSense))
Expand Down Expand Up @@ -1536,8 +1540,55 @@
public Task<ReadFileResponse> GetFileStream(MediaUri mediaUri)
{
if (mediaUri == MediaUri.NotFound) return Task.FromResult(new ReadFileResponse(ReadFileResult.NotFound));
string fullPath = Path.Combine(Cache.LangProject.LinkedFilesRootDir, mediaAdapter.PathFromMediaUri(mediaUri, Cache));
var pathFromMediaUri = mediaAdapter.PathFromMediaUri(mediaUri, Cache);
if (pathFromMediaUri is not {Length: > 0}) return Task.FromResult(new ReadFileResponse(ReadFileResult.NotFound));
string fullPath = Path.Combine(Cache.LangProject.LinkedFilesRootDir, pathFromMediaUri);
if (!File.Exists(fullPath)) return Task.FromResult(new ReadFileResponse(ReadFileResult.NotFound));
return Task.FromResult(new ReadFileResponse(File.OpenRead(fullPath), Path.GetFileName(fullPath)));
}

public async Task<UploadFileResponse> SaveFile(Stream stream, LcmFileMetadata metadata)
{
if (stream.SafeLength() > MediaFile.MaxFileSize) return new UploadFileResponse(UploadFileResult.TooBig);
var fullPath = Path.Combine(Cache.LangProject.LinkedFilesRootDir, TypeToLinkedFolder(metadata.MimeType), Path.GetFileName(metadata.Filename));

if (File.Exists(fullPath))
return new UploadFileResponse(mediaAdapter.MediaUriFromPath(fullPath, Cache), savedToLexbox: false, newResource: false);
var directory = Path.GetDirectoryName(fullPath);
if (directory is not null)
{
try
{
Directory.CreateDirectory(directory);
}
catch (Exception ex)
{
logger.LogError(ex, "Failed to create directory {Directory} for file {Filename}", directory, metadata.Filename);
return new UploadFileResponse($"Failed to create directory: {ex.Message}");
}
}

try
{
await using var fileStream = File.Create(fullPath);
await stream.CopyToAsync(fileStream);
return new UploadFileResponse(mediaAdapter.MediaUriFromPath(fullPath, Cache), savedToLexbox: false, newResource: true);
}
catch (Exception ex)
{
logger.LogError(ex, "Failed to save file {Filename} to {Path}", metadata.Filename, fullPath);
return new UploadFileResponse($"Failed to save file: {ex.Message}");
}
}

private string TypeToLinkedFolder(string mimeType)
{
return mimeType switch
{
{ } s when s.StartsWith("audio/") => AudioVisualFolder,
{ } s when s.StartsWith("video/") => AudioVisualFolder,
{ } s when s.StartsWith("image/") => "Pictures",
_ => "Others"
};
}
}
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
using System.Collections;
using System.Diagnostics.CodeAnalysis;
using MiniLcm;
using MiniLcm.Media;
using MiniLcm.Models;
using SIL.LCModel.Core.KernelInterfaces;
using SIL.LCModel.Core.Text;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,8 @@ public class LexEntryFilterMapProvider : EntryFilterMapProvider<ILexEntry>
public override Expression<Func<ILexEntry, string, object?>> EntrySensesGloss => (entry, ws) => entry.AllSenses.Select(s => s.PickText(s.Gloss, ws));
public override Expression<Func<ILexEntry, string, object?>> EntrySensesDefinition => (entry, ws) => entry.AllSenses.Select(s => s.PickText(s.Definition, ws));
public override Expression<Func<ILexEntry, string, object?>> EntryNote => (entry, ws) => entry.PickText(entry.Comment, ws);
public override Expression<Func<ILexEntry, string, object?>> EntryLexemeForm => (entry, ws) => entry.PickText(entry.LexemeFormOA.Form, ws);
public override Expression<Func<ILexEntry, string, object?>> EntryLexemeForm => (entry, ws) =>
entry.LexemeFormOA == null ? null : entry.PickText(entry.LexemeFormOA.Form, ws);
public override Expression<Func<ILexEntry, string, object?>> EntryCitationForm => (entry, ws) => entry.PickText(entry.CitationForm, ws);
public override Expression<Func<ILexEntry, string, object?>> EntryLiteralMeaning => (entry, ws) => entry.PickText(entry.LiteralMeaning, ws);
public override Expression<Func<ILexEntry, object?>> EntryComplexFormTypes => e => EmptyToNull(e.ComplexFormEntryRefs.SelectMany(r => r.ComplexEntryTypesRS));
Expand Down
7 changes: 4 additions & 3 deletions backend/FwLite/FwDataMiniLcmBridge/Media/IMediaAdapter.cs
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
using MiniLcm;
using MiniLcm.Media;
using SIL.LCModel;

namespace FwDataMiniLcmBridge.Media;
Expand All @@ -8,7 +9,7 @@ public interface IMediaAdapter
/// <summary>
/// get the MediaUri representing a file, can be used later to get the path back
/// </summary>
/// <param name="path">the path relative to LinkedFiles to find the file at</param>
/// <param name="path">the full file path must be inside the project LinkedFiles directory</param>
/// <param name="cache">the current project</param>
/// <returns>a media uri which can later be used to get the path</returns>
MediaUri MediaUriFromPath(string path, LcmCache cache);
Expand All @@ -17,6 +18,6 @@ public interface IMediaAdapter
/// </summary>
/// <param name="mediaUri"></param>
/// <param name="cache"></param>
/// <returns>the path to the file represented by the mediaUri, relative to the LinkedFiles directory in the given project</returns>
string PathFromMediaUri(MediaUri mediaUri, LcmCache cache);
/// <returns>the full path to the file represented by the mediaUri, will return null when it can't find the file</returns>
string? PathFromMediaUri(MediaUri mediaUri, LcmCache cache);
}
30 changes: 25 additions & 5 deletions backend/FwLite/FwDataMiniLcmBridge/Media/LocalMediaAdapter.cs
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@
using Microsoft.Extensions.Caching.Memory;
using MiniLcm;
using MiniLcm.Exceptions;
using MiniLcm.Media;
using SIL.LCModel;
using UUIDNext;

Expand All @@ -21,19 +22,38 @@ private Dictionary<Guid, string> Paths(LcmCache cache)
entry.SlidingExpiration = TimeSpan.FromMinutes(10);
return Directory
.EnumerateFiles(cache.LangProject.LinkedFilesRootDir, "*", SearchOption.AllDirectories)
.Select(file => Path.GetRelativePath(cache.LangProject.LinkedFilesRootDir, file))
.ToDictionary(file => MediaUriFromPath(file, cache).FileId, file => file);
.ToDictionary(file => PathToUri(file).FileId, file => file);
}) ?? throw new Exception("Failed to get paths");
}

//path is expected to be relative to the LinkedFilesRootDir
public MediaUri MediaUriFromPath(string path, LcmCache cache)
{
if (!File.Exists(Path.Combine(cache.LangProject.LinkedFilesRootDir, path))) return MediaUri.NotFound;
EnsureCorrectRootFolder(path, cache);
if (!File.Exists(path)) return MediaUri.NotFound;
var uri = PathToUri(path);
//this may be a new file, so we need to add it to the cache
Paths(cache)[uri.FileId] = path;
return uri;
}

private void EnsureCorrectRootFolder(string path, LcmCache cache)
{
if (Path.IsPathRooted(path))
{
if (path.StartsWith(cache.LangProject.LinkedFilesRootDir)) return;
throw new ArgumentException("Path must be in the LinkedFilesRootDir", nameof(path));
}

throw new ArgumentException("Path must be absolute, " + path, nameof(path));
}

private static MediaUri PathToUri(string path)
{
return new MediaUri(NewGuidV5(path), LocalMediaAuthority);
}

public string PathFromMediaUri(MediaUri mediaUri, LcmCache cache)
public string? PathFromMediaUri(MediaUri mediaUri, LcmCache cache)
{
var paths = Paths(cache);
if (mediaUri.Authority != LocalMediaAuthority) throw new ArgumentException("MediaUri must be local", nameof(mediaUri));
Expand All @@ -42,7 +62,7 @@ public string PathFromMediaUri(MediaUri mediaUri, LcmCache cache)
return path;
}

throw new NotFoundException("Media not found: " + mediaUri.FileId, "MedaiUri");
return null;
}

// produces the same Guid for the same input name
Expand Down
39 changes: 39 additions & 0 deletions backend/FwLite/FwLiteMaui/MainPage.xaml.Android.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
#if ANDROID

using AndroidX.Activity;
using FwLiteMaui.Platforms.Android;
using Microsoft.AspNetCore.Components.WebView;
using Microsoft.Maui.Platform;

namespace FwLiteMaui;

public partial class MainPage
{
// To manage Android permissions, update AndroidManifest.xml to include the permissions and
// features required by your app. You may have to perform additional configuration to enable
// use of those APIs from the WebView, as is done below. A custom WebChromeClient is needed
// to define what happens when the WebView requests a set of permissions. See
// PermissionManagingBlazorWebChromeClient.cs to explore the approach taken in this example.

private partial void BlazorWebViewInitializing(object? sender, BlazorWebViewInitializingEventArgs e)
{
}

private partial void BlazorWebViewInitialized(object? sender, BlazorWebViewInitializedEventArgs e)
{
if (e.WebView.Context?.GetActivity() is not ComponentActivity activity)
{
throw new InvalidOperationException(
$"The permission-managing WebChromeClient requires that the current activity be a '{nameof(ComponentActivity)}'.");
}

e.WebView.Settings.JavaScriptEnabled = true;
e.WebView.Settings.AllowFileAccess = true;
e.WebView.Settings.MediaPlaybackRequiresUserGesture = false;
e.WebView.Settings.SetGeolocationEnabled(true);
e.WebView.Settings.SetGeolocationDatabasePath(e.WebView.Context?.FilesDir?.Path);

Check warning on line 34 in backend/FwLite/FwLiteMaui/MainPage.xaml.Android.cs

View workflow job for this annotation

GitHub Actions / Publish FW Lite app for Android

This call site is reachable on: 'Android' 24.0 and later. 'WebSettings.SetGeolocationDatabasePath(string?)' is obsoleted on: 'Android' 24.0 and later. (https://learn.microsoft.com/dotnet/fundamentals/code-analysis/quality-rules/ca1422)
e.WebView.SetWebChromeClient(new PermissionManagingBlazorWebChromeClient(e.WebView.WebChromeClient!, activity));

Check warning on line 35 in backend/FwLite/FwLiteMaui/MainPage.xaml.Android.cs

View workflow job for this annotation

GitHub Actions / Publish FW Lite app for Android

This call site is reachable on: 'Android' 24.0 and later. 'WebView.WebChromeClient' is only supported on: 'android' 26.0 and later. (https://learn.microsoft.com/dotnet/fundamentals/code-analysis/quality-rules/ca1416)
}
}

#endif
18 changes: 18 additions & 0 deletions backend/FwLite/FwLiteMaui/MainPage.xaml.Windows.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
using Microsoft.AspNetCore.Components.WebView;

#if WINDOWS
namespace FwLiteMaui;

public partial class MainPage
{
private partial void BlazorWebViewInitializing(object? sender, BlazorWebViewInitializingEventArgs e)
{
}

private partial void BlazorWebViewInitialized(object? sender, BlazorWebViewInitializedEventArgs e)
{
e.WebView.CoreWebView2.PermissionRequested += new SilentPermissionRequestHandler().OnPermissionRequested;
}
}

#endif
Loading
Loading