mirror of
https://github.com/ppy/osu.git
synced 2025-01-15 06:42:56 +08:00
Merge branch 'master' into guard-url-protocols
This commit is contained in:
commit
ba54551313
@ -564,7 +564,7 @@ namespace osu.Game.Tests.Database
|
||||
|
||||
var imported = await importer.Import(
|
||||
progressNotification,
|
||||
new ImportTask(zipStream, string.Empty)
|
||||
new[] { new ImportTask(zipStream, string.Empty) }
|
||||
);
|
||||
|
||||
realm.Run(r => r.Refresh());
|
||||
@ -1052,7 +1052,7 @@ namespace osu.Game.Tests.Database
|
||||
{
|
||||
string? temp = path ?? TestResources.GetTestBeatmapForImport(virtualTrack);
|
||||
|
||||
var importedSet = await importer.Import(new ImportTask(temp), batchImport);
|
||||
var importedSet = await importer.Import(new ImportTask(temp), new ImportParameters { Batch = batchImport });
|
||||
|
||||
Assert.NotNull(importedSet);
|
||||
Debug.Assert(importedSet != null);
|
||||
|
@ -226,12 +226,12 @@ namespace osu.Game.Tests.Online
|
||||
this.testBeatmapManager = testBeatmapManager;
|
||||
}
|
||||
|
||||
public override Live<BeatmapSetInfo> ImportModel(BeatmapSetInfo item, ArchiveReader archive = null, bool batchImport = false, CancellationToken cancellationToken = default)
|
||||
public override Live<BeatmapSetInfo> ImportModel(BeatmapSetInfo item, ArchiveReader archive = null, ImportParameters parameters = default, CancellationToken cancellationToken = default)
|
||||
{
|
||||
if (!testBeatmapManager.AllowImport.Wait(TimeSpan.FromSeconds(10), cancellationToken))
|
||||
throw new TimeoutException("Timeout waiting for import to be allowed.");
|
||||
|
||||
return (testBeatmapManager.CurrentImport = base.ImportModel(item, archive, batchImport, cancellationToken));
|
||||
return (testBeatmapManager.CurrentImport = base.ImportModel(item, archive, parameters, cancellationToken));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -360,7 +360,7 @@ namespace osu.Game.Tests.Skins.IO
|
||||
private async Task<Live<SkinInfo>> loadSkinIntoOsu(OsuGameBase osu, ImportTask import, bool batchImport = false)
|
||||
{
|
||||
var skinManager = osu.Dependencies.Get<SkinManager>();
|
||||
return await skinManager.Import(import, batchImport);
|
||||
return await skinManager.Import(import, new ImportParameters { Batch = batchImport });
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -19,6 +19,7 @@ using osu.Game.Configuration;
|
||||
using osu.Game.Graphics.UserInterface;
|
||||
using osu.Game.Online.Leaderboards;
|
||||
using osu.Game.Overlays;
|
||||
using osu.Game.Overlays.BeatmapListing;
|
||||
using osu.Game.Overlays.Mods;
|
||||
using osu.Game.Overlays.Toolbar;
|
||||
using osu.Game.Rulesets.Mods;
|
||||
@ -515,6 +516,28 @@ namespace osu.Game.Tests.Visual.Navigation
|
||||
AddWaitStep("wait two frames", 2);
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void TestFeaturedArtistDisclaimerDialog()
|
||||
{
|
||||
BeatmapListingOverlay getBeatmapListingOverlay() => Game.ChildrenOfType<BeatmapListingOverlay>().FirstOrDefault();
|
||||
|
||||
AddStep("Wait for notifications to load", () => Game.SearchBeatmapSet(string.Empty));
|
||||
AddUntilStep("wait for dialog overlay", () => Game.ChildrenOfType<DialogOverlay>().SingleOrDefault() != null);
|
||||
|
||||
AddUntilStep("Wait for beatmap overlay to load", () => getBeatmapListingOverlay()?.State.Value == Visibility.Visible);
|
||||
AddAssert("featured artist filter is on", () => getBeatmapListingOverlay().ChildrenOfType<BeatmapSearchGeneralFilterRow>().First().Current.Contains(SearchGeneral.FeaturedArtists));
|
||||
AddStep("toggle featured artist filter",
|
||||
() => getBeatmapListingOverlay().ChildrenOfType<FilterTabItem<SearchGeneral>>().First(i => i.Value == SearchGeneral.FeaturedArtists).TriggerClick());
|
||||
|
||||
AddAssert("disclaimer dialog is shown", () => Game.ChildrenOfType<DialogOverlay>().Single().CurrentDialog != null);
|
||||
AddAssert("featured artist filter is still on", () => getBeatmapListingOverlay().ChildrenOfType<BeatmapSearchGeneralFilterRow>().First().Current.Contains(SearchGeneral.FeaturedArtists));
|
||||
|
||||
AddStep("confirm", () => InputManager.Key(Key.Enter));
|
||||
AddAssert("dialog dismissed", () => Game.ChildrenOfType<DialogOverlay>().Single().CurrentDialog == null);
|
||||
|
||||
AddUntilStep("featured artist filter is off", () => !getBeatmapListingOverlay().ChildrenOfType<BeatmapSearchGeneralFilterRow>().First().Current.Contains(SearchGeneral.FeaturedArtists));
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void TestMainOverlaysClosesNotificationOverlay()
|
||||
{
|
||||
|
@ -80,6 +80,15 @@ namespace osu.Game.Tests.Visual.Online
|
||||
AddStep("reset size", () => localConfig.SetValue(OsuSetting.BeatmapListingCardSize, BeatmapCardSize.Normal));
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void TestFeaturedArtistFilter()
|
||||
{
|
||||
AddAssert("is visible", () => overlay.State.Value == Visibility.Visible);
|
||||
AddAssert("featured artist filter is on", () => overlay.ChildrenOfType<BeatmapSearchGeneralFilterRow>().First().Current.Contains(SearchGeneral.FeaturedArtists));
|
||||
AddStep("toggle featured artist filter", () => overlay.ChildrenOfType<FilterTabItem<SearchGeneral>>().First(i => i.Value == SearchGeneral.FeaturedArtists).TriggerClick());
|
||||
AddAssert("featured artist filter is off", () => !overlay.ChildrenOfType<BeatmapSearchGeneralFilterRow>().First().Current.Contains(SearchGeneral.FeaturedArtists));
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void TestHideViaBack()
|
||||
{
|
||||
|
@ -44,7 +44,7 @@ namespace osu.Game.Beatmaps
|
||||
|
||||
public override async Task<Live<BeatmapSetInfo>?> ImportAsUpdate(ProgressNotification notification, ImportTask importTask, BeatmapSetInfo original)
|
||||
{
|
||||
var imported = await Import(notification, importTask).ConfigureAwait(false);
|
||||
var imported = await Import(notification, new[] { importTask }).ConfigureAwait(false);
|
||||
|
||||
if (!imported.Any())
|
||||
return null;
|
||||
@ -203,10 +203,10 @@ namespace osu.Game.Beatmaps
|
||||
}
|
||||
}
|
||||
|
||||
protected override void PostImport(BeatmapSetInfo model, Realm realm, bool batchImport)
|
||||
protected override void PostImport(BeatmapSetInfo model, Realm realm, ImportParameters parameters)
|
||||
{
|
||||
base.PostImport(model, realm, batchImport);
|
||||
ProcessBeatmap?.Invoke((model, batchImport));
|
||||
base.PostImport(model, realm, parameters);
|
||||
ProcessBeatmap?.Invoke((model, parameters.Batch));
|
||||
}
|
||||
|
||||
private void validateOnlineIds(BeatmapSetInfo beatmapSet, Realm realm)
|
||||
|
@ -456,15 +456,15 @@ namespace osu.Game.Beatmaps
|
||||
|
||||
public Task Import(params string[] paths) => beatmapImporter.Import(paths);
|
||||
|
||||
public Task Import(params ImportTask[] tasks) => beatmapImporter.Import(tasks);
|
||||
public Task Import(ImportTask[] tasks, ImportParameters parameters = default) => beatmapImporter.Import(tasks, parameters);
|
||||
|
||||
public Task<IEnumerable<Live<BeatmapSetInfo>>> Import(ProgressNotification notification, params ImportTask[] tasks) => beatmapImporter.Import(notification, tasks);
|
||||
public Task<IEnumerable<Live<BeatmapSetInfo>>> Import(ProgressNotification notification, ImportTask[] tasks, ImportParameters parameters = default) => beatmapImporter.Import(notification, tasks, parameters);
|
||||
|
||||
public Task<Live<BeatmapSetInfo>?> Import(ImportTask task, bool batchImport = false, CancellationToken cancellationToken = default) =>
|
||||
beatmapImporter.Import(task, batchImport, cancellationToken);
|
||||
public Task<Live<BeatmapSetInfo>?> Import(ImportTask task, ImportParameters parameters = default, CancellationToken cancellationToken = default) =>
|
||||
beatmapImporter.Import(task, parameters, cancellationToken);
|
||||
|
||||
public Live<BeatmapSetInfo>? Import(BeatmapSetInfo item, ArchiveReader? archive = null, CancellationToken cancellationToken = default) =>
|
||||
beatmapImporter.ImportModel(item, archive, false, cancellationToken);
|
||||
beatmapImporter.ImportModel(item, archive, default, cancellationToken);
|
||||
|
||||
public IEnumerable<string> HandledExtensions => beatmapImporter.HandledExtensions;
|
||||
|
||||
|
@ -141,6 +141,9 @@ namespace osu.Game.Beatmaps
|
||||
try
|
||||
{
|
||||
string fileStorePath = BeatmapSetInfo.GetPathForFile(BeatmapInfo.Path);
|
||||
|
||||
// TODO: check validity of file
|
||||
|
||||
var stream = GetStream(fileStorePath);
|
||||
|
||||
if (stream == null)
|
||||
|
@ -19,6 +19,7 @@ namespace osu.Game.Configuration
|
||||
SetDefault(Static.LoginOverlayDisplayed, false);
|
||||
SetDefault(Static.MutedAudioNotificationShownOnce, false);
|
||||
SetDefault(Static.LowBatteryNotificationShownOnce, false);
|
||||
SetDefault(Static.FeaturedArtistDisclaimerShownOnce, false);
|
||||
SetDefault(Static.LastHoverSoundPlaybackTime, (double?)null);
|
||||
SetDefault<APISeasonalBackgrounds>(Static.SeasonalBackgrounds, null);
|
||||
}
|
||||
@ -42,6 +43,7 @@ namespace osu.Game.Configuration
|
||||
LoginOverlayDisplayed,
|
||||
MutedAudioNotificationShownOnce,
|
||||
LowBatteryNotificationShownOnce,
|
||||
FeaturedArtistDisclaimerShownOnce,
|
||||
|
||||
/// <summary>
|
||||
/// Info about seasonal backgrounds available fetched from API - see <see cref="APISeasonalBackgrounds"/>.
|
||||
@ -53,6 +55,6 @@ namespace osu.Game.Configuration
|
||||
/// The last playback time in milliseconds of a hover sample (from <see cref="HoverSounds"/>).
|
||||
/// Used to debounce hover sounds game-wide to avoid volume saturation, especially in scrolling views with many UI controls like <see cref="SettingsOverlay"/>.
|
||||
/// </summary>
|
||||
LastHoverSoundPlaybackTime
|
||||
LastHoverSoundPlaybackTime,
|
||||
}
|
||||
}
|
||||
|
@ -31,7 +31,8 @@ namespace osu.Game.Database
|
||||
/// This will post notifications tracking progress.
|
||||
/// </remarks>
|
||||
/// <param name="tasks">The import tasks from which the files should be imported.</param>
|
||||
Task Import(params ImportTask[] tasks);
|
||||
/// <param name="parameters">Parameters to further configure the import process.</param>
|
||||
Task Import(ImportTask[] tasks, ImportParameters parameters = default);
|
||||
|
||||
/// <summary>
|
||||
/// An array of accepted file extensions (in the standard format of ".abc").
|
||||
|
@ -20,8 +20,9 @@ namespace osu.Game.Database
|
||||
/// </summary>
|
||||
/// <param name="notification">The notification to update.</param>
|
||||
/// <param name="tasks">The import tasks.</param>
|
||||
/// <param name="parameters">Parameters to further configure the import process.</param>
|
||||
/// <returns>The imported models.</returns>
|
||||
Task<IEnumerable<Live<TModel>>> Import(ProgressNotification notification, params ImportTask[] tasks);
|
||||
Task<IEnumerable<Live<TModel>>> Import(ProgressNotification notification, ImportTask[] tasks, ImportParameters parameters = default);
|
||||
|
||||
/// <summary>
|
||||
/// Process a single import as an update for an existing model.
|
||||
|
25
osu.Game/Database/ImportParameters.cs
Normal file
25
osu.Game/Database/ImportParameters.cs
Normal file
@ -0,0 +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.
|
||||
|
||||
namespace osu.Game.Database
|
||||
{
|
||||
public struct ImportParameters
|
||||
{
|
||||
/// <summary>
|
||||
/// Whether this import is part of a larger batch.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// May skip intensive pre-import checks in favour of faster processing.
|
||||
///
|
||||
/// More specifically, imports will be skipped before they begin, given an existing model matches on hash and filenames. Should generally only be used for large batch imports, as it may defy user expectations when updating an existing model.
|
||||
///
|
||||
/// Will also change scheduling behaviour to run at a lower priority.
|
||||
/// </remarks>
|
||||
public bool Batch { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Whether this import should use hard links rather than file copy operations if available.
|
||||
/// </summary>
|
||||
public bool PreferHardLinks { get; set; }
|
||||
}
|
||||
}
|
@ -54,6 +54,19 @@ namespace osu.Game.Database
|
||||
|
||||
public void UpdateStorage(string stablePath) => cachedStorage = new StableStorage(stablePath, gameHost as DesktopGameHost);
|
||||
|
||||
public bool CheckHardLinkAvailability()
|
||||
{
|
||||
var stableStorage = GetCurrentStableStorage();
|
||||
|
||||
if (stableStorage == null || gameHost is not DesktopGameHost desktopGameHost)
|
||||
return false;
|
||||
|
||||
string testExistingPath = stableStorage.GetFullPath(string.Empty);
|
||||
string testDestinationPath = desktopGameHost.Storage.GetFullPath(string.Empty);
|
||||
|
||||
return HardLinkHelper.CheckAvailability(testDestinationPath, testExistingPath);
|
||||
}
|
||||
|
||||
public virtual async Task<int> GetImportCount(StableContent content, CancellationToken cancellationToken)
|
||||
{
|
||||
var stableStorage = GetCurrentStableStorage();
|
||||
|
@ -57,7 +57,12 @@ namespace osu.Game.Database
|
||||
return Task.CompletedTask;
|
||||
}
|
||||
|
||||
return Task.Run(async () => await Importer.Import(GetStableImportPaths(storage).ToArray()).ConfigureAwait(false));
|
||||
return Task.Run(async () =>
|
||||
{
|
||||
var tasks = GetStableImportPaths(storage).Select(p => new ImportTask(p)).ToArray();
|
||||
|
||||
await Importer.Import(tasks, new ImportParameters { Batch = true, PreferHardLinks = true }).ConfigureAwait(false);
|
||||
});
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
|
@ -73,7 +73,7 @@ namespace osu.Game.Database
|
||||
if (originalModel != null)
|
||||
importSuccessful = (await importer.ImportAsUpdate(notification, new ImportTask(filename), originalModel).ConfigureAwait(false)) != null;
|
||||
else
|
||||
importSuccessful = (await importer.Import(notification, new ImportTask(filename)).ConfigureAwait(false)).Any();
|
||||
importSuccessful = (await importer.Import(notification, new[] { new ImportTask(filename) }).ConfigureAwait(false)).Any();
|
||||
|
||||
// for now a failed import will be marked as a failed download for simplicity.
|
||||
if (!importSuccessful)
|
||||
|
@ -81,16 +81,16 @@ namespace osu.Game.Database
|
||||
|
||||
public Task Import(params string[] paths) => Import(paths.Select(p => new ImportTask(p)).ToArray());
|
||||
|
||||
public Task Import(params ImportTask[] tasks)
|
||||
public Task Import(ImportTask[] tasks, ImportParameters parameters = default)
|
||||
{
|
||||
var notification = new ProgressNotification { State = ProgressNotificationState.Active };
|
||||
|
||||
PostNotification?.Invoke(notification);
|
||||
|
||||
return Import(notification, tasks);
|
||||
return Import(notification, tasks, parameters);
|
||||
}
|
||||
|
||||
public async Task<IEnumerable<Live<TModel>>> Import(ProgressNotification notification, params ImportTask[] tasks)
|
||||
public async Task<IEnumerable<Live<TModel>>> Import(ProgressNotification notification, ImportTask[] tasks, ImportParameters parameters = default)
|
||||
{
|
||||
if (tasks.Length == 0)
|
||||
{
|
||||
@ -106,7 +106,7 @@ namespace osu.Game.Database
|
||||
|
||||
var imported = new List<Live<TModel>>();
|
||||
|
||||
bool isBatchImport = tasks.Length >= minimum_items_considered_batch_import;
|
||||
parameters.Batch |= tasks.Length >= minimum_items_considered_batch_import;
|
||||
|
||||
await Task.WhenAll(tasks.Select(async task =>
|
||||
{
|
||||
@ -115,7 +115,7 @@ namespace osu.Game.Database
|
||||
|
||||
try
|
||||
{
|
||||
var model = await Import(task, isBatchImport, notification.CancellationToken).ConfigureAwait(false);
|
||||
var model = await Import(task, parameters, notification.CancellationToken).ConfigureAwait(false);
|
||||
|
||||
lock (imported)
|
||||
{
|
||||
@ -176,16 +176,16 @@ namespace osu.Game.Database
|
||||
/// Note that this bypasses the UI flow and should only be used for special cases or testing.
|
||||
/// </summary>
|
||||
/// <param name="task">The <see cref="ImportTask"/> containing data about the <typeparamref name="TModel"/> to import.</param>
|
||||
/// <param name="batchImport">Whether this import is part of a larger batch.</param>
|
||||
/// <param name="parameters">Parameters to further configure the import process.</param>
|
||||
/// <param name="cancellationToken">An optional cancellation token.</param>
|
||||
/// <returns>The imported model, if successful.</returns>
|
||||
public async Task<Live<TModel>?> Import(ImportTask task, bool batchImport = false, CancellationToken cancellationToken = default)
|
||||
public async Task<Live<TModel>?> Import(ImportTask task, ImportParameters parameters = default, CancellationToken cancellationToken = default)
|
||||
{
|
||||
cancellationToken.ThrowIfCancellationRequested();
|
||||
|
||||
Live<TModel>? import;
|
||||
using (ArchiveReader reader = task.GetReader())
|
||||
import = await importFromArchive(reader, batchImport, cancellationToken).ConfigureAwait(false);
|
||||
import = await importFromArchive(reader, parameters, cancellationToken).ConfigureAwait(false);
|
||||
|
||||
// We may or may not want to delete the file depending on where it is stored.
|
||||
// e.g. reconstructing/repairing database with items from default storage.
|
||||
@ -211,9 +211,9 @@ namespace osu.Game.Database
|
||||
/// This method also handled queueing the import task on a relevant import thread pool.
|
||||
/// </remarks>
|
||||
/// <param name="archive">The archive to be imported.</param>
|
||||
/// <param name="batchImport">Whether this import is part of a larger batch.</param>
|
||||
/// <param name="parameters">Parameters to further configure the import process.</param>
|
||||
/// <param name="cancellationToken">An optional cancellation token.</param>
|
||||
private async Task<Live<TModel>?> importFromArchive(ArchiveReader archive, bool batchImport = false, CancellationToken cancellationToken = default)
|
||||
private async Task<Live<TModel>?> importFromArchive(ArchiveReader archive, ImportParameters parameters = default, CancellationToken cancellationToken = default)
|
||||
{
|
||||
cancellationToken.ThrowIfCancellationRequested();
|
||||
|
||||
@ -236,10 +236,10 @@ namespace osu.Game.Database
|
||||
return null;
|
||||
}
|
||||
|
||||
var scheduledImport = Task.Factory.StartNew(() => ImportModel(model, archive, batchImport, cancellationToken),
|
||||
var scheduledImport = Task.Factory.StartNew(() => ImportModel(model, archive, parameters, cancellationToken),
|
||||
cancellationToken,
|
||||
TaskCreationOptions.HideScheduler,
|
||||
batchImport ? import_scheduler_batch : import_scheduler);
|
||||
parameters.Batch ? import_scheduler_batch : import_scheduler);
|
||||
|
||||
return await scheduledImport.ConfigureAwait(false);
|
||||
}
|
||||
@ -249,15 +249,15 @@ namespace osu.Game.Database
|
||||
/// </summary>
|
||||
/// <param name="item">The model to be imported.</param>
|
||||
/// <param name="archive">An optional archive to use for model population.</param>
|
||||
/// <param name="batchImport">If <c>true</c>, imports will be skipped before they begin, given an existing model matches on hash and filenames. Should generally only be used for large batch imports, as it may defy user expectations when updating an existing model.</param>
|
||||
/// <param name="parameters">Parameters to further configure the import process.</param>
|
||||
/// <param name="cancellationToken">An optional cancellation token.</param>
|
||||
public virtual Live<TModel>? ImportModel(TModel item, ArchiveReader? archive = null, bool batchImport = false, CancellationToken cancellationToken = default) => Realm.Run(realm =>
|
||||
public virtual Live<TModel>? ImportModel(TModel item, ArchiveReader? archive = null, ImportParameters parameters = default, CancellationToken cancellationToken = default) => Realm.Run(realm =>
|
||||
{
|
||||
cancellationToken.ThrowIfCancellationRequested();
|
||||
|
||||
TModel? existing;
|
||||
|
||||
if (batchImport && archive != null)
|
||||
if (parameters.Batch && archive != null)
|
||||
{
|
||||
// this is a fast bail condition to improve large import performance.
|
||||
item.Hash = computeHashFast(archive);
|
||||
@ -303,7 +303,7 @@ namespace osu.Game.Database
|
||||
foreach (var filenames in getShortenedFilenames(archive))
|
||||
{
|
||||
using (Stream s = archive.GetStream(filenames.original))
|
||||
files.Add(new RealmNamedFileUsage(Files.Add(s, realm, false), filenames.shortened));
|
||||
files.Add(new RealmNamedFileUsage(Files.Add(s, realm, false, parameters.PreferHardLinks), filenames.shortened));
|
||||
}
|
||||
}
|
||||
|
||||
@ -358,7 +358,7 @@ namespace osu.Game.Database
|
||||
// import to store
|
||||
realm.Add(item);
|
||||
|
||||
PostImport(item, realm, batchImport);
|
||||
PostImport(item, realm, parameters);
|
||||
|
||||
transaction.Commit();
|
||||
}
|
||||
@ -493,8 +493,8 @@ namespace osu.Game.Database
|
||||
/// </summary>
|
||||
/// <param name="model">The model prepared for import.</param>
|
||||
/// <param name="realm">The current realm context.</param>
|
||||
/// <param name="batchImport">Whether the import was part of a batch.</param>
|
||||
protected virtual void PostImport(TModel model, Realm realm, bool batchImport)
|
||||
/// <param name="parameters">Parameters to further configure the import process.</param>
|
||||
protected virtual void PostImport(TModel model, Realm realm, ImportParameters parameters)
|
||||
{
|
||||
}
|
||||
|
||||
|
@ -4,12 +4,14 @@
|
||||
using System;
|
||||
using System.IO;
|
||||
using System.Linq;
|
||||
using osu.Framework;
|
||||
using osu.Framework.Extensions;
|
||||
using osu.Framework.IO.Stores;
|
||||
using osu.Framework.Logging;
|
||||
using osu.Framework.Platform;
|
||||
using osu.Framework.Testing;
|
||||
using osu.Game.Extensions;
|
||||
using osu.Game.IO;
|
||||
using osu.Game.Models;
|
||||
using Realms;
|
||||
|
||||
@ -41,7 +43,8 @@ namespace osu.Game.Database
|
||||
/// <param name="data">The file data stream.</param>
|
||||
/// <param name="realm">The realm instance to add to. Should already be in a transaction.</param>
|
||||
/// <param name="addToRealm">Whether the <see cref="RealmFile"/> should immediately be added to the underlying realm. If <c>false</c> is provided here, the instance must be manually added.</param>
|
||||
public RealmFile Add(Stream data, Realm realm, bool addToRealm = true)
|
||||
/// <param name="preferHardLinks">Whether this import should use hard links rather than file copy operations if available.</param>
|
||||
public RealmFile Add(Stream data, Realm realm, bool addToRealm = true, bool preferHardLinks = false)
|
||||
{
|
||||
string hash = data.ComputeSHA2Hash();
|
||||
|
||||
@ -50,7 +53,7 @@ namespace osu.Game.Database
|
||||
var file = existing ?? new RealmFile { Hash = hash };
|
||||
|
||||
if (!checkFileExistsAndMatchesHash(file))
|
||||
copyToStore(file, data);
|
||||
copyToStore(file, data, preferHardLinks);
|
||||
|
||||
if (addToRealm && !file.IsManaged)
|
||||
realm.Add(file);
|
||||
@ -58,8 +61,15 @@ namespace osu.Game.Database
|
||||
return file;
|
||||
}
|
||||
|
||||
private void copyToStore(RealmFile file, Stream data)
|
||||
private void copyToStore(RealmFile file, Stream data, bool preferHardLinks)
|
||||
{
|
||||
if (RuntimeInfo.OS == RuntimeInfo.Platform.Windows && data is FileStream fs && preferHardLinks)
|
||||
{
|
||||
// attempt to do a fast hard link rather than copy.
|
||||
if (HardLinkHelper.CreateHardLink(Storage.GetFullPath(file.GetStoragePath(), true), fs.Name, IntPtr.Zero))
|
||||
return;
|
||||
}
|
||||
|
||||
data.Seek(0, SeekOrigin.Begin);
|
||||
|
||||
using (var output = Storage.CreateFileSafely(file.GetStoragePath()))
|
||||
|
@ -234,7 +234,7 @@ namespace osu.Game.Graphics.Cursor
|
||||
SampleChannel channel = tapSample.GetChannel();
|
||||
|
||||
// Scale to [-0.75, 0.75] so that the sample isn't fully panned left or right (sounds weird)
|
||||
channel.Balance.Value = ((activeCursor.X / DrawWidth) * 2 - 1) * 0.75;
|
||||
channel.Balance.Value = ((activeCursor.X / DrawWidth) * 2 - 1) * OsuGameBase.SFX_STEREO_STRENGTH;
|
||||
channel.Frequency.Value = baseFrequency - (random_range / 2f) + RNG.NextDouble(random_range);
|
||||
channel.Volume.Value = baseFrequency;
|
||||
|
||||
|
@ -9,6 +9,7 @@ using osu.Framework.Graphics;
|
||||
using osu.Framework.Graphics.Containers;
|
||||
using osu.Framework.Graphics.Cursor;
|
||||
using osu.Framework.Graphics.Shapes;
|
||||
using osu.Framework.Localisation;
|
||||
using osu.Game.Graphics.Sprites;
|
||||
using osuTK;
|
||||
|
||||
@ -69,8 +70,8 @@ namespace osu.Game.Graphics
|
||||
{
|
||||
DateTimeOffset localDate = date.ToLocalTime();
|
||||
|
||||
dateText.Text = $"{localDate:d MMMM yyyy} ";
|
||||
timeText.Text = $"{localDate:HH:mm:ss \"UTC\"z}";
|
||||
dateText.Text = LocalisableString.Interpolate($"{localDate:d MMMM yyyy} ");
|
||||
timeText.Text = LocalisableString.Interpolate($"{localDate:HH:mm:ss \"UTC\"z}");
|
||||
}
|
||||
|
||||
public void Move(Vector2 pos) => Position = pos;
|
||||
|
108
osu.Game/IO/HardLinkHelper.cs
Normal file
108
osu.Game/IO/HardLinkHelper.cs
Normal file
@ -0,0 +1,108 @@
|
||||
// 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.IO;
|
||||
using System.Runtime.InteropServices;
|
||||
using System.Runtime.InteropServices.ComTypes;
|
||||
using Microsoft.Win32.SafeHandles;
|
||||
using osu.Framework;
|
||||
|
||||
namespace osu.Game.IO
|
||||
{
|
||||
internal static class HardLinkHelper
|
||||
{
|
||||
public static bool CheckAvailability(string testDestinationPath, string testSourcePath)
|
||||
{
|
||||
// We can support other operating systems quite easily in the future.
|
||||
// Let's handle the most common one for now, though.
|
||||
if (RuntimeInfo.OS != RuntimeInfo.Platform.Windows)
|
||||
return false;
|
||||
|
||||
const string test_filename = "_hard_link_test";
|
||||
|
||||
testDestinationPath = Path.Combine(testDestinationPath, test_filename);
|
||||
testSourcePath = Path.Combine(testSourcePath, test_filename);
|
||||
|
||||
cleanupFiles();
|
||||
|
||||
try
|
||||
{
|
||||
File.WriteAllText(testSourcePath, string.Empty);
|
||||
|
||||
// Test availability by creating an arbitrary hard link between the source and destination paths.
|
||||
return CreateHardLink(testDestinationPath, testSourcePath, IntPtr.Zero);
|
||||
}
|
||||
catch
|
||||
{
|
||||
return false;
|
||||
}
|
||||
finally
|
||||
{
|
||||
cleanupFiles();
|
||||
}
|
||||
|
||||
void cleanupFiles()
|
||||
{
|
||||
try
|
||||
{
|
||||
File.Delete(testDestinationPath);
|
||||
File.Delete(testSourcePath);
|
||||
}
|
||||
catch
|
||||
{
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// For future use (to detect if a file is a hard link with other references existing on disk).
|
||||
public static int GetFileLinkCount(string filePath)
|
||||
{
|
||||
int result = 0;
|
||||
SafeFileHandle handle = CreateFile(filePath, FileAccess.Read, FileShare.Read, IntPtr.Zero, FileMode.Open, FileAttributes.Archive, IntPtr.Zero);
|
||||
|
||||
ByHandleFileInformation fileInfo;
|
||||
|
||||
if (GetFileInformationByHandle(handle, out fileInfo))
|
||||
result = (int)fileInfo.NumberOfLinks;
|
||||
CloseHandle(handle);
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
[DllImport("Kernel32.dll", SetLastError = true, CharSet = CharSet.Unicode)]
|
||||
public static extern bool CreateHardLink(string lpFileName, string lpExistingFileName, IntPtr lpSecurityAttributes);
|
||||
|
||||
[DllImport("kernel32.dll", SetLastError = true, CharSet = CharSet.Auto)]
|
||||
private static extern SafeFileHandle CreateFile(
|
||||
string lpFileName,
|
||||
[MarshalAs(UnmanagedType.U4)] FileAccess dwDesiredAccess,
|
||||
[MarshalAs(UnmanagedType.U4)] FileShare dwShareMode,
|
||||
IntPtr lpSecurityAttributes,
|
||||
[MarshalAs(UnmanagedType.U4)] FileMode dwCreationDisposition,
|
||||
[MarshalAs(UnmanagedType.U4)] FileAttributes dwFlagsAndAttributes,
|
||||
IntPtr hTemplateFile);
|
||||
|
||||
[DllImport("kernel32.dll", SetLastError = true)]
|
||||
private static extern bool GetFileInformationByHandle(SafeFileHandle handle, out ByHandleFileInformation lpFileInformation);
|
||||
|
||||
[DllImport("kernel32.dll", SetLastError = true)]
|
||||
[return: MarshalAs(UnmanagedType.Bool)]
|
||||
private static extern bool CloseHandle(SafeHandle hObject);
|
||||
|
||||
[StructLayout(LayoutKind.Sequential)]
|
||||
private struct ByHandleFileInformation
|
||||
{
|
||||
public readonly uint FileAttributes;
|
||||
public readonly FILETIME CreationTime;
|
||||
public readonly FILETIME LastAccessTime;
|
||||
public readonly FILETIME LastWriteTime;
|
||||
public readonly uint VolumeSerialNumber;
|
||||
public readonly uint FileSizeHigh;
|
||||
public readonly uint FileSizeLow;
|
||||
public readonly uint NumberOfLinks;
|
||||
public readonly uint FileIndexHigh;
|
||||
public readonly uint FileIndexLow;
|
||||
}
|
||||
}
|
||||
}
|
33
osu.Game/Localisation/BeatmapOverlayStrings.cs
Normal file
33
osu.Game/Localisation/BeatmapOverlayStrings.cs
Normal file
@ -0,0 +1,33 @@
|
||||
// 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 osu.Framework.Localisation;
|
||||
|
||||
namespace osu.Game.Localisation
|
||||
{
|
||||
public static class BeatmapOverlayStrings
|
||||
{
|
||||
private const string prefix = @"osu.Game.Resources.Localisation.BeatmapOverlayStrings";
|
||||
|
||||
/// <summary>
|
||||
/// "User content disclaimer"
|
||||
/// </summary>
|
||||
public static LocalisableString UserContentDisclaimerHeader => new TranslatableString(getKey(@"user_content_disclaimer"), @"User content disclaimer");
|
||||
|
||||
/// <summary>
|
||||
/// "By turning off the "Featured Artist" filter, all user-uploaded content will be displayed.
|
||||
///
|
||||
/// This includes content that may not be correctly licensed for osu! usage. Browse at your own risk."
|
||||
/// </summary>
|
||||
public static LocalisableString UserContentDisclaimerDescription => new TranslatableString(getKey(@"by_turning_off_the_featured"), @"By turning off the ""Featured Artist"" filter, all user-uploaded content will be displayed.
|
||||
|
||||
This includes content that may not be correctly licensed for osu! usage. Browse at your own risk.");
|
||||
|
||||
/// <summary>
|
||||
/// "I understand"
|
||||
/// </summary>
|
||||
public static LocalisableString UserContentConfirmButtonText => new TranslatableString(getKey(@"understood"), @"I understand");
|
||||
|
||||
private static string getKey(string key) => $@"{prefix}:{key}";
|
||||
}
|
||||
}
|
@ -15,10 +15,10 @@ namespace osu.Game.Localisation
|
||||
public static LocalisableString Header => new TranslatableString(getKey(@"header"), @"Import");
|
||||
|
||||
/// <summary>
|
||||
/// "If you have an installation of a previous osu! version, you can choose to migrate your existing content. Note that this will create a copy, and not affect your existing installation."
|
||||
/// "If you have an installation of a previous osu! version, you can choose to migrate your existing content. Note that this will not affect your existing installation's files in any way."
|
||||
/// </summary>
|
||||
public static LocalisableString Description => new TranslatableString(getKey(@"description"),
|
||||
@"If you have an installation of a previous osu! version, you can choose to migrate your existing content. Note that this will create a copy, and not affect your existing installation.");
|
||||
@"If you have an installation of a previous osu! version, you can choose to migrate your existing content. Note that this will not affect your existing installation's files in any way.");
|
||||
|
||||
/// <summary>
|
||||
/// "previous osu! install"
|
||||
|
@ -136,9 +136,8 @@ namespace osu.Game.Online.Leaderboards
|
||||
{
|
||||
if (displayedScore != null)
|
||||
{
|
||||
timestampLabel.Text = prefer24HourTime.Value
|
||||
? $"Played on {displayedScore.Date.ToLocalTime():d MMMM yyyy HH:mm}"
|
||||
: $"Played on {displayedScore.Date.ToLocalTime():d MMMM yyyy h:mm tt}";
|
||||
timestampLabel.Text = LocalisableString.Format("Played on {0}",
|
||||
displayedScore.Date.ToLocalTime().ToLocalisableString(prefer24HourTime.Value ? @"d MMMM yyyy HH:mm" : @"d MMMM yyyy h:mm tt"));
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -627,14 +627,14 @@ namespace osu.Game
|
||||
}, validScreens: validScreens);
|
||||
}
|
||||
|
||||
public override Task Import(params ImportTask[] imports)
|
||||
public override Task Import(ImportTask[] imports, ImportParameters parameters = default)
|
||||
{
|
||||
// encapsulate task as we don't want to begin the import process until in a ready state.
|
||||
|
||||
// ReSharper disable once AsyncVoidLambda
|
||||
// TODO: This is bad because `new Task` doesn't have a Func<Task?> override.
|
||||
// Only used for android imports and a bit of a mess. Probably needs rethinking overall.
|
||||
var importTask = new Task(async () => await base.Import(imports).ConfigureAwait(false));
|
||||
var importTask = new Task(async () => await base.Import(imports, parameters).ConfigureAwait(false));
|
||||
|
||||
waitForReady(() => this, _ => importTask.Start());
|
||||
|
||||
|
@ -83,6 +83,8 @@ namespace osu.Game
|
||||
|
||||
public const int SAMPLE_CONCURRENCY = 6;
|
||||
|
||||
public const double SFX_STEREO_STRENGTH = 0.75;
|
||||
|
||||
/// <summary>
|
||||
/// Length of debounce (in milliseconds) for commonly occuring sample playbacks that could stack.
|
||||
/// </summary>
|
||||
|
@ -44,13 +44,13 @@ namespace osu.Game
|
||||
}
|
||||
}
|
||||
|
||||
public virtual async Task Import(params ImportTask[] tasks)
|
||||
public virtual async Task Import(ImportTask[] tasks, ImportParameters parameters = default)
|
||||
{
|
||||
var tasksPerExtension = tasks.GroupBy(t => Path.GetExtension(t.Path).ToLowerInvariant());
|
||||
await Task.WhenAll(tasksPerExtension.Select(taskGroup =>
|
||||
{
|
||||
var importer = fileImporters.FirstOrDefault(i => i.HandledExtensions.Contains(taskGroup.Key));
|
||||
return importer?.Import(taskGroup.ToArray()) ?? Task.CompletedTask;
|
||||
return importer?.Import(taskGroup.ToArray(), parameters) ?? Task.CompletedTask;
|
||||
})).ConfigureAwait(false);
|
||||
}
|
||||
|
||||
|
@ -146,6 +146,7 @@ namespace osu.Game.Overlays.BeatmapListing
|
||||
}
|
||||
});
|
||||
|
||||
generalFilter.Current.Add(SearchGeneral.FeaturedArtists);
|
||||
categoryFilter.Current.Value = SearchCategory.Leaderboard;
|
||||
}
|
||||
|
||||
|
@ -3,10 +3,18 @@
|
||||
|
||||
#nullable disable
|
||||
|
||||
using System;
|
||||
using osu.Framework.Allocation;
|
||||
using osu.Framework.Bindables;
|
||||
using osu.Framework.Graphics.Sprites;
|
||||
using osu.Framework.Input.Events;
|
||||
using osu.Game.Configuration;
|
||||
using osu.Game.Graphics;
|
||||
using osu.Game.Localisation;
|
||||
using osu.Game.Overlays.Dialog;
|
||||
using osu.Game.Resources.Localisation.Web;
|
||||
using osuTK.Graphics;
|
||||
using CommonStrings = osu.Game.Resources.Localisation.Web.CommonStrings;
|
||||
|
||||
namespace osu.Game.Overlays.BeatmapListing
|
||||
{
|
||||
@ -32,6 +40,8 @@ namespace osu.Game.Overlays.BeatmapListing
|
||||
|
||||
private partial class FeaturedArtistsTabItem : MultipleSelectionFilterTabItem
|
||||
{
|
||||
private Bindable<bool> disclaimerShown;
|
||||
|
||||
public FeaturedArtistsTabItem()
|
||||
: base(SearchGeneral.FeaturedArtists)
|
||||
{
|
||||
@ -40,7 +50,60 @@ namespace osu.Game.Overlays.BeatmapListing
|
||||
[Resolved]
|
||||
private OsuColour colours { get; set; }
|
||||
|
||||
[Resolved]
|
||||
private SessionStatics sessionStatics { get; set; }
|
||||
|
||||
[Resolved(canBeNull: true)]
|
||||
private IDialogOverlay dialogOverlay { get; set; }
|
||||
|
||||
protected override Color4 GetStateColour() => colours.Orange1;
|
||||
|
||||
protected override void LoadComplete()
|
||||
{
|
||||
base.LoadComplete();
|
||||
|
||||
disclaimerShown = sessionStatics.GetBindable<bool>(Static.FeaturedArtistDisclaimerShownOnce);
|
||||
}
|
||||
|
||||
protected override bool OnClick(ClickEvent e)
|
||||
{
|
||||
if (!disclaimerShown.Value && dialogOverlay != null)
|
||||
{
|
||||
dialogOverlay.Push(new FeaturedArtistConfirmDialog(() =>
|
||||
{
|
||||
disclaimerShown.Value = true;
|
||||
base.OnClick(e);
|
||||
}));
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
return base.OnClick(e);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
internal partial class FeaturedArtistConfirmDialog : PopupDialog
|
||||
{
|
||||
public FeaturedArtistConfirmDialog(Action confirm)
|
||||
{
|
||||
HeaderText = BeatmapOverlayStrings.UserContentDisclaimerHeader;
|
||||
BodyText = BeatmapOverlayStrings.UserContentDisclaimerDescription;
|
||||
|
||||
Icon = FontAwesome.Solid.ExclamationTriangle;
|
||||
|
||||
Buttons = new PopupDialogButton[]
|
||||
{
|
||||
new PopupDialogDangerousButton
|
||||
{
|
||||
Text = BeatmapOverlayStrings.UserContentConfirmButtonText,
|
||||
Action = confirm
|
||||
},
|
||||
new PopupDialogCancelButton
|
||||
{
|
||||
Text = CommonStrings.ButtonsCancel,
|
||||
},
|
||||
};
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -5,6 +5,7 @@
|
||||
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Collections.Specialized;
|
||||
using System.Linq;
|
||||
using JetBrains.Annotations;
|
||||
using osu.Framework.Allocation;
|
||||
@ -18,6 +19,7 @@ using osuTK;
|
||||
namespace osu.Game.Overlays.BeatmapListing
|
||||
{
|
||||
public partial class BeatmapSearchMultipleSelectionFilterRow<T> : BeatmapSearchFilterRow<List<T>>
|
||||
where T : Enum
|
||||
{
|
||||
public new readonly BindableList<T> Current = new BindableList<T>();
|
||||
|
||||
@ -31,7 +33,7 @@ namespace osu.Game.Overlays.BeatmapListing
|
||||
[BackgroundDependencyLoader]
|
||||
private void load()
|
||||
{
|
||||
Current.BindTo(filter.Current);
|
||||
filter.Current.BindTo(Current);
|
||||
}
|
||||
|
||||
protected sealed override Drawable CreateFilter() => filter = CreateMultipleSelectionFilter();
|
||||
@ -64,6 +66,14 @@ namespace osu.Game.Overlays.BeatmapListing
|
||||
|
||||
foreach (var item in Children)
|
||||
item.Active.BindValueChanged(active => toggleItem(item.Value, active.NewValue));
|
||||
|
||||
Current.BindCollectionChanged(currentChanged, true);
|
||||
}
|
||||
|
||||
private void currentChanged(object sender, NotifyCollectionChangedEventArgs e)
|
||||
{
|
||||
foreach (var c in Children)
|
||||
c.Active.Value = Current.Contains(c.Value);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
@ -79,7 +89,10 @@ namespace osu.Game.Overlays.BeatmapListing
|
||||
private void toggleItem(T value, bool active)
|
||||
{
|
||||
if (active)
|
||||
Current.Add(value);
|
||||
{
|
||||
if (!Current.Contains(value))
|
||||
Current.Add(value);
|
||||
}
|
||||
else
|
||||
Current.Remove(value);
|
||||
}
|
||||
|
@ -6,6 +6,7 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using osu.Framework.Allocation;
|
||||
using osu.Framework.Extensions.LocalisationExtensions;
|
||||
using osu.Framework.Graphics;
|
||||
using osu.Framework.Graphics.Containers;
|
||||
using osu.Framework.Graphics.Shapes;
|
||||
@ -51,7 +52,7 @@ namespace osu.Game.Overlays.Changelog
|
||||
Anchor = Anchor.TopCentre,
|
||||
Origin = Anchor.TopCentre,
|
||||
Margin = new MarginPadding { Top = 20 },
|
||||
Text = build.CreatedAt.Date.ToString("dd MMMM yyyy"),
|
||||
Text = build.CreatedAt.Date.ToLocalisableString("dd MMMM yyyy"),
|
||||
Font = OsuFont.GetFont(weight: FontWeight.Regular, size: 24),
|
||||
});
|
||||
|
||||
|
@ -7,6 +7,7 @@ using System;
|
||||
using System.Threading;
|
||||
using osu.Framework.Allocation;
|
||||
using osu.Framework.Extensions.Color4Extensions;
|
||||
using osu.Framework.Extensions.LocalisationExtensions;
|
||||
using osu.Framework.Graphics;
|
||||
using osu.Framework.Graphics.Containers;
|
||||
using osu.Framework.Graphics.Shapes;
|
||||
@ -114,7 +115,7 @@ namespace osu.Game.Overlays.Changelog
|
||||
{
|
||||
Anchor = Anchor.TopCentre,
|
||||
Origin = Anchor.TopCentre,
|
||||
Text = Build.CreatedAt.Date.ToString("dd MMMM yyyy"),
|
||||
Text = Build.CreatedAt.Date.ToLocalisableString("dd MMMM yyyy"),
|
||||
Font = OsuFont.GetFont(weight: FontWeight.Regular, size: 14),
|
||||
Margin = new MarginPadding { Top = 5 },
|
||||
Scale = new Vector2(1.25f),
|
||||
|
@ -6,6 +6,7 @@ using System.Collections.Generic;
|
||||
using osu.Framework.Allocation;
|
||||
using osu.Framework.Bindables;
|
||||
using osu.Framework.Extensions.Color4Extensions;
|
||||
using osu.Framework.Extensions.LocalisationExtensions;
|
||||
using osu.Framework.Graphics;
|
||||
using osu.Framework.Graphics.Containers;
|
||||
using osu.Framework.Graphics.Shapes;
|
||||
@ -175,9 +176,7 @@ namespace osu.Game.Overlays.Chat
|
||||
|
||||
private void updateTimestamp()
|
||||
{
|
||||
drawableTimestamp.Text = prefer24HourTime.Value
|
||||
? $@"{message.Timestamp.LocalDateTime:HH:mm:ss}"
|
||||
: $@"{message.Timestamp.LocalDateTime:hh:mm:ss tt}";
|
||||
drawableTimestamp.Text = message.Timestamp.LocalDateTime.ToLocalisableString(prefer24HourTime.Value ? @"HH:mm:ss" : @"hh:mm:ss tt");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -5,6 +5,7 @@
|
||||
|
||||
using System;
|
||||
using osu.Framework.Allocation;
|
||||
using osu.Framework.Extensions.LocalisationExtensions;
|
||||
using osu.Framework.Graphics;
|
||||
using osu.Framework.Graphics.Containers;
|
||||
using osu.Framework.Graphics.Cursor;
|
||||
@ -167,7 +168,7 @@ namespace osu.Game.Overlays.Dashboard.Home.News
|
||||
Origin = Anchor.TopRight,
|
||||
Font = OsuFont.GetFont(weight: FontWeight.Bold), // using Bold since there is no 800 weight alternative
|
||||
Colour = colourProvider.Light1,
|
||||
Text = $"{date:dd}"
|
||||
Text = date.ToLocalisableString(@"dd")
|
||||
},
|
||||
new TextFlowContainer(f =>
|
||||
{
|
||||
@ -178,7 +179,7 @@ namespace osu.Game.Overlays.Dashboard.Home.News
|
||||
Anchor = Anchor.TopRight,
|
||||
Origin = Anchor.TopRight,
|
||||
AutoSizeAxes = Axes.Both,
|
||||
Text = $"{date:MMM yyyy}"
|
||||
Text = date.ToLocalisableString(@"MMM yyyy")
|
||||
}
|
||||
}
|
||||
};
|
||||
|
@ -5,6 +5,7 @@
|
||||
|
||||
using System;
|
||||
using osu.Framework.Allocation;
|
||||
using osu.Framework.Extensions.LocalisationExtensions;
|
||||
using osu.Framework.Graphics;
|
||||
using osu.Framework.Graphics.Containers;
|
||||
using osu.Framework.Graphics.Cursor;
|
||||
@ -98,12 +99,12 @@ namespace osu.Game.Overlays.Dashboard.Home.News
|
||||
Margin = new MarginPadding { Vertical = 5 }
|
||||
};
|
||||
|
||||
textFlow.AddText($"{date:dd}", t =>
|
||||
textFlow.AddText(date.ToLocalisableString(@"dd"), t =>
|
||||
{
|
||||
t.Font = OsuFont.GetFont(size: 14, weight: FontWeight.Bold);
|
||||
});
|
||||
|
||||
textFlow.AddText($"{date: MMM}", t =>
|
||||
textFlow.AddText(date.ToLocalisableString(@" MMM"), t =>
|
||||
{
|
||||
t.Font = OsuFont.GetFont(size: 14, weight: FontWeight.Regular);
|
||||
});
|
||||
|
@ -198,6 +198,7 @@ namespace osu.Game.Overlays.Dialog
|
||||
TextAnchor = Anchor.TopCentre,
|
||||
RelativeSizeAxes = Axes.X,
|
||||
AutoSizeAxes = Axes.Y,
|
||||
Padding = new MarginPadding(5),
|
||||
},
|
||||
},
|
||||
},
|
||||
|
@ -7,6 +7,7 @@ using System.IO;
|
||||
using System.Linq;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using osu.Framework;
|
||||
using osu.Framework.Allocation;
|
||||
using osu.Framework.Bindables;
|
||||
using osu.Framework.Extensions;
|
||||
@ -14,12 +15,15 @@ using osu.Framework.Graphics;
|
||||
using osu.Framework.Graphics.Containers;
|
||||
using osu.Framework.Graphics.UserInterface;
|
||||
using osu.Framework.Localisation;
|
||||
using osu.Framework.Logging;
|
||||
using osu.Framework.Screens;
|
||||
using osu.Game.Database;
|
||||
using osu.Game.Graphics;
|
||||
using osu.Game.Graphics.Containers;
|
||||
using osu.Game.Graphics.UserInterfaceV2;
|
||||
using osu.Game.Localisation;
|
||||
using osu.Game.Overlays.Settings;
|
||||
using osu.Game.Overlays.Settings.Sections.Maintenance;
|
||||
using osu.Game.Screens.Edit.Setup;
|
||||
using osuTK;
|
||||
|
||||
@ -39,6 +43,8 @@ namespace osu.Game.Overlays.FirstRunSetup
|
||||
|
||||
private StableLocatorLabelledTextBox stableLocatorTextBox = null!;
|
||||
|
||||
private LinkFlowContainer copyInformation = null!;
|
||||
|
||||
private IEnumerable<ImportCheckbox> contentCheckboxes => Content.Children.OfType<ImportCheckbox>();
|
||||
|
||||
[BackgroundDependencyLoader(permitNulls: true)]
|
||||
@ -46,7 +52,7 @@ namespace osu.Game.Overlays.FirstRunSetup
|
||||
{
|
||||
Content.Children = new Drawable[]
|
||||
{
|
||||
new OsuTextFlowContainer(cp => cp.Font = OsuFont.Default.With(size: CONTENT_FONT_SIZE))
|
||||
new LinkFlowContainer(cp => cp.Font = OsuFont.Default.With(size: CONTENT_FONT_SIZE))
|
||||
{
|
||||
Colour = OverlayColourProvider.Content1,
|
||||
Text = FirstRunOverlayImportFromStableScreenStrings.Description,
|
||||
@ -62,6 +68,12 @@ namespace osu.Game.Overlays.FirstRunSetup
|
||||
new ImportCheckbox(CommonStrings.Scores, StableContent.Scores),
|
||||
new ImportCheckbox(CommonStrings.Skins, StableContent.Skins),
|
||||
new ImportCheckbox(CommonStrings.Collections, StableContent.Collections),
|
||||
copyInformation = new LinkFlowContainer(cp => cp.Font = OsuFont.Default.With(size: CONTENT_FONT_SIZE))
|
||||
{
|
||||
Colour = OverlayColourProvider.Content1,
|
||||
RelativeSizeAxes = Axes.X,
|
||||
AutoSizeAxes = Axes.Y
|
||||
},
|
||||
importButton = new ProgressRoundedButton
|
||||
{
|
||||
Size = button_size,
|
||||
@ -83,6 +95,9 @@ namespace osu.Game.Overlays.FirstRunSetup
|
||||
stableLocatorTextBox.Current.BindValueChanged(_ => updateStablePath(), true);
|
||||
}
|
||||
|
||||
[Resolved(canBeNull: true)]
|
||||
private OsuGame? game { get; set; }
|
||||
|
||||
private void updateStablePath()
|
||||
{
|
||||
var storage = legacyImportManager.GetCurrentStableStorage();
|
||||
@ -105,6 +120,25 @@ namespace osu.Game.Overlays.FirstRunSetup
|
||||
toggleInteraction(true);
|
||||
stableLocatorTextBox.Current.Value = storage.GetFullPath(string.Empty);
|
||||
importButton.Enabled.Value = true;
|
||||
|
||||
bool available = legacyImportManager.CheckHardLinkAvailability();
|
||||
Logger.Log($"Hard link support is {available}");
|
||||
|
||||
if (available)
|
||||
{
|
||||
copyInformation.Text = "Data migration will use \"hard links\". No extra disk space will be used, and you can delete either data folder at any point without affecting the other installation.";
|
||||
}
|
||||
else if (RuntimeInfo.OS != RuntimeInfo.Platform.Windows)
|
||||
copyInformation.Text = "Lightweight linking of files is not supported on your operating system yet, so a copy of all files will be made during import.";
|
||||
else
|
||||
{
|
||||
copyInformation.Text =
|
||||
"A second copy of all files will be made during import. To avoid this, please make sure the lazer data folder is on the same drive as your previous osu! install (and the file system is NTFS). ";
|
||||
copyInformation.AddLink(GeneralSettingsStrings.ChangeFolderLocation, () =>
|
||||
{
|
||||
game?.PerformFromScreen(menu => menu.Push(new MigrationSelectScreen()));
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
private void runImport()
|
||||
@ -235,7 +269,7 @@ namespace osu.Game.Overlays.FirstRunSetup
|
||||
return Task.CompletedTask;
|
||||
}
|
||||
|
||||
Task ICanAcceptFiles.Import(params ImportTask[] tasks) => throw new NotImplementedException();
|
||||
Task ICanAcceptFiles.Import(ImportTask[] tasks, ImportParameters parameters) => throw new NotImplementedException();
|
||||
|
||||
protected override void Dispose(bool isDisposing)
|
||||
{
|
||||
|
@ -19,6 +19,7 @@ using osu.Framework.Graphics.Sprites;
|
||||
using System.Diagnostics;
|
||||
using osu.Framework.Audio;
|
||||
using osu.Framework.Audio.Sample;
|
||||
using osu.Framework.Extensions.LocalisationExtensions;
|
||||
using osu.Framework.Platform;
|
||||
|
||||
namespace osu.Game.Overlays.News.Sidebar
|
||||
@ -99,7 +100,7 @@ namespace osu.Game.Overlays.News.Sidebar
|
||||
Anchor = Anchor.CentreLeft,
|
||||
Origin = Anchor.CentreLeft,
|
||||
Font = OsuFont.GetFont(size: 12, weight: FontWeight.Bold),
|
||||
Text = date.ToString("MMM yyyy")
|
||||
Text = date.ToLocalisableString(@"MMM yyyy")
|
||||
},
|
||||
icon = new SpriteIcon
|
||||
{
|
||||
|
@ -5,6 +5,7 @@
|
||||
|
||||
using System;
|
||||
using osu.Framework.Allocation;
|
||||
using osu.Framework.Extensions.LocalisationExtensions;
|
||||
using osu.Framework.Graphics;
|
||||
using osu.Game.Graphics;
|
||||
using osu.Game.Graphics.Sprites;
|
||||
@ -69,7 +70,7 @@ namespace osu.Game.Overlays.Toolbar
|
||||
|
||||
protected override void UpdateDisplay(DateTimeOffset now)
|
||||
{
|
||||
realTime.Text = use24HourDisplay ? $"{now:HH:mm:ss}" : $"{now:h:mm:ss tt}";
|
||||
realTime.Text = now.ToLocalisableString(use24HourDisplay ? @"HH:mm:ss" : @"h:mm:ss tt");
|
||||
gameTime.Text = $"running {new TimeSpan(TimeSpan.TicksPerSecond * (int)(Clock.CurrentTime / 1000)):c}";
|
||||
}
|
||||
|
||||
|
@ -145,9 +145,9 @@ namespace osu.Game.Scoring
|
||||
#pragma warning restore CS0618
|
||||
}
|
||||
|
||||
protected override void PostImport(ScoreInfo model, Realm realm, bool batchImport)
|
||||
protected override void PostImport(ScoreInfo model, Realm realm, ImportParameters parameters)
|
||||
{
|
||||
base.PostImport(model, realm, batchImport);
|
||||
base.PostImport(model, realm, parameters);
|
||||
|
||||
var userRequest = new GetUserRequest(model.RealmUser.Username);
|
||||
|
||||
|
@ -169,18 +169,18 @@ namespace osu.Game.Scoring
|
||||
|
||||
public Task Import(params string[] paths) => scoreImporter.Import(paths);
|
||||
|
||||
public Task Import(params ImportTask[] tasks) => scoreImporter.Import(tasks);
|
||||
public Task Import(ImportTask[] imports, ImportParameters parameters = default) => scoreImporter.Import(imports, parameters);
|
||||
|
||||
public override bool IsAvailableLocally(ScoreInfo model) => Realm.Run(realm => realm.All<ScoreInfo>().Any(s => s.OnlineID == model.OnlineID));
|
||||
|
||||
public IEnumerable<string> HandledExtensions => scoreImporter.HandledExtensions;
|
||||
|
||||
public Task<IEnumerable<Live<ScoreInfo>>> Import(ProgressNotification notification, params ImportTask[] tasks) => scoreImporter.Import(notification, tasks);
|
||||
public Task<IEnumerable<Live<ScoreInfo>>> Import(ProgressNotification notification, ImportTask[] tasks, ImportParameters parameters = default) => scoreImporter.Import(notification, tasks);
|
||||
|
||||
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, bool batchImport = false, CancellationToken cancellationToken = default) =>
|
||||
scoreImporter.ImportModel(item, archive, batchImport, cancellationToken);
|
||||
public Live<ScoreInfo> Import(ScoreInfo item, ArchiveReader archive = null, ImportParameters parameters = default, CancellationToken cancellationToken = default) =>
|
||||
scoreImporter.ImportModel(item, archive, parameters, cancellationToken);
|
||||
|
||||
/// <summary>
|
||||
/// Populates the <see cref="ScoreInfo.MaximumStatistics"/> for a given <see cref="ScoreInfo"/>.
|
||||
|
@ -91,7 +91,7 @@ namespace osu.Game.Screens.Edit.Setup
|
||||
return Task.CompletedTask;
|
||||
}
|
||||
|
||||
Task ICanAcceptFiles.Import(params ImportTask[] tasks) => throw new NotImplementedException();
|
||||
Task ICanAcceptFiles.Import(ImportTask[] tasks, ImportParameters parameters) => throw new NotImplementedException();
|
||||
|
||||
protected override void Dispose(bool isDisposing)
|
||||
{
|
||||
|
@ -9,6 +9,7 @@ using System.Linq;
|
||||
using osu.Framework.Allocation;
|
||||
using osu.Framework.Bindables;
|
||||
using osu.Framework.Extensions;
|
||||
using osu.Framework.Extensions.LocalisationExtensions;
|
||||
using osu.Framework.Graphics;
|
||||
using osu.Framework.Graphics.Containers;
|
||||
using osu.Framework.Localisation;
|
||||
@ -309,7 +310,8 @@ namespace osu.Game.Screens.Ranking.Expanded
|
||||
|
||||
private void updateDisplay()
|
||||
{
|
||||
Text = prefer24HourTime.Value ? $"Played on {time.ToLocalTime():d MMMM yyyy HH:mm}" : $"Played on {time.ToLocalTime():d MMMM yyyy h:mm tt}";
|
||||
Text = LocalisableString.Format("Played on {0}",
|
||||
time.ToLocalTime().ToLocalisableString(prefer24HourTime.Value ? @"d MMMM yyyy HH:mm" : @"d MMMM yyyy h:mm tt"));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -225,7 +225,7 @@ namespace osu.Game.Screens.Ranking
|
||||
protected override void Update()
|
||||
{
|
||||
base.Update();
|
||||
audioContent.Balance.Value = (ScreenSpaceDrawQuad.Centre.X / game.ScreenSpaceDrawQuad.Width) * 2 - 1;
|
||||
audioContent.Balance.Value = ((ScreenSpaceDrawQuad.Centre.X / game.ScreenSpaceDrawQuad.Width) * 2 - 1) * OsuGameBase.SFX_STEREO_STRENGTH;
|
||||
}
|
||||
|
||||
private void playAppearSample()
|
||||
|
@ -411,7 +411,7 @@ namespace osu.Game.Skinning.Editor
|
||||
return Task.CompletedTask;
|
||||
}
|
||||
|
||||
public Task Import(params ImportTask[] tasks) => throw new NotImplementedException();
|
||||
Task ICanAcceptFiles.Import(ImportTask[] tasks, ImportParameters parameters) => throw new NotImplementedException();
|
||||
|
||||
public IEnumerable<string> HandledExtensions => new[] { ".jpg", ".jpeg", ".png" };
|
||||
|
||||
|
@ -270,15 +270,18 @@ namespace osu.Game.Skinning
|
||||
|
||||
public Task Import(params string[] paths) => skinImporter.Import(paths);
|
||||
|
||||
public Task Import(params ImportTask[] tasks) => skinImporter.Import(tasks);
|
||||
public Task Import(ImportTask[] imports, ImportParameters parameters = default) => skinImporter.Import(imports, parameters);
|
||||
|
||||
public IEnumerable<string> HandledExtensions => skinImporter.HandledExtensions;
|
||||
|
||||
public Task<IEnumerable<Live<SkinInfo>>> Import(ProgressNotification notification, params ImportTask[] tasks) => skinImporter.Import(notification, tasks);
|
||||
public Task<IEnumerable<Live<SkinInfo>>> Import(ProgressNotification notification, ImportTask[] tasks, ImportParameters parameters = default) =>
|
||||
skinImporter.Import(notification, tasks, parameters);
|
||||
|
||||
public Task<Live<SkinInfo>> ImportAsUpdate(ProgressNotification notification, ImportTask task, SkinInfo original) => skinImporter.ImportAsUpdate(notification, task, original);
|
||||
public Task<Live<SkinInfo>> ImportAsUpdate(ProgressNotification notification, ImportTask task, SkinInfo original) =>
|
||||
skinImporter.ImportAsUpdate(notification, task, original);
|
||||
|
||||
public Task<Live<SkinInfo>> Import(ImportTask task, bool batchImport = false, CancellationToken cancellationToken = default) => skinImporter.Import(task, batchImport, cancellationToken);
|
||||
public Task<Live<SkinInfo>> Import(ImportTask task, ImportParameters parameters = default, CancellationToken cancellationToken = default) =>
|
||||
skinImporter.Import(task, parameters, cancellationToken);
|
||||
|
||||
#endregion
|
||||
|
||||
|
Loading…
Reference in New Issue
Block a user