1
0
mirror of https://github.com/ppy/osu.git synced 2025-01-12 21:52:55 +08:00

Merge pull request #3339 from peppy/add-skin-import-delete

Add skin import and mass delete options
This commit is contained in:
Dean Herbert 2018-09-07 18:27:47 +09:00 committed by GitHub
commit 20719439c6
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
8 changed files with 113 additions and 64 deletions

View File

@ -63,6 +63,8 @@ namespace osu.Game.Beatmaps
public override string[] HandledExtensions => new[] { ".osz" }; public override string[] HandledExtensions => new[] { ".osz" };
protected override string ImportFromStablePath => "Songs";
private readonly RulesetStore rulesets; private readonly RulesetStore rulesets;
private readonly BeatmapStore beatmaps; private readonly BeatmapStore beatmaps;
@ -73,11 +75,6 @@ namespace osu.Game.Beatmaps
private readonly List<DownloadBeatmapSetRequest> currentDownloads = new List<DownloadBeatmapSetRequest>(); private readonly List<DownloadBeatmapSetRequest> currentDownloads = new List<DownloadBeatmapSetRequest>();
/// <summary>
/// Set a storage with access to an osu-stable install for import purposes.
/// </summary>
public Func<Storage> GetStableStorage { private get; set; }
public BeatmapManager(Storage storage, IDatabaseContextFactory contextFactory, RulesetStore rulesets, APIAccess api, AudioManager audioManager, IIpcHost importHost = null) public BeatmapManager(Storage storage, IDatabaseContextFactory contextFactory, RulesetStore rulesets, APIAccess api, AudioManager audioManager, IIpcHost importHost = null)
: base(storage, contextFactory, new BeatmapStore(contextFactory), importHost) : base(storage, contextFactory, new BeatmapStore(contextFactory), importHost)
{ {
@ -318,27 +315,6 @@ namespace osu.Game.Beatmaps
/// <returns>Results from the provided query.</returns> /// <returns>Results from the provided query.</returns>
public IQueryable<BeatmapInfo> QueryBeatmaps(Expression<Func<BeatmapInfo, bool>> query) => beatmaps.Beatmaps.AsNoTracking().Where(query); public IQueryable<BeatmapInfo> QueryBeatmaps(Expression<Func<BeatmapInfo, bool>> query) => beatmaps.Beatmaps.AsNoTracking().Where(query);
/// <summary>
/// Denotes whether an osu-stable installation is present to perform automated imports from.
/// </summary>
public bool StableInstallationAvailable => GetStableStorage?.Invoke() != null;
/// <summary>
/// This is a temporary method and will likely be replaced by a full-fledged (and more correctly placed) migration process in the future.
/// </summary>
public Task ImportFromStable()
{
var stable = GetStableStorage?.Invoke();
if (stable == null)
{
Logger.Log("No osu!stable installation available!", LoggingTarget.Information, LogLevel.Error);
return Task.CompletedTask;
}
return Task.Factory.StartNew(() => Import(stable.GetDirectories("Songs").Select(f => stable.GetFullPath(f)).ToArray()), TaskCreationOptions.LongRunning);
}
/// <summary> /// <summary>
/// Create a SHA-2 hash from the provided archive based on contained beatmap (.osu) file content. /// Create a SHA-2 hash from the provided archive based on contained beatmap (.osu) file content.
/// </summary> /// </summary>

View File

@ -5,6 +5,7 @@ using System;
using System.Collections.Generic; using System.Collections.Generic;
using System.IO; using System.IO;
using System.Linq; using System.Linq;
using System.Threading.Tasks;
using JetBrains.Annotations; using JetBrains.Annotations;
using Microsoft.EntityFrameworkCore; using Microsoft.EntityFrameworkCore;
using osu.Framework.IO.File; using osu.Framework.IO.File;
@ -129,7 +130,6 @@ namespace osu.Game.Database
List<TModel> imported = new List<TModel>(); List<TModel> imported = new List<TModel>();
int current = 0; int current = 0;
int errors = 0;
foreach (string path in paths) foreach (string path in paths)
{ {
if (notification.State == ProgressNotificationState.Cancelled) if (notification.State == ProgressNotificationState.Cancelled)
@ -162,18 +162,25 @@ namespace osu.Game.Database
{ {
e = e.InnerException ?? e; e = e.InnerException ?? e;
Logger.Error(e, $@"Could not import ({Path.GetFileName(path)})"); Logger.Error(e, $@"Could not import ({Path.GetFileName(path)})");
errors++;
} }
} }
notification.CompletionText = errors > 0 ? $"Import complete with {errors} errors" : "Import successful!"; if (imported.Count == 0)
notification.CompletionClickAction += () =>
{ {
if (imported.Count > 0) notification.Text = "Import failed!";
PresentCompletedImport(imported); notification.State = ProgressNotificationState.Cancelled;
return true; }
}; else
notification.State = ProgressNotificationState.Completed; {
notification.CompletionText = $"Imported {current} {typeof(TModel).Name.Replace("Info", "").ToLower()}s!";
notification.CompletionClickAction += () =>
{
if (imported.Count > 0)
PresentCompletedImport(imported);
return true;
};
notification.State = ProgressNotificationState.Completed;
}
} }
protected virtual void PresentCompletedImport(IEnumerable<TModel> imported) protected virtual void PresentCompletedImport(IEnumerable<TModel> imported)
@ -293,7 +300,7 @@ namespace osu.Game.Database
var notification = new ProgressNotification var notification = new ProgressNotification
{ {
Progress = 0, Progress = 0,
CompletionText = "Deleted all beatmaps!", CompletionText = $"Deleted all {typeof(TModel).Name.Replace("Info", "").ToLower()}s!",
State = ProgressNotificationState.Active, State = ProgressNotificationState.Active,
}; };
@ -395,6 +402,41 @@ namespace osu.Game.Database
return fileInfos; return fileInfos;
} }
#region osu-stable import
/// <summary>
/// Set a storage with access to an osu-stable install for import purposes.
/// </summary>
public Func<Storage> GetStableStorage { private get; set; }
/// <summary>
/// Denotes whether an osu-stable installation is present to perform automated imports from.
/// </summary>
public bool StableInstallationAvailable => GetStableStorage?.Invoke() != null;
/// <summary>
/// The relative path from osu-stable's data directory to import items from.
/// </summary>
protected virtual string ImportFromStablePath => null;
/// <summary>
/// This is a temporary method and will likely be replaced by a full-fledged (and more correctly placed) migration process in the future.
/// </summary>
public Task ImportFromStableAsync()
{
var stable = GetStableStorage?.Invoke();
if (stable == null)
{
Logger.Log("No osu!stable installation available!", LoggingTarget.Information, LogLevel.Error);
return Task.CompletedTask;
}
return Task.Factory.StartNew(() => Import(stable.GetDirectories(ImportFromStablePath).Select(f => stable.GetFullPath(f)).ToArray()), TaskCreationOptions.LongRunning);
}
#endregion
/// <summary> /// <summary>
/// Create a barebones model from the provided archive. /// Create a barebones model from the provided archive.
/// Actual expensive population should be done in <see cref="Populate"/>; this should just prepare for duplicate checking. /// Actual expensive population should be done in <see cref="Populate"/>; this should just prepare for duplicate checking.

