Merge pull request #17596 from peppy/skin-editor-sprites

Allow importing sprites into a skin via drag-and-drop
This commit is contained in:
Dan Balasescu 2022-04-05 17:38:58 +09:00 committed by GitHub
commit 2b59f76b2d
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
11 changed files with 289 additions and 38 deletions

View File

@ -88,6 +88,7 @@ namespace osu.Game.Configuration
throw new InvalidOperationException($"{nameof(SettingSourceAttribute)} had an unsupported custom control type ({controlType.ReadableName()})"); throw new InvalidOperationException($"{nameof(SettingSourceAttribute)} had an unsupported custom control type ({controlType.ReadableName()})");
var control = (Drawable)Activator.CreateInstance(controlType); var control = (Drawable)Activator.CreateInstance(controlType);
controlType.GetProperty(nameof(SettingsItem<object>.SettingSourceObject))?.SetValue(control, obj);
controlType.GetProperty(nameof(SettingsItem<object>.LabelText))?.SetValue(control, attr.Label); controlType.GetProperty(nameof(SettingsItem<object>.LabelText))?.SetValue(control, attr.Label);
controlType.GetProperty(nameof(SettingsItem<object>.TooltipText))?.SetValue(control, attr.Description); controlType.GetProperty(nameof(SettingsItem<object>.TooltipText))?.SetValue(control, attr.Description);
controlType.GetProperty(nameof(SettingsItem<object>.Current))?.SetValue(control, value); controlType.GetProperty(nameof(SettingsItem<object>.Current))?.SetValue(control, value);

View File

@ -11,6 +11,7 @@ using osu.Framework.Graphics.Cursor;
using osu.Framework.Graphics.Sprites; using osu.Framework.Graphics.Sprites;
using osu.Framework.Graphics.UserInterface; using osu.Framework.Graphics.UserInterface;
using osu.Framework.Localisation; using osu.Framework.Localisation;
using osu.Game.Configuration;
using osu.Game.Graphics; using osu.Game.Graphics;
using osu.Game.Graphics.Sprites; using osu.Game.Graphics.Sprites;
using osu.Game.Graphics.Containers; using osu.Game.Graphics.Containers;
@ -24,6 +25,11 @@ namespace osu.Game.Overlays.Settings
protected Drawable Control { get; } protected Drawable Control { get; }
/// <summary>
/// The source component if this <see cref="SettingsItem{T}"/> was created via <see cref="SettingSourceAttribute"/>.
/// </summary>
public object SettingSourceObject { get; internal set; }
private IHasCurrentValue<T> controlWithCurrent => Control as IHasCurrentValue<T>; private IHasCurrentValue<T> controlWithCurrent => Control as IHasCurrentValue<T>;
protected override Container<Drawable> Content => FlowContent; protected override Container<Drawable> Content => FlowContent;

View File

@ -8,6 +8,7 @@ using osu.Framework.Audio.Sample;
using osu.Framework.Bindables; using osu.Framework.Bindables;
using osu.Framework.Graphics; using osu.Framework.Graphics;
using osu.Framework.Graphics.OpenGL.Textures; using osu.Framework.Graphics.OpenGL.Textures;
using osu.Framework.Graphics.Sprites;
using osu.Framework.Graphics.Textures; using osu.Framework.Graphics.Textures;
using osu.Game.Audio; using osu.Game.Audio;
using osu.Game.Beatmaps.Formats; using osu.Game.Beatmaps.Formats;
@ -46,13 +47,13 @@ namespace osu.Game.Skinning
this.resources = resources; this.resources = resources;
} }
public override Texture GetTexture(string componentName, WrapMode wrapModeS, WrapMode wrapModeT) => null; public override Texture GetTexture(string componentName, WrapMode wrapModeS, WrapMode wrapModeT) => Textures?.Get(componentName, wrapModeS, wrapModeT);
public override ISample GetSample(ISampleInfo sampleInfo) public override ISample GetSample(ISampleInfo sampleInfo)
{ {
foreach (string lookup in sampleInfo.LookupNames) foreach (string lookup in sampleInfo.LookupNames)
{ {
var sample = resources.AudioManager.Samples.Get(lookup); var sample = Samples?.Get(lookup) ?? resources.AudioManager.Samples.Get(lookup);
if (sample != null) if (sample != null)
return sample; return sample;
} }
@ -157,6 +158,16 @@ namespace osu.Game.Skinning
break; break;
} }
switch (component.LookupName)
{
// Temporary until default skin has a valid hit lighting.
case @"lighting":
return Drawable.Empty();
}
if (GetTexture(component.LookupName) is Texture t)
return new Sprite { Texture = t };
return null; return null;
} }

