diff --git a/osu.Game.Tests/Database/LegacyExporterTest.cs b/osu.Game.Tests/Database/LegacyModelExporterTest.cs similarity index 67% rename from osu.Game.Tests/Database/LegacyExporterTest.cs rename to osu.Game.Tests/Database/LegacyModelExporterTest.cs index d41b3a5017..0c4b0cc9c4 100644 --- a/osu.Game.Tests/Database/LegacyExporterTest.cs +++ b/osu.Game.Tests/Database/LegacyModelExporterTest.cs @@ -1,19 +1,26 @@ // Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. // See the LICENCE file in the repository root for full licence text. +using System; using System.Collections.Generic; +using System.IO; +using System.Threading; +using System.Threading.Tasks; using NUnit.Framework; +using osu.Framework.Extensions; using osu.Framework.Extensions.ObjectExtensions; using osu.Framework.Platform; using osu.Framework.Testing; using osu.Game.Database; +using osu.Game.Overlays.Notifications; +using Realms; namespace osu.Game.Tests.Database { [TestFixture] - public class LegacyExporterTest + public class LegacyModelExporterTest { - private TestLegacyExporter legacyExporter = null!; + private TestLegacyModelExporter legacyExporter = null!; private TemporaryNativeStorage storage = null!; private const string short_filename = "normal file name"; @@ -25,15 +32,15 @@ namespace osu.Game.Tests.Database public void SetUp() { storage = new TemporaryNativeStorage("export-storage"); - legacyExporter = new TestLegacyExporter(storage); + legacyExporter = new TestLegacyModelExporter(storage); } [Test] public void ExportFileWithNormalNameTest() { - var item = new TestPathInfo(short_filename); + var item = new TestModel(short_filename); - Assert.That(item.Filename.Length, Is.LessThan(TestLegacyExporter.MAX_FILENAME_LENGTH)); + Assert.That(item.Filename.Length, Is.LessThan(TestLegacyModelExporter.MAX_FILENAME_LENGTH)); exportItemAndAssert(item, short_filename); } @@ -41,9 +48,9 @@ namespace osu.Game.Tests.Database [Test] public void ExportFileWithNormalNameMultipleTimesTest() { - var item = new TestPathInfo(short_filename); + var item = new TestModel(short_filename); - Assert.That(item.Filename.Length, Is.LessThan(TestLegacyExporter.MAX_FILENAME_LENGTH)); + Assert.That(item.Filename.Length, Is.LessThan(TestLegacyModelExporter.MAX_FILENAME_LENGTH)); //Export multiple times for (int i = 0; i < 100; i++) @@ -56,24 +63,24 @@ namespace osu.Game.Tests.Database [Test] public void ExportFileWithSuperLongNameTest() { - int expectedLength = TestLegacyExporter.MAX_FILENAME_LENGTH - (legacyExporter.GetExtension().Length); + int expectedLength = TestLegacyModelExporter.MAX_FILENAME_LENGTH - (legacyExporter.GetExtension().Length); string expectedName = long_filename.Remove(expectedLength); - var item = new TestPathInfo(long_filename); + var item = new TestModel(long_filename); - Assert.That(item.Filename.Length, Is.GreaterThan(TestLegacyExporter.MAX_FILENAME_LENGTH)); + Assert.That(item.Filename.Length, Is.GreaterThan(TestLegacyModelExporter.MAX_FILENAME_LENGTH)); exportItemAndAssert(item, expectedName); } [Test] public void ExportFileWithSuperLongNameMultipleTimesTest() { - int expectedLength = TestLegacyExporter.MAX_FILENAME_LENGTH - (legacyExporter.GetExtension().Length); + int expectedLength = TestLegacyModelExporter.MAX_FILENAME_LENGTH - (legacyExporter.GetExtension().Length); string expectedName = long_filename.Remove(expectedLength); - var item = new TestPathInfo(long_filename); + var item = new TestModel(long_filename); - Assert.That(item.Filename.Length, Is.GreaterThan(TestLegacyExporter.MAX_FILENAME_LENGTH)); + Assert.That(item.Filename.Length, Is.GreaterThan(TestLegacyModelExporter.MAX_FILENAME_LENGTH)); //Export multiple times for (int i = 0; i < 100; i++) @@ -83,9 +90,12 @@ namespace osu.Game.Tests.Database } } - private void exportItemAndAssert(IHasNamedFiles item, string expectedName) + private void exportItemAndAssert(TestModel item, string expectedName) { - Assert.DoesNotThrow(() => legacyExporter.Export(item)); + Assert.DoesNotThrow(() => + { + Task.Run(() => legacyExporter.ExportAsync(new RealmLiveUnmanaged(item))).WaitSafely(); + }); Assert.That(storage.Exists($"exports/{expectedName}{legacyExporter.GetExtension()}"), Is.True); } @@ -96,30 +106,36 @@ namespace osu.Game.Tests.Database storage.Dispose(); } - private class TestPathInfo : IHasNamedFiles + private class TestLegacyModelExporter : LegacyExporter { - public string Filename { get; } - - public IEnumerable Files { get; } = new List(); - - public TestPathInfo(string filename) - { - Filename = filename; - } - - public override string ToString() => Filename; - } - - private class TestLegacyExporter : LegacyExporter - { - public TestLegacyExporter(Storage storage) + public TestLegacyModelExporter(Storage storage) : base(storage) { } public string GetExtension() => FileExtension; + public override void ExportToStream(TestModel model, Stream outputStream, ProgressNotification? notification, CancellationToken cancellationToken = default) + { + } + protected override string FileExtension => ".test"; } + + private class TestModel : RealmObject, IHasNamedFiles, IHasGuidPrimaryKey + { + public Guid ID => Guid.Empty; + + public string Filename { get; } + + public IEnumerable Files { get; } = new List(); + + public TestModel(string filename) + { + Filename = filename; + } + + public override string ToString() => Filename; + } } } diff --git a/osu.Game.Tests/Skins/IO/ImportSkinTest.cs b/osu.Game.Tests/Skins/IO/ImportSkinTest.cs index 81ebc59729..0c25934d52 100644 --- a/osu.Game.Tests/Skins/IO/ImportSkinTest.cs +++ b/osu.Game.Tests/Skins/IO/ImportSkinTest.cs @@ -10,7 +10,6 @@ using System.Runtime.CompilerServices; using System.Threading.Tasks; using NUnit.Framework; using osu.Framework.Allocation; -using osu.Framework.Extensions; using osu.Framework.Platform; using osu.Game.Database; using osu.Game.Extensions; @@ -120,10 +119,7 @@ namespace osu.Game.Tests.Skins.IO var import1 = await loadSkinIntoOsu(osu, new ImportTask(createOskWithIni("name 1", "author 1"), "custom.osk")); assertCorrectMetadata(import1, "name 1 [custom]", "author 1", osu); - import1.PerformRead(s => - { - new LegacySkinExporter(osu.Dependencies.Get()).ExportModelTo(s, exportStream); - }); + await new LegacySkinExporter(osu.Dependencies.Get()).ExportToStreamAsync(import1, exportStream); string exportFilename = import1.GetDisplayString(); @@ -141,10 +137,7 @@ namespace osu.Game.Tests.Skins.IO var import1 = await loadSkinIntoOsu(osu, new ImportTask(createOskWithIni("name 『1』", "author 1"), "custom.osk")); assertCorrectMetadata(import1, "name 『1』 [custom]", "author 1", osu); - import1.PerformRead(s => - { - new LegacySkinExporter(osu.Dependencies.Get()).ExportModelTo(s, exportStream); - }); + await new LegacySkinExporter(osu.Dependencies.Get()).ExportToStreamAsync(import1, exportStream); string exportFilename = import1.GetDisplayString().GetValidFilename(); @@ -208,7 +201,7 @@ namespace osu.Game.Tests.Skins.IO }); [Test] - public Task TestExportThenImportDefaultSkin() => runSkinTest(osu => + public Task TestExportThenImportDefaultSkin() => runSkinTest(async osu => { var skinManager = osu.Dependencies.Get(); @@ -218,30 +211,28 @@ namespace osu.Game.Tests.Skins.IO Guid originalSkinId = skinManager.CurrentSkinInfo.Value.ID; - skinManager.CurrentSkinInfo.Value.PerformRead(s => + await skinManager.CurrentSkinInfo.Value.PerformRead(async s => { Assert.IsFalse(s.Protected); Assert.AreEqual(typeof(ArgonSkin), s.CreateInstance(skinManager).GetType()); - new LegacySkinExporter(osu.Dependencies.Get()).ExportModelTo(s, exportStream); + await new LegacySkinExporter(osu.Dependencies.Get()).ExportToStreamAsync(skinManager.CurrentSkinInfo.Value, exportStream); Assert.Greater(exportStream.Length, 0); }); - var imported = skinManager.Import(new ImportTask(exportStream, "exported.osk")); + var imported = await skinManager.Import(new ImportTask(exportStream, "exported.osk")); - imported.GetResultSafely().PerformRead(s => + imported.PerformRead(s => { Assert.IsFalse(s.Protected); Assert.AreNotEqual(originalSkinId, s.ID); Assert.AreEqual(typeof(ArgonSkin), s.CreateInstance(skinManager).GetType()); }); - - return Task.CompletedTask; }); [Test] - public Task TestExportThenImportClassicSkin() => runSkinTest(osu => + public Task TestExportThenImportClassicSkin() => runSkinTest(async osu => { var skinManager = osu.Dependencies.Get(); @@ -253,26 +244,24 @@ namespace osu.Game.Tests.Skins.IO Guid originalSkinId = skinManager.CurrentSkinInfo.Value.ID; - skinManager.CurrentSkinInfo.Value.PerformRead(s => + await skinManager.CurrentSkinInfo.Value.PerformRead(async s => { Assert.IsFalse(s.Protected); Assert.AreEqual(typeof(DefaultLegacySkin), s.CreateInstance(skinManager).GetType()); - new LegacySkinExporter(osu.Dependencies.Get()).ExportModelTo(s, exportStream); + await new LegacySkinExporter(osu.Dependencies.Get()).ExportToStreamAsync(skinManager.CurrentSkinInfo.Value, exportStream); Assert.Greater(exportStream.Length, 0); }); - var imported = skinManager.Import(new ImportTask(exportStream, "exported.osk")); + var imported = await skinManager.Import(new ImportTask(exportStream, "exported.osk")); - imported.GetResultSafely().PerformRead(s => + imported.PerformRead(s => { Assert.IsFalse(s.Protected); Assert.AreNotEqual(originalSkinId, s.ID); Assert.AreEqual(typeof(DefaultLegacySkin), s.CreateInstance(skinManager).GetType()); }); - - return Task.CompletedTask; }); #endregion diff --git a/osu.Game/Beatmaps/BeatmapManager.cs b/osu.Game/Beatmaps/BeatmapManager.cs index ae62564b0d..61b234d88a 100644 --- a/osu.Game/Beatmaps/BeatmapManager.cs +++ b/osu.Game/Beatmaps/BeatmapManager.cs @@ -42,6 +42,8 @@ namespace osu.Game.Beatmaps private readonly WorkingBeatmapCache workingBeatmapCache; + private readonly LegacyBeatmapExporter beatmapExporter; + public ProcessBeatmapDelegate? ProcessBeatmap { private get; set; } public override bool PauseImports @@ -76,6 +78,11 @@ namespace osu.Game.Beatmaps beatmapImporter.PostNotification = obj => PostNotification?.Invoke(obj); workingBeatmapCache = CreateWorkingBeatmapCache(audioManager, gameResources, userResources, defaultBeatmap, host); + + beatmapExporter = new LegacyBeatmapExporter(storage) + { + PostNotification = obj => PostNotification?.Invoke(obj) + }; } protected virtual WorkingBeatmapCache CreateWorkingBeatmapCache(AudioManager audioManager, IResourceStore resources, IResourceStore storage, WorkingBeatmap? defaultBeatmap, @@ -393,6 +400,8 @@ namespace osu.Game.Beatmaps public Task?> ImportAsUpdate(ProgressNotification notification, ImportTask importTask, BeatmapSetInfo original) => beatmapImporter.ImportAsUpdate(notification, importTask, original); + public Task Export(BeatmapSetInfo beatmap) => beatmapExporter.ExportAsync(beatmap.ToLive(Realm)); + private void updateHashAndMarkDirty(BeatmapSetInfo setInfo) { setInfo.Hash = beatmapImporter.ComputeHash(setInfo); diff --git a/osu.Game/Database/LegacyArchiveExporter.cs b/osu.Game/Database/LegacyArchiveExporter.cs new file mode 100644 index 0000000000..7689ffc13d --- /dev/null +++ b/osu.Game/Database/LegacyArchiveExporter.cs @@ -0,0 +1,69 @@ +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// See the LICENCE file in the repository root for full licence text. + +using System.IO; +using System.Linq; +using System.Threading; +using osu.Framework.Logging; +using osu.Framework.Platform; +using osu.Game.Extensions; +using osu.Game.Overlays.Notifications; +using Realms; +using SharpCompress.Common; +using SharpCompress.Writers; +using SharpCompress.Writers.Zip; +using Logger = osu.Framework.Logging.Logger; + +namespace osu.Game.Database +{ + /// + /// Handles the common scenario of exporting a model to a zip-based archive, usually with a custom file extension. + /// + public abstract class LegacyArchiveExporter : LegacyExporter + where TModel : RealmObject, IHasNamedFiles, IHasGuidPrimaryKey + { + protected LegacyArchiveExporter(Storage storage) + : base(storage) + { + } + + public override void ExportToStream(TModel model, Stream outputStream, ProgressNotification? notification, CancellationToken cancellationToken = default) + { + using (var writer = new ZipWriter(outputStream, new ZipWriterOptions(CompressionType.Deflate))) + { + int i = 0; + int fileCount = model.Files.Count(); + bool anyFileMissing = false; + + foreach (var file in model.Files) + { + cancellationToken.ThrowIfCancellationRequested(); + + using (var stream = UserFileStorage.GetStream(file.File.GetStoragePath())) + { + if (stream == null) + { + Logger.Log($"File {file.Filename} is missing in local storage and will not be included in the export", LoggingTarget.Database); + anyFileMissing = true; + continue; + } + + writer.Write(file.Filename, stream); + } + + i++; + + if (notification != null) + { + notification.Progress = (float)i / fileCount; + } + } + + if (anyFileMissing) + { + Logger.Log("Some files are missing in local storage and will not be included in the export", LoggingTarget.Database, LogLevel.Error); + } + } + } + } +} diff --git a/osu.Game/Database/LegacyBeatmapExporter.cs b/osu.Game/Database/LegacyBeatmapExporter.cs index d064b9ed58..4ee8c0636e 100644 --- a/osu.Game/Database/LegacyBeatmapExporter.cs +++ b/osu.Game/Database/LegacyBeatmapExporter.cs @@ -1,20 +1,18 @@ // Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. // See the LICENCE file in the repository root for full licence text. -#nullable disable - using osu.Framework.Platform; using osu.Game.Beatmaps; namespace osu.Game.Database { - public class LegacyBeatmapExporter : LegacyExporter + public class LegacyBeatmapExporter : LegacyArchiveExporter { - protected override string FileExtension => ".osz"; - public LegacyBeatmapExporter(Storage storage) : base(storage) { } + + protected override string FileExtension => @".osz"; } } diff --git a/osu.Game/Database/LegacyExporter.cs b/osu.Game/Database/LegacyExporter.cs index 8da285daa3..f9164e34cd 100644 --- a/osu.Game/Database/LegacyExporter.cs +++ b/osu.Game/Database/LegacyExporter.cs @@ -1,24 +1,25 @@ // Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. // See the LICENCE file in the repository root for full licence text. -#nullable disable - using System; using System.Collections.Generic; using System.IO; using System.Linq; +using System.Threading; +using System.Threading.Tasks; using osu.Framework.Platform; using osu.Game.Extensions; +using osu.Game.Overlays.Notifications; using osu.Game.Utils; -using SharpCompress.Archives.Zip; +using Realms; namespace osu.Game.Database { /// - /// A class which handles exporting legacy user data of a single type from osu-stable. + /// Handles exporting models to files for sharing / consumption outside the game. /// public abstract class LegacyExporter - where TModel : class, IHasNamedFiles + where TModel : RealmObject, IHasNamedFiles, IHasGuidPrimaryKey { /// /// Max length of filename (including extension). @@ -39,55 +40,93 @@ namespace osu.Game.Database protected abstract string FileExtension { get; } protected readonly Storage UserFileStorage; - private readonly Storage exportStorage; + public Action? PostNotification { get; set; } + protected LegacyExporter(Storage storage) { exportStorage = storage.GetStorageForDirectory(@"exports"); UserFileStorage = storage.GetStorageForDirectory(@"files"); } + /// + /// Returns the baseline name of the file to which the will be exported. + /// + /// + /// The name of the file will be run through to eliminate characters + /// which are not permitted by various filesystems. + /// + /// The item being exported. protected virtual string GetFilename(TModel item) => item.GetDisplayString(); /// - /// Exports an item to a legacy (.zip based) package. + /// Exports a model to the default export location. + /// This will create a notification tracking the progress of the export, visible to the user. /// - /// The item to export. - public void Export(TModel item) + /// The model to export. + /// A cancellation token. + public async Task ExportAsync(Live model, CancellationToken cancellationToken = default) { - string itemFilename = GetFilename(item).GetValidFilename(); + string itemFilename = model.PerformRead(s => GetFilename(s).GetValidFilename()); if (itemFilename.Length > MAX_FILENAME_LENGTH - FileExtension.Length) itemFilename = itemFilename.Remove(MAX_FILENAME_LENGTH - FileExtension.Length); - IEnumerable existingExports = - exportStorage - .GetFiles(string.Empty, $"{itemFilename}*{FileExtension}") - .Concat(exportStorage.GetDirectories(string.Empty)); + IEnumerable existingExports = exportStorage + .GetFiles(string.Empty, $"{itemFilename}*{FileExtension}") + .Concat(exportStorage.GetDirectories(string.Empty)); string filename = NamingUtils.GetNextBestFilename(existingExports, $"{itemFilename}{FileExtension}"); - using (var stream = exportStorage.CreateFileSafely(filename)) - ExportModelTo(item, stream); + ProgressNotification notification = new ProgressNotification + { + State = ProgressNotificationState.Active, + Text = $"Exporting {itemFilename}...", + }; - exportStorage.PresentFileExternally(filename); + PostNotification?.Invoke(notification); + + using var linkedSource = CancellationTokenSource.CreateLinkedTokenSource(cancellationToken, notification.CancellationToken); + + try + { + using (var stream = exportStorage.CreateFileSafely(filename)) + { + await ExportToStreamAsync(model, stream, notification, linkedSource.Token).ConfigureAwait(false); + } + } + catch + { + notification.State = ProgressNotificationState.Cancelled; + + // cleanup if export is failed or canceled. + exportStorage.Delete(filename); + throw; + } + + notification.CompletionText = $"Exported {itemFilename}! Click to view."; + notification.CompletionClickAction = () => exportStorage.PresentFileExternally(filename); + notification.State = ProgressNotificationState.Completed; } /// - /// Exports an item to the given output stream. + /// Exports a model to a provided stream. /// - /// The item to export. + /// The model to export. /// The output stream to export to. - public virtual void ExportModelTo(TModel model, Stream outputStream) - { - using (var archive = ZipArchive.Create()) - { - foreach (var file in model.Files) - archive.AddEntry(file.Filename, UserFileStorage.GetStream(file.File.GetStoragePath())); + /// An optional notification to be updated with export progress. + /// A cancellation token. + public Task ExportToStreamAsync(Live model, Stream outputStream, ProgressNotification? notification = null, CancellationToken cancellationToken = default) => + Task.Run(() => { model.PerformRead(s => ExportToStream(s, outputStream, notification, cancellationToken)); }, cancellationToken); - archive.SaveTo(outputStream); - } - } + /// + /// Exports a model to a provided stream. + /// + /// The model to export. + /// The output stream to export to. + /// An optional notification to be updated with export progress. + /// A cancellation token. + public abstract void ExportToStream(TModel model, Stream outputStream, ProgressNotification? notification, CancellationToken cancellationToken = default); } } diff --git a/osu.Game/Database/LegacyScoreExporter.cs b/osu.Game/Database/LegacyScoreExporter.cs index 01f9afdc86..690070af85 100644 --- a/osu.Game/Database/LegacyScoreExporter.cs +++ b/osu.Game/Database/LegacyScoreExporter.cs @@ -1,20 +1,18 @@ // Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. // See the LICENCE file in the repository root for full licence text. -#nullable disable - using System.IO; using System.Linq; +using System.Threading; using osu.Framework.Platform; using osu.Game.Extensions; +using osu.Game.Overlays.Notifications; using osu.Game.Scoring; namespace osu.Game.Database { public class LegacyScoreExporter : LegacyExporter { - protected override string FileExtension => ".osr"; - public LegacyScoreExporter(Storage storage) : base(storage) { @@ -28,7 +26,9 @@ namespace osu.Game.Database return filename; } - public override void ExportModelTo(ScoreInfo model, Stream outputStream) + protected override string FileExtension => @".osr"; + + public override void ExportToStream(ScoreInfo model, Stream outputStream, ProgressNotification? notification, CancellationToken cancellationToken = default) { var file = model.Files.SingleOrDefault(); if (file == null) diff --git a/osu.Game/Database/LegacyScoreImporter.cs b/osu.Game/Database/LegacyScoreImporter.cs index f61241141e..b80a35f90a 100644 --- a/osu.Game/Database/LegacyScoreImporter.cs +++ b/osu.Game/Database/LegacyScoreImporter.cs @@ -1,8 +1,6 @@ // Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. // See the LICENCE file in the repository root for full licence text. -#nullable disable - using System; using System.Collections.Generic; using System.IO; @@ -22,7 +20,7 @@ namespace osu.Game.Database return Enumerable.Empty(); return storage.GetFiles(ImportFromStablePath) - .Where(p => Importer.HandledExtensions.Any(ext => Path.GetExtension(p)?.Equals(ext, StringComparison.OrdinalIgnoreCase) ?? false)) + .Where(p => Importer.HandledExtensions.Any(ext => Path.GetExtension(p).Equals(ext, StringComparison.OrdinalIgnoreCase))) .Select(path => storage.GetFullPath(path)); } diff --git a/osu.Game/Database/LegacySkinExporter.cs b/osu.Game/Database/LegacySkinExporter.cs index 1d5364fb8d..14a3907916 100644 --- a/osu.Game/Database/LegacySkinExporter.cs +++ b/osu.Game/Database/LegacySkinExporter.cs @@ -1,20 +1,18 @@ // Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. // See the LICENCE file in the repository root for full licence text. -#nullable disable - using osu.Framework.Platform; using osu.Game.Skinning; namespace osu.Game.Database { - public class LegacySkinExporter : LegacyExporter + public class LegacySkinExporter : LegacyArchiveExporter { - protected override string FileExtension => ".osk"; - public LegacySkinExporter(Storage storage) : base(storage) { } + + protected override string FileExtension => @".osk"; } } diff --git a/osu.Game/Database/LegacySkinImporter.cs b/osu.Game/Database/LegacySkinImporter.cs index 42b2f2e1d8..2f05ccae45 100644 --- a/osu.Game/Database/LegacySkinImporter.cs +++ b/osu.Game/Database/LegacySkinImporter.cs @@ -1,8 +1,6 @@ // Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. // See the LICENCE file in the repository root for full licence text. -#nullable disable - using osu.Game.Skinning; namespace osu.Game.Database diff --git a/osu.Game/Online/Leaderboards/LeaderboardScore.cs b/osu.Game/Online/Leaderboards/LeaderboardScore.cs index e20b28ee0c..e4ea277756 100644 --- a/osu.Game/Online/Leaderboards/LeaderboardScore.cs +++ b/osu.Game/Online/Leaderboards/LeaderboardScore.cs @@ -17,8 +17,6 @@ using osu.Framework.Graphics.Sprites; using osu.Framework.Graphics.UserInterface; using osu.Framework.Input.Events; using osu.Framework.Localisation; -using osu.Framework.Platform; -using osu.Game.Database; using osu.Game.Extensions; using osu.Game.Graphics; using osu.Game.Graphics.Containers; @@ -66,18 +64,18 @@ namespace osu.Game.Online.Leaderboards private List statisticsLabels; - [Resolved(CanBeNull = true)] + [Resolved(canBeNull: true)] private IDialogOverlay dialogOverlay { get; set; } - [Resolved(CanBeNull = true)] + [Resolved(canBeNull: true)] private SongSelect songSelect { get; set; } - [Resolved] - private Storage storage { get; set; } - public ITooltip GetCustomTooltip() => new LeaderboardScoreTooltip(); public virtual ScoreInfo TooltipContent => Score; + [Resolved] + private ScoreManager scoreManager { get; set; } = null!; + public LeaderboardScore(ScoreInfo score, int? rank, bool isOnlineScope = true) { Score = score; @@ -90,7 +88,7 @@ namespace osu.Game.Online.Leaderboards } [BackgroundDependencyLoader] - private void load(IAPIProvider api, OsuColour colour, ScoreManager scoreManager) + private void load(IAPIProvider api, OsuColour colour) { var user = Score.User; @@ -427,7 +425,7 @@ namespace osu.Game.Online.Leaderboards if (Score.Files.Count > 0) { - items.Add(new OsuMenuItem("Export", MenuItemType.Standard, () => new LegacyScoreExporter(storage).Export(Score))); + items.Add(new OsuMenuItem("Export", MenuItemType.Standard, () => scoreManager.Export(Score))); items.Add(new OsuMenuItem(CommonStrings.ButtonsDelete, MenuItemType.Destructive, () => dialogOverlay?.Push(new LocalScoreDeleteDialog(Score)))); } diff --git a/osu.Game/Overlays/Settings/Sections/SkinSection.cs b/osu.Game/Overlays/Settings/Sections/SkinSection.cs index 5cf8157812..5382eac675 100644 --- a/osu.Game/Overlays/Settings/Sections/SkinSection.cs +++ b/osu.Game/Overlays/Settings/Sections/SkinSection.cs @@ -13,7 +13,6 @@ using osu.Framework.Graphics; using osu.Framework.Graphics.Sprites; using osu.Framework.Localisation; using osu.Framework.Logging; -using osu.Framework.Platform; using osu.Game.Database; using osu.Game.Graphics.UserInterface; using osu.Game.Localisation; @@ -139,9 +138,6 @@ namespace osu.Game.Overlays.Settings.Sections [Resolved] private SkinManager skins { get; set; } - [Resolved] - private Storage storage { get; set; } - private Bindable currentSkin; [BackgroundDependencyLoader] @@ -163,7 +159,7 @@ namespace osu.Game.Overlays.Settings.Sections { try { - currentSkin.Value.SkinInfo.PerformRead(s => new LegacySkinExporter(storage).Export(s)); + skins.ExportCurrentSkin(); } catch (Exception e) { diff --git a/osu.Game/Scoring/ScoreManager.cs b/osu.Game/Scoring/ScoreManager.cs index 3217c79768..3e6d09b74a 100644 --- a/osu.Game/Scoring/ScoreManager.cs +++ b/osu.Game/Scoring/ScoreManager.cs @@ -27,6 +27,7 @@ namespace osu.Game.Scoring { private readonly OsuConfigManager configManager; private readonly ScoreImporter scoreImporter; + private readonly LegacyScoreExporter scoreExporter; public override bool PauseImports { @@ -48,6 +49,11 @@ namespace osu.Game.Scoring { PostNotification = obj => PostNotification?.Invoke(obj) }; + + scoreExporter = new LegacyScoreExporter(storage) + { + PostNotification = obj => PostNotification?.Invoke(obj) + }; } public Score GetScore(ScoreInfo score) => scoreImporter.GetScore(score); @@ -187,6 +193,8 @@ namespace osu.Game.Scoring public Task>> Import(ProgressNotification notification, ImportTask[] tasks, ImportParameters parameters = default) => scoreImporter.Import(notification, tasks); + public Task Export(ScoreInfo score) => scoreExporter.ExportAsync(score.ToLive(Realm)); + public Task> ImportAsUpdate(ProgressNotification notification, ImportTask task, ScoreInfo original) => scoreImporter.ImportAsUpdate(notification, task, original); public Live Import(ScoreInfo item, ArchiveReader archive = null, ImportParameters parameters = default, CancellationToken cancellationToken = default) => diff --git a/osu.Game/Screens/Edit/Editor.cs b/osu.Game/Screens/Edit/Editor.cs index b5d304a031..b8fa7f6579 100644 --- a/osu.Game/Screens/Edit/Editor.cs +++ b/osu.Game/Screens/Edit/Editor.cs @@ -20,7 +20,6 @@ using osu.Framework.Input.Bindings; using osu.Framework.Input.Events; using osu.Framework.Localisation; using osu.Framework.Logging; -using osu.Framework.Platform; using osu.Framework.Screens; using osu.Framework.Testing; using osu.Framework.Threading; @@ -29,7 +28,6 @@ using osu.Game.Audio; using osu.Game.Beatmaps; using osu.Game.Beatmaps.ControlPoints; using osu.Game.Configuration; -using osu.Game.Database; using osu.Game.Graphics.Cursor; using osu.Game.Graphics.UserInterface; using osu.Game.Input.Bindings; @@ -88,9 +86,6 @@ namespace osu.Game.Screens.Edit [Resolved] private RulesetStore rulesets { get; set; } - [Resolved] - private Storage storage { get; set; } - [Resolved(canBeNull: true)] private IDialogOverlay dialogOverlay { get; set; } @@ -983,7 +978,7 @@ namespace osu.Game.Screens.Edit private void exportBeatmap() { Save(); - new LegacyBeatmapExporter(storage).Export(Beatmap.Value.BeatmapSetInfo); + beatmapManager.Export(Beatmap.Value.BeatmapSetInfo); } /// diff --git a/osu.Game/Skinning/SkinManager.cs b/osu.Game/Skinning/SkinManager.cs index fca7dc0f5e..45536e04eb 100644 --- a/osu.Game/Skinning/SkinManager.cs +++ b/osu.Game/Skinning/SkinManager.cs @@ -58,6 +58,8 @@ namespace osu.Game.Skinning private readonly SkinImporter skinImporter; + private readonly LegacySkinExporter skinExporter; + private readonly IResourceStore userFiles; private Skin argonSkin { get; } @@ -120,6 +122,11 @@ namespace osu.Game.Skinning SourceChanged?.Invoke(); }; + + skinExporter = new LegacySkinExporter(storage) + { + PostNotification = obj => PostNotification?.Invoke(obj) + }; } public void SelectRandomSkin() @@ -298,6 +305,10 @@ namespace osu.Game.Skinning public Task> Import(ImportTask task, ImportParameters parameters = default, CancellationToken cancellationToken = default) => skinImporter.Import(task, parameters, cancellationToken); + public Task ExportCurrentSkin() => ExportSkin(CurrentSkinInfo.Value); + + public Task ExportSkin(Live skin) => skinExporter.ExportAsync(skin); + #endregion public void Delete([CanBeNull] Expression> filter = null, bool silent = false)