View File

@ -299,11 +299,13 @@ namespace osu.Game
// This prevents the cursor from showing until we have a screen with CursorVisible = true // This prevents the cursor from showing until we have a screen with CursorVisible = true
MenuCursorContainer.CanShowCursor = currentScreen?.CursorVisible ?? false; MenuCursorContainer.CanShowCursor = currentScreen?.CursorVisible ?? false;
// hook up notifications to components. // todo: all archive managers should be able to be looped here.
SkinManager.PostNotification = n => notifications?.Post(n); SkinManager.PostNotification = n => notifications?.Post(n);
BeatmapManager.PostNotification = n => notifications?.Post(n); SkinManager.GetStableStorage = GetStorageForStableInstall;
BeatmapManager.PostNotification = n => notifications?.Post(n);
BeatmapManager.GetStableStorage = GetStorageForStableInstall; BeatmapManager.GetStableStorage = GetStorageForStableInstall;
BeatmapManager.PresentBeatmap = PresentBeatmap; BeatmapManager.PresentBeatmap = PresentBeatmap;
AddRange(new Drawable[] AddRange(new Drawable[]

View File

@ -7,6 +7,7 @@ using osu.Framework.Allocation;
using osu.Framework.Graphics; using osu.Framework.Graphics;
using osu.Game.Beatmaps; using osu.Game.Beatmaps;
using osu.Game.Graphics.UserInterface; using osu.Game.Graphics.UserInterface;
using osu.Game.Skinning;
namespace osu.Game.Overlays.Settings.Sections.Maintenance namespace osu.Game.Overlays.Settings.Sections.Maintenance
{ {
@ -20,7 +21,7 @@ namespace osu.Game.Overlays.Settings.Sections.Maintenance
protected override string Header => "General"; protected override string Header => "General";
[BackgroundDependencyLoader] [BackgroundDependencyLoader]
private void load(BeatmapManager beatmaps, DialogOverlay dialogOverlay) private void load(BeatmapManager beatmaps, SkinManager skins, DialogOverlay dialogOverlay)
{ {
Children = new Drawable[] Children = new Drawable[]
{ {
@ -30,7 +31,7 @@ namespace osu.Game.Overlays.Settings.Sections.Maintenance
Action = () => Action = () =>
{ {
importButton.Enabled.Value = false; importButton.Enabled.Value = false;
beatmaps.ImportFromStable().ContinueWith(t => Schedule(() => importButton.Enabled.Value = true)); beatmaps.ImportFromStableAsync().ContinueWith(t => Schedule(() => importButton.Enabled.Value = true));
} }
}, },
deleteButton = new DangerousSettingsButton deleteButton = new DangerousSettingsButton
@ -45,6 +46,27 @@ namespace osu.Game.Overlays.Settings.Sections.Maintenance
})); }));
} }
}, },
importButton = new SettingsButton
{
Text = "Import skins from stable",
Action = () =>
{
importButton.Enabled.Value = false;
skins.ImportFromStableAsync().ContinueWith(t => Schedule(() => importButton.Enabled.Value = true));
}
},
deleteButton = new DangerousSettingsButton
{
Text = "Delete ALL skins",
Action = () =>
{
dialogOverlay?.Push(new DeleteAllBeatmapsDialog(() =>
{
deleteButton.Enabled.Value = false;
Task.Run(() => skins.Delete(skins.GetAllUserSkins())).ContinueWith(t => Schedule(() => deleteButton.Enabled.Value = true));
}));
}
},
restoreButton = new SettingsButton restoreButton = new SettingsButton
{ {
Text = "Restore all hidden difficulties", Text = "Restore all hidden difficulties",

View File

@ -51,10 +51,10 @@ namespace osu.Game.Overlays.Settings.Sections
}, },
}; };
skins.ItemAdded += onItemsChanged; skins.ItemAdded += itemAdded;
skins.ItemRemoved += onItemsChanged; skins.ItemRemoved += itemRemoved;
reloadSkins(); skinDropdown.Items = skins.GetAllUsableSkins().Select(s => new KeyValuePair<string, int>(s.ToString(), s.ID));
var skinBindable = config.GetBindable<int>(OsuSetting.Skin); var skinBindable = config.GetBindable<int>(OsuSetting.Skin);
@ -65,9 +65,8 @@ namespace osu.Game.Overlays.Settings.Sections
skinDropdown.Bindable = skinBindable; skinDropdown.Bindable = skinBindable;
} }
private void reloadSkins() => skinDropdown.Items = skins.GetAllUsableSkins().Select(s => new KeyValuePair<string, int>(s.ToString(), s.ID)); private void itemRemoved(SkinInfo s) => skinDropdown.Items = skinDropdown.Items.Where(i => i.Value != s.ID);
private void itemAdded(SkinInfo s) => skinDropdown.Items = skinDropdown.Items.Append(new KeyValuePair<string, int>(s.ToString(), s.ID));
private void onItemsChanged(SkinInfo _) => Schedule(reloadSkins);
protected override void Dispose(bool isDisposing) protected override void Dispose(bool isDisposing)
{ {
@ -75,8 +74,8 @@ namespace osu.Game.Overlays.Settings.Sections
if (skins != null) if (skins != null)
{ {
skins.ItemAdded -= onItemsChanged; skins.ItemAdded -= itemAdded;
skins.ItemRemoved -= onItemsChanged; skins.ItemRemoved -= itemRemoved;
} }
} }