View File

@ -3,7 +3,9 @@
using System; using System;
using System.Collections.Generic; using System.Collections.Generic;
using System.IO;
using System.Linq; using System.Linq;
using System.Threading.Tasks;
using osu.Framework.Allocation; using osu.Framework.Allocation;
using osu.Framework.Bindables; using osu.Framework.Bindables;
using osu.Framework.Graphics; using osu.Framework.Graphics;
@ -11,6 +13,7 @@ using osu.Framework.Graphics.Containers;
using osu.Framework.Graphics.UserInterface; using osu.Framework.Graphics.UserInterface;
using osu.Framework.Input.Events; using osu.Framework.Input.Events;
using osu.Framework.Testing; using osu.Framework.Testing;
using osu.Game.Database;
using osu.Game.Graphics; using osu.Game.Graphics;
using osu.Game.Graphics.Containers; using osu.Game.Graphics.Containers;
using osu.Game.Graphics.Cursor; using osu.Game.Graphics.Cursor;
@ -22,7 +25,7 @@ using osu.Game.Screens.Edit.Components.Menus;
namespace osu.Game.Skinning.Editor namespace osu.Game.Skinning.Editor
{ {
[Cached(typeof(SkinEditor))] [Cached(typeof(SkinEditor))]
public class SkinEditor : VisibilityContainer public class SkinEditor : VisibilityContainer, ICanAcceptFiles
{ {
public const double TRANSITION_DURATION = 500; public const double TRANSITION_DURATION = 500;
@ -36,12 +39,18 @@ namespace osu.Game.Skinning.Editor
private Bindable<Skin> currentSkin; private Bindable<Skin> currentSkin;
[Resolved(canBeNull: true)]
private OsuGame game { get; set; }
[Resolved] [Resolved]
private SkinManager skins { get; set; } private SkinManager skins { get; set; }
[Resolved] [Resolved]
private OsuColour colours { get; set; } private OsuColour colours { get; set; }
[Resolved]
private RealmAccess realm { get; set; }
[Resolved(canBeNull: true)] [Resolved(canBeNull: true)]
private SkinEditorOverlay skinEditorOverlay { get; set; } private SkinEditorOverlay skinEditorOverlay { get; set; }
@ -171,6 +180,8 @@ namespace osu.Game.Skinning.Editor
Show(); Show();
game?.RegisterImportHandler(this);
// as long as the skin editor is loaded, let's make sure we can modify the current skin. // as long as the skin editor is loaded, let's make sure we can modify the current skin.
currentSkin = skins.CurrentSkin.GetBoundCopy(); currentSkin = skins.CurrentSkin.GetBoundCopy();
@ -229,15 +240,20 @@ namespace osu.Game.Skinning.Editor
} }
private void placeComponent(Type type) private void placeComponent(Type type)
{
if (!(Activator.CreateInstance(type) is ISkinnableDrawable component))
throw new InvalidOperationException($"Attempted to instantiate a component for placement which was not an {typeof(ISkinnableDrawable)}.");
placeComponent(component);
}
private void placeComponent(ISkinnableDrawable component)
{ {
var targetContainer = getFirstTarget(); var targetContainer = getFirstTarget();
if (targetContainer == null) if (targetContainer == null)
return; return;
if (!(Activator.CreateInstance(type) is ISkinnableDrawable component))
throw new InvalidOperationException($"Attempted to instantiate a component for placement which was not an {typeof(ISkinnableDrawable)}.");
var drawableComponent = (Drawable)component; var drawableComponent = (Drawable)component;
// give newly added components a sane starting location. // give newly added components a sane starting location.
@ -313,5 +329,48 @@ namespace osu.Game.Skinning.Editor
foreach (var item in items) foreach (var item in items)
availableTargets.FirstOrDefault(t => t.Components.Contains(item))?.Remove(item); availableTargets.FirstOrDefault(t => t.Components.Contains(item))?.Remove(item);
} }
#region Drag & drop import handling
public Task Import(params string[] paths)
{
Schedule(() =>
{
var file = new FileInfo(paths.First());
// import to skin
currentSkin.Value.SkinInfo.PerformWrite(skinInfo =>
{
using (var contents = file.OpenRead())
skins.AddFile(skinInfo, contents, file.Name);
});
// Even though we are 100% on an update thread, we need to wait for realm callbacks to fire (to correctly invalidate caches in RealmBackedResourceStore).
// See https://github.com/realm/realm-dotnet/discussions/2634#discussioncomment-2483573 for further discussion.
// This is the best we can do for now.
realm.Run(r => r.Refresh());
// place component
placeComponent(new SkinnableSprite
{
SpriteName = { Value = file.Name }
});
});
return Task.CompletedTask;
}
public Task Import(params ImportTask[] tasks) => throw new NotImplementedException();
public IEnumerable<string> HandledExtensions => new[] { ".jpg", ".jpeg", ".png" };
#endregion
protected override void Dispose(bool isDisposing)
{
base.Dispose(isDisposing);
game?.UnregisterImportHandler(this);
}
} }
} }

