1
0
mirror of https://github.com/ppy/osu.git synced 2025-01-14 02:03:22 +08:00

Merge pull request #21308 from cdwcgt/export

Make model exporting asynchronous
This commit is contained in:
Bartłomiej Dach 2023-05-06 21:04:09 +02:00 committed by GitHub
commit 0fa32fed51
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
15 changed files with 244 additions and 122 deletions

View File

@ -1,19 +1,26 @@
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. 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<TestModel>(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<TestModel>
{
public string Filename { get; }
public IEnumerable<INamedFileUsage> Files { get; } = new List<INamedFileUsage>();
public TestPathInfo(string filename)
{
Filename = filename;
}
public override string ToString() => Filename;
}
private class TestLegacyExporter : LegacyExporter<IHasNamedFiles>
{
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<INamedFileUsage> Files { get; } = new List<INamedFileUsage>();
public TestModel(string filename)
{
Filename = filename;
}
public override string ToString() => Filename;
}
}
}

View File

@ -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<Storage>()).ExportModelTo(s, exportStream);
});
await new LegacySkinExporter(osu.Dependencies.Get<Storage>()).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<Storage>()).ExportModelTo(s, exportStream);
});
await new LegacySkinExporter(osu.Dependencies.Get<Storage>()).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<SkinManager>();
@ -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<Storage>()).ExportModelTo(s, exportStream);
await new LegacySkinExporter(osu.Dependencies.Get<Storage>()).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<SkinManager>();
@ -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<Storage>()).ExportModelTo(s, exportStream);
await new LegacySkinExporter(osu.Dependencies.Get<Storage>()).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

View File

@ -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<byte[]> resources, IResourceStore<byte[]> storage, WorkingBeatmap? defaultBeatmap,
@ -393,6 +400,8 @@ namespace osu.Game.Beatmaps
public Task<Live<BeatmapSetInfo>?> 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);

View File

@ -0,0 +1,69 @@
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. 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
{
/// <summary>
/// Handles the common scenario of exporting a model to a zip-based archive, usually with a custom file extension.
/// </summary>
public abstract class LegacyArchiveExporter<TModel> : LegacyExporter<TModel>
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);
}
}
}
}
}

View File

@ -1,20 +1,18 @@
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. 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<BeatmapSetInfo>
public class LegacyBeatmapExporter : LegacyArchiveExporter<BeatmapSetInfo>
{
protected override string FileExtension => ".osz";
public LegacyBeatmapExporter(Storage storage)
: base(storage)
{
}
protected override string FileExtension => @".osz";
}
}

View File

@ -1,24 +1,25 @@
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. 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
{
/// <summary>
/// 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.
/// </summary>
public abstract class LegacyExporter<TModel>
where TModel : class, IHasNamedFiles
where TModel : RealmObject, IHasNamedFiles, IHasGuidPrimaryKey
{
/// <summary>
/// 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<Notification>? PostNotification { get; set; }
protected LegacyExporter(Storage storage)
{
exportStorage = storage.GetStorageForDirectory(@"exports");
UserFileStorage = storage.GetStorageForDirectory(@"files");
}
/// <summary>
/// Returns the baseline name of the file to which the <paramref name="item"/> will be exported.
/// </summary>
/// <remarks>
/// The name of the file will be run through <see cref="ModelExtensions.GetValidFilename"/> to eliminate characters
/// which are not permitted by various filesystems.
/// </remarks>
/// <param name="item">The item being exported.</param>
protected virtual string GetFilename(TModel item) => item.GetDisplayString();
/// <summary>
/// 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.
/// </summary>
/// <param name="item">The item to export.</param>
public void Export(TModel item)
/// <param name="model">The model to export.</param>
/// <param name="cancellationToken">A cancellation token.</param>
public async Task ExportAsync(Live<TModel> 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<string> existingExports =
exportStorage
.GetFiles(string.Empty, $"{itemFilename}*{FileExtension}")
.Concat(exportStorage.GetDirectories(string.Empty));
IEnumerable<string> 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;
}
/// <summary>
/// Exports an item to the given output stream.
/// Exports a model to a provided stream.
/// </summary>
/// <param name="model">The item to export.</param>
/// <param name="model">The model to export.</param>
/// <param name="outputStream">The output stream to export to.</param>
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()));
/// <param name="notification">An optional notification to be updated with export progress.</param>
/// <param name="cancellationToken">A cancellation token.</param>
public Task ExportToStreamAsync(Live<TModel> model, Stream outputStream, ProgressNotification? notification = null, CancellationToken cancellationToken = default) =>
Task.Run(() => { model.PerformRead(s => ExportToStream(s, outputStream, notification, cancellationToken)); }, cancellationToken);
archive.SaveTo(outputStream);
}
}
/// <summary>
/// Exports a model to a provided stream.
/// </summary>
/// <param name="model">The model to export.</param>
/// <param name="outputStream">The output stream to export to.</param>
/// <param name="notification">An optional notification to be updated with export progress.</param>
/// <param name="cancellationToken">A cancellation token.</param>
public abstract void ExportToStream(TModel model, Stream outputStream, ProgressNotification? notification, CancellationToken cancellationToken = default);
}
}

View File

@ -1,20 +1,18 @@
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. 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<ScoreInfo>
{
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)

View File

@ -1,8 +1,6 @@
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. 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<string>();
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));
}

View File

@ -1,20 +1,18 @@
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. 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<SkinInfo>
public class LegacySkinExporter : LegacyArchiveExporter<SkinInfo>
{
protected override string FileExtension => ".osk";
public LegacySkinExporter(Storage storage)
: base(storage)
{
}
protected override string FileExtension => @".osk";
}
}

View File

@ -1,8 +1,6 @@
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. 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

View File

@ -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<ScoreComponentLabel> 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<ScoreInfo> 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))));
}

View File

@ -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<Skin> 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)
{

View File

@ -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<IEnumerable<Live<ScoreInfo>>> Import(ProgressNotification notification, ImportTask[] tasks, ImportParameters parameters = default) => scoreImporter.Import(notification, tasks);
public Task Export(ScoreInfo score) => scoreExporter.ExportAsync(score.ToLive(Realm));
public Task<Live<ScoreInfo>> ImportAsUpdate(ProgressNotification notification, ImportTask task, ScoreInfo original) => scoreImporter.ImportAsUpdate(notification, task, original);
public Live<ScoreInfo> Import(ScoreInfo item, ArchiveReader archive = null, ImportParameters parameters = default, CancellationToken cancellationToken = default) =>

View File

@ -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);
}
/// <summary>

View File

@ -58,6 +58,8 @@ namespace osu.Game.Skinning
private readonly SkinImporter skinImporter;
private readonly LegacySkinExporter skinExporter;
private readonly IResourceStore<byte[]> 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<Live<SkinInfo>> Import(ImportTask task, ImportParameters parameters = default, CancellationToken cancellationToken = default) =>
skinImporter.Import(task, parameters, cancellationToken);
public Task ExportCurrentSkin() => ExportSkin(CurrentSkinInfo.Value);
public Task ExportSkin(Live<SkinInfo> skin) => skinExporter.ExportAsync(skin);
#endregion
public void Delete([CanBeNull] Expression<Func<SkinInfo, bool>> filter = null, bool silent = false)