View File

@ -12,7 +12,7 @@ namespace osu.Game.Screens.Select
public ImportFromStablePopup(Action importFromStable) public ImportFromStablePopup(Action importFromStable)
{ {
HeaderText = @"You have no beatmaps!"; HeaderText = @"You have no beatmaps!";
BodyText = "An existing copy of osu! was found, though.\nWould you like to import your beatmaps?"; BodyText = "An existing copy of osu! was found, though.\nWould you like to import your beatmaps (and skins)?";
Icon = FontAwesome.fa_plane; Icon = FontAwesome.fa_plane;

View File

@ -3,7 +3,6 @@
using System.Collections.Generic; using System.Collections.Generic;
using System.Linq; using System.Linq;
using System.Threading.Tasks;
using OpenTK.Input; using OpenTK.Input;
using osu.Framework.Allocation; using osu.Framework.Allocation;
using osu.Framework.Audio; using osu.Framework.Audio;
@ -20,6 +19,7 @@ using osu.Game.Rulesets.Mods;
using osu.Game.Screens.Edit; using osu.Game.Screens.Edit;
using osu.Game.Screens.Play; using osu.Game.Screens.Play;
using osu.Game.Screens.Ranking; using osu.Game.Screens.Ranking;
using osu.Game.Skinning;
namespace osu.Game.Screens.Select namespace osu.Game.Screens.Select
{ {
@ -55,7 +55,7 @@ namespace osu.Game.Screens.Select
private readonly Bindable<IEnumerable<Mod>> selectedMods = new Bindable<IEnumerable<Mod>>(new Mod[] { }); private readonly Bindable<IEnumerable<Mod>> selectedMods = new Bindable<IEnumerable<Mod>>(new Mod[] { });
[BackgroundDependencyLoader(true)] [BackgroundDependencyLoader(true)]
private void load(OsuColour colours, AudioManager audio, BeatmapManager beatmaps, DialogOverlay dialogOverlay, Bindable<IEnumerable<Mod>> selectedMods) private void load(OsuColour colours, AudioManager audio, BeatmapManager beatmaps, SkinManager skins, DialogOverlay dialogOverlay, Bindable<IEnumerable<Mod>> selectedMods)
{ {
if (selectedMods != null) this.selectedMods.BindTo(selectedMods); if (selectedMods != null) this.selectedMods.BindTo(selectedMods);
@ -78,7 +78,10 @@ namespace osu.Game.Screens.Select
// if we have no beatmaps but osu-stable is found, let's prompt the user to import. // if we have no beatmaps but osu-stable is found, let's prompt the user to import.
if (!beatmaps.GetAllUsableBeatmapSets().Any() && beatmaps.StableInstallationAvailable) if (!beatmaps.GetAllUsableBeatmapSets().Any() && beatmaps.StableInstallationAvailable)
dialogOverlay.Push(new ImportFromStablePopup(() => dialogOverlay.Push(new ImportFromStablePopup(() =>
Task.Factory.StartNew(beatmaps.ImportFromStable, TaskCreationOptions.LongRunning))); {
beatmaps.ImportFromStableAsync();
skins.ImportFromStableAsync();
}));
}); });
} }
} }