View File

@ -11,6 +11,7 @@ using osu.Framework.IO.Stores;
using osu.Game.Audio; using osu.Game.Audio;
using osu.Game.Beatmaps; using osu.Game.Beatmaps;
using osu.Game.Beatmaps.Formats; using osu.Game.Beatmaps.Formats;
using osu.Game.Database;
using osu.Game.IO; using osu.Game.IO;
using osu.Game.Rulesets.Objects.Legacy; using osu.Game.Rulesets.Objects.Legacy;
using osu.Game.Rulesets.Objects.Types; using osu.Game.Rulesets.Objects.Types;
@ -37,11 +38,11 @@ namespace osu.Game.Skinning
private static IResourceStore<byte[]> createRealmBackedStore(BeatmapInfo beatmapInfo, IStorageResourceProvider? resources) private static IResourceStore<byte[]> createRealmBackedStore(BeatmapInfo beatmapInfo, IStorageResourceProvider? resources)
{ {
if (resources == null) if (resources == null || beatmapInfo.BeatmapSet == null)
// should only ever be used in tests. // should only ever be used in tests.
return new ResourceStore<byte[]>(); return new ResourceStore<byte[]>();
return new RealmBackedResourceStore(beatmapInfo.BeatmapSet, resources.Files, new[] { @"ogg" }); return new RealmBackedResourceStore<BeatmapSetInfo>(beatmapInfo.BeatmapSet.ToLive(resources.RealmAccess), resources.Files, resources.RealmAccess);
} }
public override Drawable? GetDrawableComponent(ISkinComponent component) public override Drawable? GetDrawableComponent(ISkinComponent component)

View File

