mirror of
https://github.com/ppy/osu.git
synced 2025-01-27 02:32:59 +08:00
Update all usages of CreateContext
to use either Run
or Write
This commit is contained in:
parent
da0a803e6c
commit
114c9e8c1f
@ -55,8 +55,7 @@ namespace osu.Game.Tests.Beatmaps.IO
|
||||
{
|
||||
var realmContextFactory = osu.Dependencies.Get<RealmContextFactory>();
|
||||
|
||||
using (var realm = realmContextFactory.CreateContext())
|
||||
BeatmapImporterTests.EnsureLoaded(realm, timeout);
|
||||
realmContextFactory.Run(realm => BeatmapImporterTests.EnsureLoaded(realm, timeout));
|
||||
|
||||
// TODO: add back some extra checks outside of the realm ones?
|
||||
// var set = queryBeatmapSets().First();
|
||||
|
@ -21,7 +21,7 @@ namespace osu.Game.Tests.Database
|
||||
[Test]
|
||||
public void TestConstructRealm()
|
||||
{
|
||||
RunTestWithRealm((realmFactory, _) => { realmFactory.CreateContext().Refresh(); });
|
||||
RunTestWithRealm((realmFactory, _) => { realmFactory.Run(realm => realm.Refresh()); });
|
||||
}
|
||||
|
||||
[Test]
|
||||
@ -46,23 +46,21 @@ namespace osu.Game.Tests.Database
|
||||
{
|
||||
bool callbackRan = false;
|
||||
|
||||
using (var context = realmFactory.CreateContext())
|
||||
realmFactory.Run(realm =>
|
||||
{
|
||||
var subscription = context.All<BeatmapInfo>().QueryAsyncWithNotifications((sender, changes, error) =>
|
||||
var subscription = realm.All<BeatmapInfo>().QueryAsyncWithNotifications((sender, changes, error) =>
|
||||
{
|
||||
using (realmFactory.CreateContext())
|
||||
realmFactory.Run(_ =>
|
||||
{
|
||||
callbackRan = true;
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
// Force the callback above to run.
|
||||
using (realmFactory.CreateContext())
|
||||
{
|
||||
}
|
||||
realmFactory.Run(r => r.Refresh());
|
||||
|
||||
subscription?.Dispose();
|
||||
}
|
||||
});
|
||||
|
||||
Assert.IsTrue(callbackRan);
|
||||
});
|
||||
@ -78,12 +76,12 @@ namespace osu.Game.Tests.Database
|
||||
|
||||
Task.Factory.StartNew(() =>
|
||||
{
|
||||
using (realmFactory.CreateContext())
|
||||
realmFactory.Run(_ =>
|
||||
{
|
||||
hasThreadedUsage.Set();
|
||||
|
||||
stopThreadedUsage.Wait();
|
||||
}
|
||||
});
|
||||
}, TaskCreationOptions.LongRunning | TaskCreationOptions.HideScheduler);
|
||||
|
||||
hasThreadedUsage.Wait();
|
||||
|
@ -23,9 +23,9 @@ namespace osu.Game.Tests.Database
|
||||
{
|
||||
RunTestWithRealm((realmFactory, _) =>
|
||||
{
|
||||
ILive<BeatmapInfo> beatmap = realmFactory.CreateContext().Write(r => r.Add(new BeatmapInfo(CreateRuleset(), new BeatmapDifficulty(), new BeatmapMetadata()))).ToLive(realmFactory);
|
||||
ILive<BeatmapInfo> beatmap = realmFactory.Run(realm => realm.Write(r => r.Add(new BeatmapInfo(CreateRuleset(), new BeatmapDifficulty(), new BeatmapMetadata()))).ToLive(realmFactory));
|
||||
|
||||
ILive<BeatmapInfo> beatmap2 = realmFactory.CreateContext().All<BeatmapInfo>().First().ToLive(realmFactory);
|
||||
ILive<BeatmapInfo> beatmap2 = realmFactory.Run(realm => realm.All<BeatmapInfo>().First().ToLive(realmFactory));
|
||||
|
||||
Assert.AreEqual(beatmap, beatmap2);
|
||||
});
|
||||
@ -38,14 +38,14 @@ namespace osu.Game.Tests.Database
|
||||
{
|
||||
var beatmap = new BeatmapInfo(CreateRuleset(), new BeatmapDifficulty(), new BeatmapMetadata());
|
||||
|
||||
ILive<BeatmapInfo> liveBeatmap;
|
||||
ILive<BeatmapInfo>? liveBeatmap = null;
|
||||
|
||||
using (var context = realmFactory.CreateContext())
|
||||
realmFactory.Run(realm =>
|
||||
{
|
||||
context.Write(r => r.Add(beatmap));
|
||||
realm.Write(r => r.Add(beatmap));
|
||||
|
||||
liveBeatmap = beatmap.ToLive(realmFactory);
|
||||
}
|
||||
});
|
||||
|
||||
using (var migratedStorage = new TemporaryNativeStorage("realm-test-migration-target"))
|
||||
{
|
||||
@ -53,7 +53,7 @@ namespace osu.Game.Tests.Database
|
||||
|
||||
storage.Migrate(migratedStorage);
|
||||
|
||||
Assert.IsFalse(liveBeatmap.PerformRead(l => l.Hidden));
|
||||
Assert.IsFalse(liveBeatmap?.PerformRead(l => l.Hidden));
|
||||
}
|
||||
});
|
||||
}
|
||||
@ -67,8 +67,7 @@ namespace osu.Game.Tests.Database
|
||||
|
||||
var liveBeatmap = beatmap.ToLive(realmFactory);
|
||||
|
||||
using (var context = realmFactory.CreateContext())
|
||||
context.Write(r => r.Add(beatmap));
|
||||
realmFactory.Run(realm => realm.Write(r => r.Add(beatmap)));
|
||||
|
||||
Assert.IsFalse(liveBeatmap.PerformRead(l => l.Hidden));
|
||||
});
|
||||
@ -99,12 +98,12 @@ namespace osu.Game.Tests.Database
|
||||
ILive<BeatmapInfo>? liveBeatmap = null;
|
||||
Task.Factory.StartNew(() =>
|
||||
{
|
||||
using (var threadContext = realmFactory.CreateContext())
|
||||
realmFactory.Run(threadContext =>
|
||||
{
|
||||
var beatmap = threadContext.Write(r => r.Add(new BeatmapInfo(CreateRuleset(), new BeatmapDifficulty(), new BeatmapMetadata())));
|
||||
|
||||
liveBeatmap = beatmap.ToLive(realmFactory);
|
||||
}
|
||||
});
|
||||
}, TaskCreationOptions.LongRunning | TaskCreationOptions.HideScheduler).WaitSafely();
|
||||
|
||||
Debug.Assert(liveBeatmap != null);
|
||||
@ -128,12 +127,12 @@ namespace osu.Game.Tests.Database
|
||||
ILive<BeatmapInfo>? liveBeatmap = null;
|
||||
Task.Factory.StartNew(() =>
|
||||
{
|
||||
using (var threadContext = realmFactory.CreateContext())
|
||||
realmFactory.Run(threadContext =>
|
||||
{
|
||||
var beatmap = threadContext.Write(r => r.Add(new BeatmapInfo(CreateRuleset(), new BeatmapDifficulty(), new BeatmapMetadata())));
|
||||
|
||||
liveBeatmap = beatmap.ToLive(realmFactory);
|
||||
}
|
||||
});
|
||||
}, TaskCreationOptions.LongRunning | TaskCreationOptions.HideScheduler).WaitSafely();
|
||||
|
||||
Debug.Assert(liveBeatmap != null);
|
||||
@ -170,12 +169,12 @@ namespace osu.Game.Tests.Database
|
||||
|
||||
Task.Factory.StartNew(() =>
|
||||
{
|
||||
using (var threadContext = realmFactory.CreateContext())
|
||||
realmFactory.Run(threadContext =>
|
||||
{
|
||||
var beatmap = threadContext.Write(r => r.Add(new BeatmapInfo(CreateRuleset(), new BeatmapDifficulty(), new BeatmapMetadata())));
|
||||
|
||||
liveBeatmap = beatmap.ToLive(realmFactory);
|
||||
}
|
||||
});
|
||||
}, TaskCreationOptions.LongRunning | TaskCreationOptions.HideScheduler).WaitSafely();
|
||||
|
||||
Debug.Assert(liveBeatmap != null);
|
||||
@ -189,13 +188,13 @@ namespace osu.Game.Tests.Database
|
||||
});
|
||||
|
||||
// Can't be used, even from within a valid context.
|
||||
using (realmFactory.CreateContext())
|
||||
realmFactory.Run(threadContext =>
|
||||
{
|
||||
Assert.Throws<InvalidOperationException>(() =>
|
||||
{
|
||||
var __ = liveBeatmap.Value;
|
||||
});
|
||||
}
|
||||
});
|
||||
}, TaskCreationOptions.LongRunning | TaskCreationOptions.HideScheduler).WaitSafely();
|
||||
});
|
||||
}
|
||||
@ -208,12 +207,12 @@ namespace osu.Game.Tests.Database
|
||||
ILive<BeatmapInfo>? liveBeatmap = null;
|
||||
Task.Factory.StartNew(() =>
|
||||
{
|
||||
using (var threadContext = realmFactory.CreateContext())
|
||||
realmFactory.Run(threadContext =>
|
||||
{
|
||||
var beatmap = threadContext.Write(r => r.Add(new BeatmapInfo(CreateRuleset(), new BeatmapDifficulty(), new BeatmapMetadata())));
|
||||
|
||||
liveBeatmap = beatmap.ToLive(realmFactory);
|
||||
}
|
||||
});
|
||||
}, TaskCreationOptions.LongRunning | TaskCreationOptions.HideScheduler).WaitSafely();
|
||||
|
||||
Debug.Assert(liveBeatmap != null);
|
||||
@ -235,50 +234,50 @@ namespace osu.Game.Tests.Database
|
||||
{
|
||||
int changesTriggered = 0;
|
||||
|
||||
using (var updateThreadContext = realmFactory.CreateContext())
|
||||
realmFactory.Run(outerRealm =>
|
||||
{
|
||||
updateThreadContext.All<BeatmapInfo>().QueryAsyncWithNotifications(gotChange);
|
||||
outerRealm.All<BeatmapInfo>().QueryAsyncWithNotifications(gotChange);
|
||||
ILive<BeatmapInfo>? liveBeatmap = null;
|
||||
|
||||
Task.Factory.StartNew(() =>
|
||||
{
|
||||
using (var threadContext = realmFactory.CreateContext())
|
||||
realmFactory.Run(innerRealm =>
|
||||
{
|
||||
var ruleset = CreateRuleset();
|
||||
var beatmap = threadContext.Write(r => r.Add(new BeatmapInfo(ruleset, new BeatmapDifficulty(), new BeatmapMetadata())));
|
||||
var beatmap = innerRealm.Write(r => r.Add(new BeatmapInfo(ruleset, new BeatmapDifficulty(), new BeatmapMetadata())));
|
||||
|
||||
// add a second beatmap to ensure that a full refresh occurs below.
|
||||
// not just a refresh from the resolved Live.
|
||||
threadContext.Write(r => r.Add(new BeatmapInfo(ruleset, new BeatmapDifficulty(), new BeatmapMetadata())));
|
||||
innerRealm.Write(r => r.Add(new BeatmapInfo(ruleset, new BeatmapDifficulty(), new BeatmapMetadata())));
|
||||
|
||||
liveBeatmap = beatmap.ToLive(realmFactory);
|
||||
}
|
||||
});
|
||||
}, TaskCreationOptions.LongRunning | TaskCreationOptions.HideScheduler).WaitSafely();
|
||||
|
||||
Debug.Assert(liveBeatmap != null);
|
||||
|
||||
// not yet seen by main context
|
||||
Assert.AreEqual(0, updateThreadContext.All<BeatmapInfo>().Count());
|
||||
Assert.AreEqual(0, outerRealm.All<BeatmapInfo>().Count());
|
||||
Assert.AreEqual(0, changesTriggered);
|
||||
|
||||
liveBeatmap.PerformRead(resolved =>
|
||||
{
|
||||
// retrieval causes an implicit refresh. even changes that aren't related to the retrieval are fired at this point.
|
||||
// ReSharper disable once AccessToDisposedClosure
|
||||
Assert.AreEqual(2, updateThreadContext.All<BeatmapInfo>().Count());
|
||||
Assert.AreEqual(2, outerRealm.All<BeatmapInfo>().Count());
|
||||
Assert.AreEqual(1, changesTriggered);
|
||||
|
||||
// can access properties without a crash.
|
||||
Assert.IsFalse(resolved.Hidden);
|
||||
|
||||
// ReSharper disable once AccessToDisposedClosure
|
||||
updateThreadContext.Write(r =>
|
||||
outerRealm.Write(r =>
|
||||
{
|
||||
// can use with the main context.
|
||||
r.Remove(resolved);
|
||||
});
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
void gotChange(IRealmCollection<BeatmapInfo> sender, ChangeSet changes, Exception error)
|
||||
{
|
||||
|
@ -60,15 +60,12 @@ namespace osu.Game.Tests.Database
|
||||
KeyBindingContainer testContainer = new TestKeyBindingContainer();
|
||||
|
||||
// Add some excess bindings for an action which only supports 1.
|
||||
using (var realm = realmContextFactory.CreateContext())
|
||||
using (var transaction = realm.BeginWrite())
|
||||
realmContextFactory.Write(realm =>
|
||||
{
|
||||
realm.Add(new RealmKeyBinding(GlobalAction.Back, new KeyCombination(InputKey.A)));
|
||||
realm.Add(new RealmKeyBinding(GlobalAction.Back, new KeyCombination(InputKey.S)));
|
||||
realm.Add(new RealmKeyBinding(GlobalAction.Back, new KeyCombination(InputKey.D)));
|
||||
|
||||
transaction.Commit();
|
||||
}
|
||||
});
|
||||
|
||||
Assert.That(queryCount(GlobalAction.Back), Is.EqualTo(3));
|
||||
|
||||
@ -79,13 +76,13 @@ namespace osu.Game.Tests.Database
|
||||
|
||||
private int queryCount(GlobalAction? match = null)
|
||||
{
|
||||
using (var realm = realmContextFactory.CreateContext())
|
||||
return realmContextFactory.Run(realm =>
|
||||
{
|
||||
var results = realm.All<RealmKeyBinding>();
|
||||
if (match.HasValue)
|
||||
results = results.Where(k => k.ActionInt == (int)match.Value);
|
||||
return results.Count();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
[Test]
|
||||
@ -95,26 +92,26 @@ namespace osu.Game.Tests.Database
|
||||
|
||||
keyBindingStore.Register(testContainer, Enumerable.Empty<RulesetInfo>());
|
||||
|
||||
using (var primaryRealm = realmContextFactory.CreateContext())
|
||||
realmContextFactory.Run(outerRealm =>
|
||||
{
|
||||
var backBinding = primaryRealm.All<RealmKeyBinding>().Single(k => k.ActionInt == (int)GlobalAction.Back);
|
||||
var backBinding = outerRealm.All<RealmKeyBinding>().Single(k => k.ActionInt == (int)GlobalAction.Back);
|
||||
|
||||
Assert.That(backBinding.KeyCombination.Keys, Is.EquivalentTo(new[] { InputKey.Escape }));
|
||||
|
||||
var tsr = ThreadSafeReference.Create(backBinding);
|
||||
|
||||
using (var threadedContext = realmContextFactory.CreateContext())
|
||||
realmContextFactory.Run(innerRealm =>
|
||||
{
|
||||
var binding = threadedContext.ResolveReference(tsr);
|
||||
threadedContext.Write(() => binding.KeyCombination = new KeyCombination(InputKey.BackSpace));
|
||||
}
|
||||
var binding = innerRealm.ResolveReference(tsr);
|
||||
innerRealm.Write(() => binding.KeyCombination = new KeyCombination(InputKey.BackSpace));
|
||||
});
|
||||
|
||||
Assert.That(backBinding.KeyCombination.Keys, Is.EquivalentTo(new[] { InputKey.BackSpace }));
|
||||
|
||||
// check still correct after re-query.
|
||||
backBinding = primaryRealm.All<RealmKeyBinding>().Single(k => k.ActionInt == (int)GlobalAction.Back);
|
||||
backBinding = outerRealm.All<RealmKeyBinding>().Single(k => k.ActionInt == (int)GlobalAction.Back);
|
||||
Assert.That(backBinding.KeyCombination.Keys, Is.EquivalentTo(new[] { InputKey.BackSpace }));
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
[TearDown]
|
||||
|
@ -60,8 +60,8 @@ namespace osu.Game.Tests.Online
|
||||
testBeatmapInfo = getTestBeatmapInfo(testBeatmapFile);
|
||||
testBeatmapSet = testBeatmapInfo.BeatmapSet;
|
||||
|
||||
ContextFactory.Context.Write(r => r.RemoveAll<BeatmapSetInfo>());
|
||||
ContextFactory.Context.Write(r => r.RemoveAll<BeatmapInfo>());
|
||||
ContextFactory.Write(r => r.RemoveAll<BeatmapSetInfo>());
|
||||
ContextFactory.Write(r => r.RemoveAll<BeatmapInfo>());
|
||||
|
||||
selectedItem.Value = new PlaylistItem
|
||||
{
|
||||
|
@ -42,7 +42,7 @@ namespace osu.Game.Tests.Visual.Ranking
|
||||
{
|
||||
base.LoadComplete();
|
||||
|
||||
using (var realm = realmContextFactory.CreateContext())
|
||||
realmContextFactory.Run(realm =>
|
||||
{
|
||||
var beatmapInfo = realm.All<BeatmapInfo>()
|
||||
.Filter($"{nameof(BeatmapInfo.Ruleset)}.{nameof(RulesetInfo.OnlineID)} = $0", 0)
|
||||
@ -50,7 +50,7 @@ namespace osu.Game.Tests.Visual.Ranking
|
||||
|
||||
if (beatmapInfo != null)
|
||||
Beatmap.Value = beatmaps.GetWorkingBeatmap(beatmapInfo);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
[Test]
|
||||
|
@ -122,11 +122,11 @@ namespace osu.Game.Tests.Visual.UserInterface
|
||||
[SetUp]
|
||||
public void Setup() => Schedule(() =>
|
||||
{
|
||||
using (var realm = realmFactory.CreateContext())
|
||||
realmFactory.Run(realm =>
|
||||
{
|
||||
// Due to soft deletions, we can re-use deleted scores between test runs
|
||||
scoreManager.Undelete(realm.All<ScoreInfo>().Where(s => s.DeletePending).ToList());
|
||||
}
|
||||
});
|
||||
|
||||
leaderboard.Scores = null;
|
||||
leaderboard.FinishTransforms(true); // After setting scores, we may be waiting for transforms to expire drawables
|
||||
|
@ -153,14 +153,16 @@ namespace osu.Game.Beatmaps
|
||||
|
||||
public void RestoreAll()
|
||||
{
|
||||
using (var realm = contextFactory.CreateContext())
|
||||
using (var transaction = realm.BeginWrite())
|
||||
contextFactory.Run(realm =>
|
||||
{
|
||||
foreach (var beatmap in realm.All<BeatmapInfo>().Where(b => b.Hidden))
|
||||
beatmap.Hidden = false;
|
||||
using (var transaction = realm.BeginWrite())
|
||||
{
|
||||
foreach (var beatmap in realm.All<BeatmapInfo>().Where(b => b.Hidden))
|
||||
beatmap.Hidden = false;
|
||||
|
||||
transaction.Commit();
|
||||
}
|
||||
transaction.Commit();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
@ -169,8 +171,7 @@ namespace osu.Game.Beatmaps
|
||||
/// <returns>A list of available <see cref="BeatmapSetInfo"/>.</returns>
|
||||
public List<BeatmapSetInfo> GetAllUsableBeatmapSets()
|
||||
{
|
||||
using (var context = contextFactory.CreateContext())
|
||||
return context.All<BeatmapSetInfo>().Where(b => !b.DeletePending).Detach();
|
||||
return contextFactory.Run(realm => realm.All<BeatmapSetInfo>().Where(b => !b.DeletePending).Detach());
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
@ -235,21 +236,20 @@ namespace osu.Game.Beatmaps
|
||||
|
||||
public void Delete(Expression<Func<BeatmapSetInfo, bool>>? filter = null, bool silent = false)
|
||||
{
|
||||
using (var context = contextFactory.CreateContext())
|
||||
contextFactory.Run(realm =>
|
||||
{
|
||||
var items = context.All<BeatmapSetInfo>().Where(s => !s.DeletePending && !s.Protected);
|
||||
var items = realm.All<BeatmapSetInfo>().Where(s => !s.DeletePending && !s.Protected);
|
||||
|
||||
if (filter != null)
|
||||
items = items.Where(filter);
|
||||
|
||||
beatmapModelManager.Delete(items.ToList(), silent);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
public void UndeleteAll()
|
||||
{
|
||||
using (var context = contextFactory.CreateContext())
|
||||
beatmapModelManager.Undelete(context.All<BeatmapSetInfo>().Where(s => s.DeletePending).ToList());
|
||||
contextFactory.Run(realm => beatmapModelManager.Undelete(realm.All<BeatmapSetInfo>().Where(s => s.DeletePending).ToList()));
|
||||
}
|
||||
|
||||
public void Undelete(List<BeatmapSetInfo> items, bool silent = false)
|
||||
|
@ -103,10 +103,10 @@ namespace osu.Game.Beatmaps
|
||||
|
||||
public void Update(BeatmapSetInfo item)
|
||||
{
|
||||
ContextFactory.Run(realm =>
|
||||
ContextFactory.Write(realm =>
|
||||
{
|
||||
var existing = realm.Find<BeatmapSetInfo>(item.ID);
|
||||
realm.Write(r => item.CopyChangesToRealm(existing));
|
||||
item.CopyChangesToRealm(existing);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
@ -73,7 +73,7 @@ namespace osu.Game.Database
|
||||
|
||||
int count = existingBeatmapSets.Count();
|
||||
|
||||
using (var realm = realmContextFactory.CreateContext())
|
||||
realmContextFactory.Run(realm =>
|
||||
{
|
||||
Logger.Log($"Found {count} beatmaps in EF", LoggingTarget.Database);
|
||||
|
||||
@ -160,7 +160,7 @@ namespace osu.Game.Database
|
||||
|
||||
Logger.Log($"Successfully migrated {count} beatmaps to realm", LoggingTarget.Database);
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
private BeatmapMetadata getBestMetadata(EFBeatmapMetadata? beatmapMetadata, EFBeatmapMetadata? beatmapSetMetadata)
|
||||
@ -206,7 +206,7 @@ namespace osu.Game.Database
|
||||
|
||||
int count = existingScores.Count();
|
||||
|
||||
using (var realm = realmContextFactory.CreateContext())
|
||||
realmContextFactory.Run(realm =>
|
||||
{
|
||||
Logger.Log($"Found {count} scores in EF", LoggingTarget.Database);
|
||||
|
||||
@ -276,7 +276,7 @@ namespace osu.Game.Database
|
||||
|
||||
Logger.Log($"Successfully migrated {count} scores to realm", LoggingTarget.Database);
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
private void migrateSkins(OsuDbContext db)
|
||||
@ -307,37 +307,39 @@ namespace osu.Game.Database
|
||||
break;
|
||||
}
|
||||
|
||||
using (var realm = realmContextFactory.CreateContext())
|
||||
using (var transaction = realm.BeginWrite())
|
||||
realmContextFactory.Run(realm =>
|
||||
{
|
||||
// only migrate data if the realm database is empty.
|
||||
// note that this cannot be written as: `realm.All<SkinInfo>().All(s => s.Protected)`, because realm does not support `.All()`.
|
||||
if (!realm.All<SkinInfo>().Any(s => !s.Protected))
|
||||
using (var transaction = realm.BeginWrite())
|
||||
{
|
||||
Logger.Log($"Migrating {existingSkins.Count} skins", LoggingTarget.Database);
|
||||
|
||||
foreach (var skin in existingSkins)
|
||||
// only migrate data if the realm database is empty.
|
||||
// note that this cannot be written as: `realm.All<SkinInfo>().All(s => s.Protected)`, because realm does not support `.All()`.
|
||||
if (!realm.All<SkinInfo>().Any(s => !s.Protected))
|
||||
{
|
||||
var realmSkin = new SkinInfo
|
||||
Logger.Log($"Migrating {existingSkins.Count} skins", LoggingTarget.Database);
|
||||
|
||||
foreach (var skin in existingSkins)
|
||||
{
|
||||
Name = skin.Name,
|
||||
Creator = skin.Creator,
|
||||
Hash = skin.Hash,
|
||||
Protected = false,
|
||||
InstantiationInfo = skin.InstantiationInfo,
|
||||
};
|
||||
var realmSkin = new SkinInfo
|
||||
{
|
||||
Name = skin.Name,
|
||||
Creator = skin.Creator,
|
||||
Hash = skin.Hash,
|
||||
Protected = false,
|
||||
InstantiationInfo = skin.InstantiationInfo,
|
||||
};
|
||||
|
||||
migrateFiles(skin, realm, realmSkin);
|
||||
migrateFiles(skin, realm, realmSkin);
|
||||
|
||||
realm.Add(realmSkin);
|
||||
realm.Add(realmSkin);
|
||||
|
||||
if (skin.ID == userSkinInt)
|
||||
userSkinChoice.Value = realmSkin.ID.ToString();
|
||||
if (skin.ID == userSkinInt)
|
||||
userSkinChoice.Value = realmSkin.ID.ToString();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
transaction.Commit();
|
||||
}
|
||||
transaction.Commit();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
private static void migrateFiles<T>(IHasFiles<T> fileSource, Realm realm, IHasRealmFiles realmObject) where T : INamedFileInfo
|
||||
@ -365,36 +367,38 @@ namespace osu.Game.Database
|
||||
Logger.Log("Beginning settings migration to realm", LoggingTarget.Database);
|
||||
ensureBackup();
|
||||
|
||||
using (var realm = realmContextFactory.CreateContext())
|
||||
using (var transaction = realm.BeginWrite())
|
||||
realmContextFactory.Run(realm =>
|
||||
{
|
||||
// only migrate data if the realm database is empty.
|
||||
if (!realm.All<RealmRulesetSetting>().Any())
|
||||
using (var transaction = realm.BeginWrite())
|
||||
{
|
||||
Logger.Log($"Migrating {existingSettings.Count} settings", LoggingTarget.Database);
|
||||
|
||||
foreach (var dkb in existingSettings)
|
||||
// only migrate data if the realm database is empty.
|
||||
if (!realm.All<RealmRulesetSetting>().Any())
|
||||
{
|
||||
if (dkb.RulesetID == null)
|
||||
continue;
|
||||
Logger.Log($"Migrating {existingSettings.Count} settings", LoggingTarget.Database);
|
||||
|
||||
string? shortName = getRulesetShortNameFromLegacyID(dkb.RulesetID.Value);
|
||||
|
||||
if (string.IsNullOrEmpty(shortName))
|
||||
continue;
|
||||
|
||||
realm.Add(new RealmRulesetSetting
|
||||
foreach (var dkb in existingSettings)
|
||||
{
|
||||
Key = dkb.Key,
|
||||
Value = dkb.StringValue,
|
||||
RulesetName = shortName,
|
||||
Variant = dkb.Variant ?? 0,
|
||||
});
|
||||
}
|
||||
}
|
||||
if (dkb.RulesetID == null)
|
||||
continue;
|
||||
|
||||
transaction.Commit();
|
||||
}
|
||||
string? shortName = getRulesetShortNameFromLegacyID(dkb.RulesetID.Value);
|
||||
|
||||
if (string.IsNullOrEmpty(shortName))
|
||||
continue;
|
||||
|
||||
realm.Add(new RealmRulesetSetting
|
||||
{
|
||||
Key = dkb.Key,
|
||||
Value = dkb.StringValue,
|
||||
RulesetName = shortName,
|
||||
Variant = dkb.Variant ?? 0,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
transaction.Commit();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
private string? getRulesetShortNameFromLegacyID(long rulesetId) =>
|
||||
|
@ -51,8 +51,7 @@ namespace osu.Game.Database
|
||||
return;
|
||||
}
|
||||
|
||||
using (var realm = realmFactory.CreateContext())
|
||||
perform(realm.Find<T>(ID));
|
||||
realmFactory.Run(realm => perform(realm.Find<T>(ID)));
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
@ -64,7 +63,7 @@ namespace osu.Game.Database
|
||||
if (!IsManaged)
|
||||
return perform(data);
|
||||
|
||||
using (var realm = realmFactory.CreateContext())
|
||||
return realmFactory.Run(realm =>
|
||||
{
|
||||
var returnData = perform(realm.Find<T>(ID));
|
||||
|
||||
@ -72,7 +71,7 @@ namespace osu.Game.Database
|
||||
throw new InvalidOperationException(@$"Managed realm objects should not exit the scope of {nameof(PerformRead)}.");
|
||||
|
||||
return returnData;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
|
@ -34,7 +34,7 @@ namespace osu.Game.Input
|
||||
{
|
||||
List<string> combinations = new List<string>();
|
||||
|
||||
using (var context = realmFactory.CreateContext())
|
||||
realmFactory.Run(context =>
|
||||
{
|
||||
foreach (var action in context.All<RealmKeyBinding>().Where(b => string.IsNullOrEmpty(b.RulesetName) && (GlobalAction)b.ActionInt == globalAction))
|
||||
{
|
||||
@ -44,7 +44,7 @@ namespace osu.Game.Input
|
||||
if (str.Length > 0)
|
||||
combinations.Add(str);
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
return combinations;
|
||||
}
|
||||
@ -56,24 +56,26 @@ namespace osu.Game.Input
|
||||
/// <param name="rulesets">The rulesets to populate defaults from.</param>
|
||||
public void Register(KeyBindingContainer container, IEnumerable<RulesetInfo> rulesets)
|
||||
{
|
||||
using (var realm = realmFactory.CreateContext())
|
||||
using (var transaction = realm.BeginWrite())
|
||||
realmFactory.Run(realm =>
|
||||
{
|
||||
// intentionally flattened to a list rather than querying against the IQueryable, as nullable fields being queried against aren't indexed.
|
||||
// this is much faster as a result.
|
||||
var existingBindings = realm.All<RealmKeyBinding>().ToList();
|
||||
|
||||
insertDefaults(realm, existingBindings, container.DefaultKeyBindings);
|
||||
|
||||
foreach (var ruleset in rulesets)
|
||||
using (var transaction = realm.BeginWrite())
|
||||
{
|
||||
var instance = ruleset.CreateInstance();
|
||||
foreach (int variant in instance.AvailableVariants)
|
||||
insertDefaults(realm, existingBindings, instance.GetDefaultKeyBindings(variant), ruleset.ShortName, variant);
|
||||
}
|
||||
// intentionally flattened to a list rather than querying against the IQueryable, as nullable fields being queried against aren't indexed.
|
||||
// this is much faster as a result.
|
||||
var existingBindings = realm.All<RealmKeyBinding>().ToList();
|
||||
|
||||
transaction.Commit();
|
||||
}
|
||||
insertDefaults(realm, existingBindings, container.DefaultKeyBindings);
|
||||
|
||||
foreach (var ruleset in rulesets)
|
||||
{
|
||||
var instance = ruleset.CreateInstance();
|
||||
foreach (int variant in instance.AvailableVariants)
|
||||
insertDefaults(realm, existingBindings, instance.GetDefaultKeyBindings(variant), ruleset.ShortName, variant);
|
||||
}
|
||||
|
||||
transaction.Commit();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
private void insertDefaults(Realm realm, List<RealmKeyBinding> existingBindings, IEnumerable<IKeyBinding> defaults, string? rulesetName = null, int? variant = null)
|
||||
|
@ -34,10 +34,9 @@ namespace osu.Game.Overlays.Settings.Sections.Input
|
||||
{
|
||||
string rulesetName = Ruleset?.ShortName;
|
||||
|
||||
List<RealmKeyBinding> bindings;
|
||||
List<RealmKeyBinding> bindings = null;
|
||||
|
||||
using (var realm = realmFactory.CreateContext())
|
||||
bindings = realm.All<RealmKeyBinding>().Where(b => b.RulesetName == rulesetName && b.Variant == variant).Detach();
|
||||
realmFactory.Run(realm => bindings = realm.All<RealmKeyBinding>().Where(b => b.RulesetName == rulesetName && b.Variant == variant).Detach());
|
||||
|
||||
foreach (var defaultGroup in Defaults.GroupBy(d => d.Action))
|
||||
{
|
||||
|
@ -56,21 +56,15 @@ namespace osu.Game.Rulesets.Configuration
|
||||
pendingWrites.Clear();
|
||||
}
|
||||
|
||||
if (realmFactory == null)
|
||||
return true;
|
||||
|
||||
using (var context = realmFactory.CreateContext())
|
||||
realmFactory?.Write(realm =>
|
||||
{
|
||||
context.Write(realm =>
|
||||
foreach (var c in changed)
|
||||
{
|
||||
foreach (var c in changed)
|
||||
{
|
||||
var setting = realm.All<RealmRulesetSetting>().First(s => s.RulesetName == rulesetName && s.Variant == variant && s.Key == c.ToString());
|
||||
var setting = realm.All<RealmRulesetSetting>().First(s => s.RulesetName == rulesetName && s.Variant == variant && s.Key == c.ToString());
|
||||
|
||||
setting.Value = ConfigStore[c].ToString();
|
||||
}
|
||||
});
|
||||
}
|
||||
setting.Value = ConfigStore[c].ToString();
|
||||
}
|
||||
});
|
||||
|
||||
return true;
|
||||
}
|
||||
|
@ -100,74 +100,71 @@ namespace osu.Game.Rulesets
|
||||
|
||||
private void addMissingRulesets()
|
||||
{
|
||||
using (var context = realmFactory.CreateContext())
|
||||
realmFactory.Write(realm =>
|
||||
{
|
||||
context.Write(realm =>
|
||||
var rulesets = realm.All<RulesetInfo>();
|
||||
|
||||
List<Ruleset> instances = loadedAssemblies.Values
|
||||
.Select(r => Activator.CreateInstance(r) as Ruleset)
|
||||
.Where(r => r != null)
|
||||
.Select(r => r.AsNonNull())
|
||||
.ToList();
|
||||
|
||||
// add all legacy rulesets first to ensure they have exclusive choice of primary key.
|
||||
foreach (var r in instances.Where(r => r is ILegacyRuleset))
|
||||
{
|
||||
var rulesets = realm.All<RulesetInfo>();
|
||||
if (realm.All<RulesetInfo>().FirstOrDefault(rr => rr.OnlineID == r.RulesetInfo.OnlineID) == null)
|
||||
realm.Add(new RulesetInfo(r.RulesetInfo.ShortName, r.RulesetInfo.Name, r.RulesetInfo.InstantiationInfo, r.RulesetInfo.OnlineID));
|
||||
}
|
||||
|
||||
List<Ruleset> instances = loadedAssemblies.Values
|
||||
.Select(r => Activator.CreateInstance(r) as Ruleset)
|
||||
.Where(r => r != null)
|
||||
.Select(r => r.AsNonNull())
|
||||
.ToList();
|
||||
|
||||
// add all legacy rulesets first to ensure they have exclusive choice of primary key.
|
||||
foreach (var r in instances.Where(r => r is ILegacyRuleset))
|
||||
// add any other rulesets which have assemblies present but are not yet in the database.
|
||||
foreach (var r in instances.Where(r => !(r is ILegacyRuleset)))
|
||||
{
|
||||
if (rulesets.FirstOrDefault(ri => ri.InstantiationInfo.Equals(r.RulesetInfo.InstantiationInfo, StringComparison.Ordinal)) == null)
|
||||
{
|
||||
if (realm.All<RulesetInfo>().FirstOrDefault(rr => rr.OnlineID == r.RulesetInfo.OnlineID) == null)
|
||||
var existingSameShortName = rulesets.FirstOrDefault(ri => ri.ShortName == r.RulesetInfo.ShortName);
|
||||
|
||||
if (existingSameShortName != null)
|
||||
{
|
||||
// even if a matching InstantiationInfo was not found, there may be an existing ruleset with the same ShortName.
|
||||
// this generally means the user or ruleset provider has renamed their dll but the underlying ruleset is *likely* the same one.
|
||||
// in such cases, update the instantiation info of the existing entry to point to the new one.
|
||||
existingSameShortName.InstantiationInfo = r.RulesetInfo.InstantiationInfo;
|
||||
}
|
||||
else
|
||||
realm.Add(new RulesetInfo(r.RulesetInfo.ShortName, r.RulesetInfo.Name, r.RulesetInfo.InstantiationInfo, r.RulesetInfo.OnlineID));
|
||||
}
|
||||
}
|
||||
|
||||
// add any other rulesets which have assemblies present but are not yet in the database.
|
||||
foreach (var r in instances.Where(r => !(r is ILegacyRuleset)))
|
||||
List<RulesetInfo> detachedRulesets = new List<RulesetInfo>();
|
||||
|
||||
// perform a consistency check and detach final rulesets from realm for cross-thread runtime usage.
|
||||
foreach (var r in rulesets.OrderBy(r => r.OnlineID))
|
||||
{
|
||||
try
|
||||
{
|
||||
if (rulesets.FirstOrDefault(ri => ri.InstantiationInfo.Equals(r.RulesetInfo.InstantiationInfo, StringComparison.Ordinal)) == null)
|
||||
{
|
||||
var existingSameShortName = rulesets.FirstOrDefault(ri => ri.ShortName == r.RulesetInfo.ShortName);
|
||||
var resolvedType = Type.GetType(r.InstantiationInfo)
|
||||
?? throw new RulesetLoadException(@"Type could not be resolved");
|
||||
|
||||
if (existingSameShortName != null)
|
||||
{
|
||||
// even if a matching InstantiationInfo was not found, there may be an existing ruleset with the same ShortName.
|
||||
// this generally means the user or ruleset provider has renamed their dll but the underlying ruleset is *likely* the same one.
|
||||
// in such cases, update the instantiation info of the existing entry to point to the new one.
|
||||
existingSameShortName.InstantiationInfo = r.RulesetInfo.InstantiationInfo;
|
||||
}
|
||||
else
|
||||
realm.Add(new RulesetInfo(r.RulesetInfo.ShortName, r.RulesetInfo.Name, r.RulesetInfo.InstantiationInfo, r.RulesetInfo.OnlineID));
|
||||
}
|
||||
var instanceInfo = (Activator.CreateInstance(resolvedType) as Ruleset)?.RulesetInfo
|
||||
?? throw new RulesetLoadException(@"Instantiation failure");
|
||||
|
||||
r.Name = instanceInfo.Name;
|
||||
r.ShortName = instanceInfo.ShortName;
|
||||
r.InstantiationInfo = instanceInfo.InstantiationInfo;
|
||||
r.Available = true;
|
||||
|
||||
detachedRulesets.Add(r.Clone());
|
||||
}
|
||||
|
||||
List<RulesetInfo> detachedRulesets = new List<RulesetInfo>();
|
||||
|
||||
// perform a consistency check and detach final rulesets from realm for cross-thread runtime usage.
|
||||
foreach (var r in rulesets.OrderBy(r => r.OnlineID))
|
||||
catch (Exception ex)
|
||||
{
|
||||
try
|
||||
{
|
||||
var resolvedType = Type.GetType(r.InstantiationInfo)
|
||||
?? throw new RulesetLoadException(@"Type could not be resolved");
|
||||
|
||||
var instanceInfo = (Activator.CreateInstance(resolvedType) as Ruleset)?.RulesetInfo
|
||||
?? throw new RulesetLoadException(@"Instantiation failure");
|
||||
|
||||
r.Name = instanceInfo.Name;
|
||||
r.ShortName = instanceInfo.ShortName;
|
||||
r.InstantiationInfo = instanceInfo.InstantiationInfo;
|
||||
r.Available = true;
|
||||
|
||||
detachedRulesets.Add(r.Clone());
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
r.Available = false;
|
||||
Logger.Log($"Could not load ruleset {r}: {ex.Message}");
|
||||
}
|
||||
r.Available = false;
|
||||
Logger.Log($"Could not load ruleset {r}: {ex.Message}");
|
||||
}
|
||||
}
|
||||
|
||||
availableRulesets.AddRange(detachedRulesets);
|
||||
});
|
||||
}
|
||||
availableRulesets.AddRange(detachedRulesets);
|
||||
});
|
||||
}
|
||||
|
||||
private void loadFromAppDomain()
|
||||
|
@ -51,8 +51,7 @@ namespace osu.Game.Scoring
|
||||
/// <returns>The first result for the provided query, or null if no results were found.</returns>
|
||||
public ScoreInfo Query(Expression<Func<ScoreInfo, bool>> query)
|
||||
{
|
||||
using (var context = contextFactory.CreateContext())
|
||||
return context.All<ScoreInfo>().FirstOrDefault(query)?.Detach();
|
||||
return contextFactory.Run(realm => realm.All<ScoreInfo>().FirstOrDefault(query)?.Detach());
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
@ -255,16 +254,16 @@ namespace osu.Game.Scoring
|
||||
|
||||
public void Delete([CanBeNull] Expression<Func<ScoreInfo, bool>> filter = null, bool silent = false)
|
||||
{
|
||||
using (var context = contextFactory.CreateContext())
|
||||
contextFactory.Run(realm =>
|
||||
{
|
||||
var items = context.All<ScoreInfo>()
|
||||
.Where(s => !s.DeletePending);
|
||||
var items = realm.All<ScoreInfo>()
|
||||
.Where(s => !s.DeletePending);
|
||||
|
||||
if (filter != null)
|
||||
items = items.Where(filter);
|
||||
|
||||
scoreModelManager.Delete(items.ToList(), silent);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
public void Delete(List<ScoreInfo> items, bool silent = false)
|
||||
|
@ -178,8 +178,7 @@ namespace osu.Game.Screens.Select
|
||||
|
||||
if (!loadedTestBeatmaps)
|
||||
{
|
||||
using (var realm = realmFactory.CreateContext())
|
||||
loadBeatmapSets(getBeatmapSets(realm));
|
||||
realmFactory.Run(realm => loadBeatmapSets(getBeatmapSets(realm)));
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -147,7 +147,7 @@ namespace osu.Game.Screens.Select.Leaderboards
|
||||
|
||||
if (Scope == BeatmapLeaderboardScope.Local)
|
||||
{
|
||||
using (var realm = realmFactory.CreateContext())
|
||||
realmFactory.Run(realm =>
|
||||
{
|
||||
var scores = realm.All<ScoreInfo>()
|
||||
.AsEnumerable()
|
||||
@ -171,9 +171,9 @@ namespace osu.Game.Screens.Select.Leaderboards
|
||||
|
||||
scoreManager.OrderByTotalScoreAsync(scores.ToArray(), cancellationToken)
|
||||
.ContinueWith(ordered => scoresCallback?.Invoke(ordered.GetResultSafely()), TaskContinuationOptions.OnlyOnRanToCompletion);
|
||||
});
|
||||
|
||||
return null;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
if (api?.IsLoggedIn != true)
|
||||
|
@ -87,17 +87,14 @@ namespace osu.Game.Skinning
|
||||
};
|
||||
|
||||
// Ensure the default entries are present.
|
||||
using (var context = contextFactory.CreateContext())
|
||||
using (var transaction = context.BeginWrite())
|
||||
contextFactory.Write(realm =>
|
||||
{
|
||||
foreach (var skin in defaultSkins)
|
||||
{
|
||||
if (context.Find<SkinInfo>(skin.SkinInfo.ID) == null)
|
||||
context.Add(skin.SkinInfo.Value);
|
||||
if (realm.Find<SkinInfo>(skin.SkinInfo.ID) == null)
|
||||
realm.Add(skin.SkinInfo.Value);
|
||||
}
|
||||
|
||||
transaction.Commit();
|
||||
}
|
||||
});
|
||||
|
||||
CurrentSkinInfo.ValueChanged += skin => CurrentSkin.Value = skin.NewValue.PerformRead(GetSkin);
|
||||
|
||||
@ -292,10 +289,10 @@ namespace osu.Game.Skinning
|
||||
|
||||
public void Delete([CanBeNull] Expression<Func<SkinInfo, bool>> filter = null, bool silent = false)
|
||||
{
|
||||
using (var context = contextFactory.CreateContext())
|
||||
contextFactory.Run(realm =>
|
||||
{
|
||||
var items = context.All<SkinInfo>()
|
||||
.Where(s => !s.Protected && !s.DeletePending);
|
||||
var items = realm.All<SkinInfo>()
|
||||
.Where(s => !s.Protected && !s.DeletePending);
|
||||
if (filter != null)
|
||||
items = items.Where(filter);
|
||||
|
||||
@ -306,7 +303,7 @@ namespace osu.Game.Skinning
|
||||
scheduler.Add(() => CurrentSkinInfo.Value = Skinning.DefaultSkin.CreateInfo().ToLiveUnmanaged());
|
||||
|
||||
skinModelManager.Delete(items.ToList(), silent);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
@ -205,7 +205,7 @@ namespace osu.Game.Skinning
|
||||
|
||||
private void populateMissingHashes()
|
||||
{
|
||||
using (var realm = ContextFactory.CreateContext())
|
||||
ContextFactory.Run(realm =>
|
||||
{
|
||||
var skinsWithoutHashes = realm.All<SkinInfo>().Where(i => !i.Protected && string.IsNullOrEmpty(i.Hash)).ToArray();
|
||||
|
||||
@ -221,7 +221,7 @@ namespace osu.Game.Skinning
|
||||
Logger.Error(e, $"Existing skin {skin} has been deleted during hash recomputation due to being invalid");
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
private Skin createInstance(SkinInfo item) => item.CreateInstance(skinResources);
|
||||
|
@ -320,7 +320,7 @@ namespace osu.Game.Stores
|
||||
/// <param name="cancellationToken">An optional cancellation token.</param>
|
||||
public virtual Task<ILive<TModel>?> Import(TModel item, ArchiveReader? archive = null, bool lowPriority = false, CancellationToken cancellationToken = default)
|
||||
{
|
||||
using (var realm = ContextFactory.CreateContext())
|
||||
return ContextFactory.Run(realm =>
|
||||
{
|
||||
cancellationToken.ThrowIfCancellationRequested();
|
||||
|
||||
@ -414,7 +414,7 @@ namespace osu.Game.Stores
|
||||
}
|
||||
|
||||
return Task.FromResult((ILive<TModel>?)item.ToLive(ContextFactory));
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
private string computeHashFast(ArchiveReader reader)
|
||||
|
@ -92,8 +92,7 @@ namespace osu.Game.Stores
|
||||
int removedFiles = 0;
|
||||
|
||||
// can potentially be run asynchronously, although we will need to consider operation order for disk deletion vs realm removal.
|
||||
using (var realm = realmFactory.CreateContext())
|
||||
using (var transaction = realm.BeginWrite())
|
||||
realmFactory.Write(realm =>
|
||||
{
|
||||
// TODO: consider using a realm native query to avoid iterating all files (https://github.com/realm/realm-dotnet/issues/2659#issuecomment-927823707)
|
||||
var files = realm.All<RealmFile>().ToList();
|
||||
@ -116,9 +115,7 @@ namespace osu.Game.Stores
|
||||
Logger.Error(e, $@"Could not delete databased file {file.Hash}");
|
||||
}
|
||||
}
|
||||
|
||||
transaction.Commit();
|
||||
}
|
||||
});
|
||||
|
||||
Logger.Log($@"Finished realm file store cleanup ({removedFiles} of {totalFiles} deleted)");
|
||||
}
|
||||
|
Loading…
Reference in New Issue
Block a user