View File

@ -26,17 +26,25 @@ namespace osu.Game.Skinning
public override string[] HandledExtensions => new[] { ".osk" }; public override string[] HandledExtensions => new[] { ".osk" };
protected override string ImportFromStablePath => "Skins";
/// <summary> /// <summary>
/// Returns a list of all usable <see cref="SkinInfo"/>s. /// Returns a list of all usable <see cref="SkinInfo"/>s. Includes the special default skin plus all skins from <see cref="GetAllUserSkins"/>.
/// </summary> /// </summary>
/// <returns>A list of available <see cref="SkinInfo"/>.</returns> /// <returns>A list of available <see cref="SkinInfo"/>.</returns>
public List<SkinInfo> GetAllUsableSkins() public List<SkinInfo> GetAllUsableSkins()
{ {
var userSkins = ModelStore.ConsumableItems.Where(s => !s.DeletePending).ToList(); var userSkins = GetAllUserSkins();
userSkins.Insert(0, SkinInfo.Default); userSkins.Insert(0, SkinInfo.Default);
return userSkins; return userSkins;
} }
/// <summary>
/// Returns a list of all usable <see cref="SkinInfo"/>s that have been loaded by the user.
/// </summary>
/// <returns>A list of available <see cref="SkinInfo"/>.</returns>
public List<SkinInfo> GetAllUserSkins() => ModelStore.ConsumableItems.Where(s => !s.DeletePending).ToList();
protected override SkinInfo CreateModel(ArchiveReader archive) => new SkinInfo protected override SkinInfo CreateModel(ArchiveReader archive) => new SkinInfo
{ {
Name = archive.Name Name = archive.Name
@ -85,6 +93,13 @@ namespace osu.Game.Skinning
{ {
this.audio = audio; this.audio = audio;
ItemRemoved += removedInfo =>
{
// check the removed skin is not the current user choice. if it is, switch back to default.
if (removedInfo.ID == CurrentSkinInfo.Value.ID)
CurrentSkinInfo.Value = SkinInfo.Default;
};
CurrentSkinInfo.ValueChanged += info => CurrentSkin.Value = GetSkin(info); CurrentSkinInfo.ValueChanged += info => CurrentSkin.Value = GetSkin(info);
CurrentSkin.ValueChanged += skin => CurrentSkin.ValueChanged += skin =>
{ {
@ -93,16 +108,6 @@ namespace osu.Game.Skinning
SourceChanged?.Invoke(); SourceChanged?.Invoke();
}; };
// migrate older imports which didn't have access to skin.ini
using (ContextFactory.GetForWrite())
{
foreach (var skinInfo in ModelStore.ConsumableItems.Where(s => s.Name.EndsWith(".osk")))
{
populate(skinInfo);
Update(skinInfo);
}
}
} }
/// <summary> /// <summary>