@ -23,7 +23,7 @@ namespace osu.Game.Skinning
/// The <see cref="ISkin"/> which is being transformed. /// The <see cref="ISkin"/> which is being transformed.
/// </summary> /// </summary>
[NotNull] [NotNull]
protected ISkin Skin { get; } protected internal ISkin Skin { get; }
protected LegacySkinTransformer([NotNull] ISkin skin) protected LegacySkinTransformer([NotNull] ISkin skin)
{ {

View File

@ -1,51 +1,77 @@
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence. // 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. // See the LICENCE file in the repository root for full licence text.
#nullable enable
using System;
using System.Collections.Generic; using System.Collections.Generic;
using System.Diagnostics;
using System.Linq;
using osu.Framework.Extensions; using osu.Framework.Extensions;
using osu.Framework.IO.Stores; using osu.Framework.IO.Stores;
using osu.Game.Database; using osu.Game.Database;
using osu.Game.Extensions; using osu.Game.Extensions;
using Realms;
namespace osu.Game.Skinning namespace osu.Game.Skinning
{ {
public class RealmBackedResourceStore : ResourceStore<byte[]> public class RealmBackedResourceStore<T> : ResourceStore<byte[]>
where T : RealmObject, IHasRealmFiles, IHasGuidPrimaryKey
{ {
private readonly Dictionary<string, string> fileToStoragePathMapping = new Dictionary<string, string>(); private Lazy<Dictionary<string, string>> fileToStoragePathMapping;
public RealmBackedResourceStore(IHasRealmFiles source, IResourceStore<byte[]> underlyingStore, string[] extensions = null) private readonly Live<T> liveSource;
private readonly IDisposable? realmSubscription;
public RealmBackedResourceStore(Live<T> source, IResourceStore<byte[]> underlyingStore, RealmAccess? realm)
: base(underlyingStore) : base(underlyingStore)
{ {
// Must be initialised before the file cache. liveSource = source;
if (extensions != null)
{ invalidateCache();
foreach (string extension in extensions) Debug.Assert(fileToStoragePathMapping != null);
AddExtension(extension);
realmSubscription = realm?.RegisterForNotifications(r => r.All<T>().Where(s => s.ID == source.ID), skinChanged);
} }
initialiseFileCache(source); protected override void Dispose(bool disposing)
{
base.Dispose(disposing);
realmSubscription?.Dispose();
} }
private void initialiseFileCache(IHasRealmFiles source) private void skinChanged(IRealmCollection<T> sender, ChangeSet changes, Exception error) => invalidateCache();
{
fileToStoragePathMapping.Clear();
foreach (var f in source.Files)
fileToStoragePathMapping[f.Filename.ToLowerInvariant()] = f.File.GetStoragePath();
}
protected override IEnumerable<string> GetFilenames(string name) protected override IEnumerable<string> GetFilenames(string name)
{ {
foreach (string filename in base.GetFilenames(name)) foreach (string filename in base.GetFilenames(name))
{ {
string path = getPathForFile(filename.ToStandardisedPath()); string? path = getPathForFile(filename.ToStandardisedPath());
if (path != null) if (path != null)
yield return path; yield return path;
} }
} }
private string getPathForFile(string filename) => private string? getPathForFile(string filename)
fileToStoragePathMapping.TryGetValue(filename.ToLower(), out string path) ? path : null; {
if (fileToStoragePathMapping.Value.TryGetValue(filename.ToLowerInvariant(), out string path))
return path;
public override IEnumerable<string> GetAvailableResources() => fileToStoragePathMapping.Keys; return null;
}
private void invalidateCache() => fileToStoragePathMapping = new Lazy<Dictionary<string, string>>(initialiseFileCache);
private Dictionary<string, string> initialiseFileCache() => liveSource.PerformRead(source =>
{
var dictionary = new Dictionary<string, string>();
dictionary.Clear();
foreach (var f in source.Files)
dictionary[f.Filename.ToLowerInvariant()] = f.File.GetStoragePath();
return dictionary;
});
public override IEnumerable<string> GetAvailableResources() => fileToStoragePathMapping.Value.Keys;
} }
} }

View File

@ -54,6 +54,8 @@ namespace osu.Game.Skinning
where TLookup : notnull where TLookup : notnull
where TValue : notnull; where TValue : notnull;
private readonly RealmBackedResourceStore<SkinInfo>? realmBackedStorage;
/// <summary> /// <summary>
/// Construct a new skin. /// Construct a new skin.
/// </summary> /// </summary>
@ -67,7 +69,9 @@ namespace osu.Game.Skinning
{ {
SkinInfo = skin.ToLive(resources.RealmAccess); SkinInfo = skin.ToLive(resources.RealmAccess);
storage ??= new RealmBackedResourceStore(skin, resources.Files, new[] { @"ogg" }); storage ??= realmBackedStorage = new RealmBackedResourceStore<SkinInfo>(SkinInfo, resources.Files, resources.RealmAccess);
(storage as ResourceStore<byte[]>)?.AddExtension("ogg");
var samples = resources.AudioManager?.GetSampleStore(storage); var samples = resources.AudioManager?.GetSampleStore(storage);
if (samples != null) if (samples != null)
@ -191,6 +195,8 @@ namespace osu.Game.Skinning
Textures?.Dispose(); Textures?.Dispose();
Samples?.Dispose(); Samples?.Dispose();
realmBackedStorage?.Dispose();
} }
#endregion #endregion

View File

@ -3,6 +3,7 @@
using System; using System;
using System.Collections.Generic; using System.Collections.Generic;
using System.IO;
using System.Linq; using System.Linq;
using System.Linq.Expressions; using System.Linq.Expressions;
using System.Threading; using System.Threading;
@ -23,6 +24,7 @@ using osu.Game.Audio;
using osu.Game.Database; using osu.Game.Database;
using osu.Game.IO; using osu.Game.IO;
using osu.Game.IO.Archives; using osu.Game.IO.Archives;
using osu.Game.Models;
using osu.Game.Overlays.Notifications; using osu.Game.Overlays.Notifications;
using osu.Game.Utils; using osu.Game.Utils;
@ -36,7 +38,7 @@ namespace osu.Game.Skinning
/// For gameplay components, see <see cref="RulesetSkinProvidingContainer"/> which adds extra legacy and toggle logic that may affect the lookup process. /// For gameplay components, see <see cref="RulesetSkinProvidingContainer"/> which adds extra legacy and toggle logic that may affect the lookup process.
/// </remarks> /// </remarks>
[ExcludeFromDynamicCompile] [ExcludeFromDynamicCompile]
public class SkinManager : ISkinSource, IStorageResourceProvider, IModelImporter<SkinInfo> public class SkinManager : ISkinSource, IStorageResourceProvider, IModelImporter<SkinInfo>, IModelManager<SkinInfo>, IModelFileManager<SkinInfo, RealmNamedFileUsage>
{ {
private readonly AudioManager audio; private readonly AudioManager audio;
@ -96,7 +98,10 @@ namespace osu.Game.Skinning
} }
}); });
CurrentSkinInfo.ValueChanged += skin => CurrentSkin.Value = skin.NewValue.PerformRead(GetSkin); CurrentSkinInfo.ValueChanged += skin =>
{
CurrentSkin.Value = skin.NewValue.PerformRead(GetSkin);
};
CurrentSkin.Value = DefaultSkin; CurrentSkin.Value = DefaultSkin;
CurrentSkin.ValueChanged += skin => CurrentSkin.ValueChanged += skin =>
@ -313,5 +318,45 @@ namespace osu.Game.Skinning
} }
#endregion #endregion
public bool Delete(SkinInfo item)
{
return skinModelManager.Delete(item);
}
public void Delete(List<SkinInfo> items, bool silent = false)
{
skinModelManager.Delete(items, silent);
}
public void Undelete(List<SkinInfo> items, bool silent = false)
{
skinModelManager.Undelete(items, silent);
}
public void Undelete(SkinInfo item)
{
skinModelManager.Undelete(item);
}
public bool IsAvailableLocally(SkinInfo model)
{
return skinModelManager.IsAvailableLocally(model);
}
public void ReplaceFile(SkinInfo model, RealmNamedFileUsage file, Stream contents)
{
skinModelManager.ReplaceFile(model, file, contents);
}
public void DeleteFile(SkinInfo model, RealmNamedFileUsage file)
{
skinModelManager.DeleteFile(model, file);
}
public void AddFile(SkinInfo model, Stream contents, string filename)
{
skinModelManager.AddFile(model, contents, filename);
}
} }
} }

View File

@ -31,7 +31,7 @@ namespace osu.Game.Skinning
set => base.AutoSizeAxes = value; set => base.AutoSizeAxes = value;
} }
private readonly ISkinComponent component; protected readonly ISkinComponent Component;
private readonly ConfineMode confineMode; private readonly ConfineMode confineMode;
@ -49,7 +49,7 @@ namespace osu.Game.Skinning
protected SkinnableDrawable(ISkinComponent component, ConfineMode confineMode = ConfineMode.NoScaling) protected SkinnableDrawable(ISkinComponent component, ConfineMode confineMode = ConfineMode.NoScaling)
{ {
this.component = component; Component = component;
this.confineMode = confineMode; this.confineMode = confineMode;
RelativeSizeAxes = Axes.Both; RelativeSizeAxes = Axes.Both;
@ -75,13 +75,13 @@ namespace osu.Game.Skinning
protected override void SkinChanged(ISkinSource skin) protected override void SkinChanged(ISkinSource skin)
{ {
Drawable = skin.GetDrawableComponent(component); Drawable = skin.GetDrawableComponent(Component);
isDefault = false; isDefault = false;
if (Drawable == null) if (Drawable == null)
{ {
Drawable = CreateDefault(component); Drawable = CreateDefault(Component);
isDefault = true; isDefault = true;
} }

View File

@ -1,26 +1,56 @@
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence. // 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. // See the LICENCE file in the repository root for full licence text.
using System;
using System.Collections.Generic;
using System.Linq;
using osu.Framework.Allocation; using osu.Framework.Allocation;
using osu.Framework.Bindables;
using osu.Framework.Graphics; using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers;
using osu.Framework.Graphics.Sprites; using osu.Framework.Graphics.Sprites;
using osu.Framework.Graphics.Textures; using osu.Framework.Graphics.Textures;
using osu.Game.Configuration;
using osu.Game.Graphics.Sprites;
using osu.Game.Overlays.Settings;
using osuTK;
namespace osu.Game.Skinning namespace osu.Game.Skinning
{ {
/// <summary> /// <summary>
/// A skinnable element which uses a stable sprite and can therefore share implementation logic. /// A skinnable element which uses a single texture backing.
/// </summary> /// </summary>
public class SkinnableSprite : SkinnableDrawable public class SkinnableSprite : SkinnableDrawable, ISkinnableDrawable
{ {
protected override bool ApplySizeRestrictionsToDefault => true; protected override bool ApplySizeRestrictionsToDefault => true;
[Resolved] [Resolved]
private TextureStore textures { get; set; } private TextureStore textures { get; set; }
[SettingSource("Sprite name", "The filename of the sprite", SettingControlType = typeof(SpriteSelectorControl))]
public Bindable<string> SpriteName { get; } = new Bindable<string>(string.Empty);
[Resolved]
private ISkinSource source { get; set; }
public SkinnableSprite(string textureName, ConfineMode confineMode = ConfineMode.NoScaling) public SkinnableSprite(string textureName, ConfineMode confineMode = ConfineMode.NoScaling)
: base(new SpriteComponent(textureName), confineMode) : base(new SpriteComponent(textureName), confineMode)
{ {
SpriteName.Value = textureName;
}
public SkinnableSprite()
: base(new SpriteComponent(string.Empty), ConfineMode.NoScaling)
{
RelativeSizeAxes = Axes.None;
AutoSizeAxes = Axes.Both;
SpriteName.BindValueChanged(name =>
{
((SpriteComponent)Component).LookupName = name.NewValue ?? string.Empty;
if (IsLoaded)
SkinChanged(CurrentSkin);
});
} }
protected override Drawable CreateDefault(ISkinComponent component) protected override Drawable CreateDefault(ISkinComponent component)
@ -28,19 +58,85 @@ namespace osu.Game.Skinning
var texture = textures.Get(component.LookupName); var texture = textures.Get(component.LookupName);
if (texture == null) if (texture == null)
return null; return new SpriteNotFound(component.LookupName);
return new Sprite { Texture = texture }; return new Sprite { Texture = texture };
} }
public bool UsesFixedAnchor { get; set; }
private class SpriteComponent : ISkinComponent private class SpriteComponent : ISkinComponent
{ {
public string LookupName { get; set; }
public SpriteComponent(string textureName) public SpriteComponent(string textureName)
{ {
LookupName = textureName; LookupName = textureName;
} }
}
public string LookupName { get; } public class SpriteSelectorControl : SettingsDropdown<string>
{
protected override void LoadComplete()
{
base.LoadComplete();
// Round-about way of getting the user's skin to find available resources.
// In the future we'll probably want to allow access to resources from the fallbacks, or potentially other skins
// but that requires further thought.
var highestPrioritySkin = getHighestPriorityUserSkin(((SkinnableSprite)SettingSourceObject).source.AllSources) as Skin;
string[] availableFiles = highestPrioritySkin?.SkinInfo.PerformRead(s => s.Files
.Where(f => f.Filename.EndsWith(".png", StringComparison.Ordinal)
|| f.Filename.EndsWith(".jpg", StringComparison.Ordinal))
.Select(f => f.Filename).Distinct()).ToArray();
if (availableFiles?.Length > 0)
Items = availableFiles;
static ISkin getHighestPriorityUserSkin(IEnumerable<ISkin> skins)
{
foreach (var skin in skins)
{
if (skin is LegacySkinTransformer transformer && isUserSkin(transformer.Skin))
return transformer.Skin;
if (isUserSkin(skin))
return skin;
}
return null;
}
// Temporarily used to exclude undesirable ISkin implementations
static bool isUserSkin(ISkin skin)
=> skin.GetType() == typeof(DefaultSkin)
|| skin.GetType() == typeof(DefaultLegacySkin)
|| skin.GetType() == typeof(LegacySkin);
}
}
public class SpriteNotFound : CompositeDrawable
{
public SpriteNotFound(string lookup)
{
AutoSizeAxes = Axes.Both;
InternalChildren = new Drawable[]
{
new SpriteIcon
{
Size = new Vector2(50),
Icon = FontAwesome.Solid.QuestionCircle
},
new OsuSpriteText
{
Position = new Vector2(25, 50),
Text = $"missing: {lookup}",
Origin = Anchor.TopCentre,
}
};
}
} }
} }
} }