From ccfc05f2b28e89478283bfc23e185b480b96e066 Mon Sep 17 00:00:00 2001 From: Ottermandias Date: Wed, 19 Oct 2022 01:01:40 +0200 Subject: [PATCH] Add local data, favorites and tags. --- OtterGui | 2 +- Penumbra/Mods/Manager/Mod.Manager.BasePath.cs | 16 +- Penumbra/Mods/Manager/Mod.Manager.Local.cs | 75 ++++++++ Penumbra/Mods/Manager/Mod.Manager.Meta.cs | 17 +- Penumbra/Mods/Mod.BasePath.cs | 10 +- Penumbra/Mods/Mod.LocalData.cs | 167 ++++++++++++++++++ Penumbra/Mods/Mod.Meta.Migration.cs | 21 ++- Penumbra/Mods/Mod.Meta.cs | 63 ++++--- Penumbra/Mods/ModFileSystem.cs | 8 +- Penumbra/Penumbra.cs | 1 + .../Classes/ModFileSystemSelector.Filters.cs | 15 +- Penumbra/UI/Classes/ModFileSystemSelector.cs | 15 +- Penumbra/UI/Classes/ModFilter.cs | 38 ++-- Penumbra/UI/ConfigWindow.Changelog.cs | 7 + Penumbra/UI/ConfigWindow.ModPanel.Edit.cs | 11 +- Penumbra/UI/ConfigWindow.ModPanel.Settings.cs | 20 ++- Penumbra/UI/ConfigWindow.ModPanel.Tabs.cs | 76 +++++++- Penumbra/UI/ConfigWindow.ModsTab.cs | 9 +- Penumbra/UI/ConfigWindow.Tutorial.cs | 4 + 19 files changed, 481 insertions(+), 94 deletions(-) create mode 100644 Penumbra/Mods/Manager/Mod.Manager.Local.cs create mode 100644 Penumbra/Mods/Mod.LocalData.cs diff --git a/OtterGui b/OtterGui index 1e0d04b9..0d2284a8 160000 --- a/OtterGui +++ b/OtterGui @@ -1 +1 @@ -Subproject commit 1e0d04b90043faad979c3e7316a733870eb16108 +Subproject commit 0d2284a82504aac0bff797fa3355f750a3e68834 diff --git a/Penumbra/Mods/Manager/Mod.Manager.BasePath.cs b/Penumbra/Mods/Manager/Mod.Manager.BasePath.cs index 99ab7aaa..9902ed57 100644 --- a/Penumbra/Mods/Manager/Mod.Manager.BasePath.cs +++ b/Penumbra/Mods/Manager/Mod.Manager.BasePath.cs @@ -58,6 +58,7 @@ public partial class Mod return; } + MoveDataFile( oldDirectory, BasePath ); new ModBackup( mod ).Move( null, dir.Name ); dir.Refresh(); @@ -69,9 +70,9 @@ public partial class Mod } ModPathChanged.Invoke( ModPathChangeType.Moved, mod, oldDirectory, BasePath ); - if( metaChange != MetaChangeType.None ) + if( metaChange != ModDataChangeType.None ) { - ModMetaChanged?.Invoke( metaChange, mod, oldName ); + ModDataChanged?.Invoke( metaChange, mod, oldName ); } } @@ -94,9 +95,9 @@ public partial class Mod } ModPathChanged.Invoke( ModPathChangeType.Reloaded, mod, mod.ModPath, mod.ModPath ); - if( metaChange != MetaChangeType.None ) + if( metaChange != ModDataChangeType.None ) { - ModMetaChanged?.Invoke( metaChange, mod, oldName ); + ModDataChanged?.Invoke( metaChange, mod, oldName ); } } @@ -211,6 +212,13 @@ public partial class Mod break; case ModPathChangeType.Deleted: NewMods.Remove( mod ); + break; + case ModPathChangeType.Moved: + if( oldDirectory != null && newDirectory != null ) + { + MoveDataFile( oldDirectory, newDirectory ); + } + break; } } diff --git a/Penumbra/Mods/Manager/Mod.Manager.Local.cs b/Penumbra/Mods/Manager/Mod.Manager.Local.cs new file mode 100644 index 00000000..cecf73b4 --- /dev/null +++ b/Penumbra/Mods/Manager/Mod.Manager.Local.cs @@ -0,0 +1,75 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using ImGuiScene; + +namespace Penumbra.Mods; + +public sealed partial class Mod +{ + public partial class Manager + { + public void ChangeModFavorite( Index idx, bool state ) + { + var mod = this[ idx ]; + if( mod.Favorite != state ) + { + mod.Favorite = state; + mod.SaveLocalData(); + ModDataChanged?.Invoke( ModDataChangeType.Favorite, mod, null ); + } + } + + public void ChangeModNote( Index idx, string newNote ) + { + var mod = this[ idx ]; + if( mod.Note != newNote ) + { + mod.Note = newNote; + mod.SaveLocalData(); + ModDataChanged?.Invoke( ModDataChangeType.Favorite, mod, null ); + } + } + + + private void ChangeTag( Index idx, int tagIdx, string newTag, bool local ) + { + var mod = this[ idx ]; + var which = local ? mod.LocalTags : mod.ModTags; + if( tagIdx < 0 || tagIdx > which.Count ) + { + return; + } + + ModDataChangeType flags = 0; + if( tagIdx == which.Count ) + { + flags = mod.UpdateTags( local ? null : which.Append( newTag ), local ? which.Append( newTag ) : null ); + } + else + { + var tmp = which.ToArray(); + tmp[ tagIdx ] = newTag; + flags = mod.UpdateTags( local ? null : tmp, local ? tmp : null ); + } + + if( flags.HasFlag( ModDataChangeType.ModTags ) ) + { + mod.SaveMeta(); + } + + if( flags.HasFlag( ModDataChangeType.LocalTags ) ) + { + mod.SaveLocalData(); + } + + if( flags != 0 ) + { + ModDataChanged?.Invoke( flags, mod, null ); + } + } + + public void ChangeLocalTag( Index idx, int tagIdx, string newTag ) + => ChangeTag( idx, tagIdx, newTag, true ); + } +} \ No newline at end of file diff --git a/Penumbra/Mods/Manager/Mod.Manager.Meta.cs b/Penumbra/Mods/Manager/Mod.Manager.Meta.cs index 312ee9cf..9c4d48ee 100644 --- a/Penumbra/Mods/Manager/Mod.Manager.Meta.cs +++ b/Penumbra/Mods/Manager/Mod.Manager.Meta.cs @@ -6,8 +6,8 @@ public sealed partial class Mod { public partial class Manager { - public delegate void ModMetaChangeDelegate( MetaChangeType type, Mod mod, string? oldName ); - public event ModMetaChangeDelegate? ModMetaChanged; + public delegate void ModDataChangeDelegate( ModDataChangeType type, Mod mod, string? oldName ); + public event ModDataChangeDelegate? ModDataChanged; public void ChangeModName( Index idx, string newName ) { @@ -17,7 +17,7 @@ public sealed partial class Mod var oldName = mod.Name; mod.Name = newName; mod.SaveMeta(); - ModMetaChanged?.Invoke( MetaChangeType.Name, mod, oldName.Text ); + ModDataChanged?.Invoke( ModDataChangeType.Name, mod, oldName.Text ); } } @@ -28,7 +28,7 @@ public sealed partial class Mod { mod.Author = newAuthor; mod.SaveMeta(); - ModMetaChanged?.Invoke( MetaChangeType.Author, mod, null ); + ModDataChanged?.Invoke( ModDataChangeType.Author, mod, null ); } } @@ -39,7 +39,7 @@ public sealed partial class Mod { mod.Description = newDescription; mod.SaveMeta(); - ModMetaChanged?.Invoke( MetaChangeType.Description, mod, null ); + ModDataChanged?.Invoke( ModDataChangeType.Description, mod, null ); } } @@ -50,7 +50,7 @@ public sealed partial class Mod { mod.Version = newVersion; mod.SaveMeta(); - ModMetaChanged?.Invoke( MetaChangeType.Version, mod, null ); + ModDataChanged?.Invoke( ModDataChangeType.Version, mod, null ); } } @@ -61,8 +61,11 @@ public sealed partial class Mod { mod.Website = newWebsite; mod.SaveMeta(); - ModMetaChanged?.Invoke( MetaChangeType.Website, mod, null ); + ModDataChanged?.Invoke( ModDataChangeType.Website, mod, null ); } } + + public void ChangeModTag( Index idx, int tagIdx, string newTag ) + => ChangeTag( idx, tagIdx, newTag, false ); } } \ No newline at end of file diff --git a/Penumbra/Mods/Mod.BasePath.cs b/Penumbra/Mods/Mod.BasePath.cs index 22760adf..42ebb23e 100644 --- a/Penumbra/Mods/Mod.BasePath.cs +++ b/Penumbra/Mods/Mod.BasePath.cs @@ -47,21 +47,23 @@ public partial class Mod return mod; } - private bool Reload( bool incorporateMetaChanges, out MetaChangeType metaChange ) + private bool Reload( bool incorporateMetaChanges, out ModDataChangeType modDataChange ) { - metaChange = MetaChangeType.Deletion; + modDataChange = ModDataChangeType.Deletion; ModPath.Refresh(); if( !ModPath.Exists ) { return false; } - metaChange = LoadMeta(); - if( metaChange.HasFlag( MetaChangeType.Deletion ) || Name.Length == 0 ) + modDataChange = LoadMeta(); + if( modDataChange.HasFlag( ModDataChangeType.Deletion ) || Name.Length == 0 ) { return false; } + LoadLocalData(); + LoadDefaultOption(); LoadAllGroups(); if( incorporateMetaChanges ) diff --git a/Penumbra/Mods/Mod.LocalData.cs b/Penumbra/Mods/Mod.LocalData.cs new file mode 100644 index 00000000..21841d3a --- /dev/null +++ b/Penumbra/Mods/Mod.LocalData.cs @@ -0,0 +1,167 @@ +using Newtonsoft.Json.Linq; +using System; +using System.Collections.Generic; +using System.IO; +using System.Linq; +using Newtonsoft.Json; + +namespace Penumbra.Mods; + +public sealed partial class Mod +{ + public static DirectoryInfo LocalDataDirectory + => new(Path.Combine( Dalamud.PluginInterface.ConfigDirectory.FullName, "mod_data" )); + + public long ImportDate { get; private set; } = DateTimeOffset.UnixEpoch.ToUnixTimeMilliseconds(); + + public IReadOnlyList< string > LocalTags { get; private set; } = Array.Empty< string >(); + + public string AllTagsLower { get; private set; } = string.Empty; + public string Note { get; private set; } = string.Empty; + public bool Favorite { get; private set; } = false; + + private FileInfo LocalDataFile + => new(Path.Combine( Dalamud.PluginInterface.ConfigDirectory.FullName, "mod_data", $"{ModPath.Name}.json" )); + + private ModDataChangeType LoadLocalData() + { + var dataFile = LocalDataFile; + + var importDate = 0L; + var localTags = Enumerable.Empty< string >(); + var favorite = false; + var note = string.Empty; + + var save = true; + if( File.Exists( dataFile.FullName ) ) + { + save = false; + try + { + var text = File.ReadAllText( dataFile.FullName ); + var json = JObject.Parse( text ); + + importDate = json[ nameof( ImportDate ) ]?.Value< long >() ?? importDate; + favorite = json[ nameof( Favorite ) ]?.Value< bool >() ?? favorite; + note = json[ nameof( Note ) ]?.Value< string >() ?? note; + localTags = json[ nameof( LocalTags ) ]?.Values< string >().OfType< string >() ?? localTags; + } + catch( Exception e ) + { + Penumbra.Log.Error( $"Could not load local mod data:\n{e}" ); + } + } + + if( importDate == 0 ) + { + importDate = DateTimeOffset.UtcNow.ToUnixTimeMilliseconds(); + } + + ModDataChangeType changes = 0; + if( ImportDate != importDate ) + { + ImportDate = importDate; + changes |= ModDataChangeType.ImportDate; + } + + changes |= UpdateTags( null, localTags ); + + if( Favorite != favorite ) + { + Favorite = favorite; + changes |= ModDataChangeType.Favorite; + } + + if( Note != note ) + { + Note = note; + changes |= ModDataChangeType.Note; + } + + if( save ) + { + SaveLocalDataFile(); + } + + return changes; + } + + private void SaveLocalData() + => Penumbra.Framework.RegisterDelayed( nameof( SaveLocalData ) + ModPath.Name, SaveLocalDataFile ); + + private void SaveLocalDataFile() + { + var dataFile = LocalDataFile; + try + { + var jObject = new JObject + { + { nameof( FileVersion ), JToken.FromObject( FileVersion ) }, + { nameof( ImportDate ), JToken.FromObject( ImportDate ) }, + { nameof( LocalTags ), JToken.FromObject( LocalTags ) }, + { nameof( Note ), JToken.FromObject( Note ) }, + { nameof( Favorite ), JToken.FromObject( Favorite ) }, + }; + dataFile.Directory!.Create(); + File.WriteAllText( dataFile.FullName, jObject.ToString( Formatting.Indented ) ); + } + catch( Exception e ) + { + Penumbra.Log.Error( $"Could not write local data file for mod {Name} to {dataFile.FullName}:\n{e}" ); + } + } + + private static void MoveDataFile( DirectoryInfo oldMod, DirectoryInfo newMod ) + { + var oldFile = Path.Combine( Dalamud.PluginInterface.ConfigDirectory.FullName, "mod_data", $"{oldMod.Name}.json" ); + var newFile = Path.Combine( Dalamud.PluginInterface.ConfigDirectory.FullName, "mod_data", $"{newMod.Name}.json" ); + if( File.Exists( oldFile ) ) + { + try + { + File.Move( oldFile, newFile, true ); + } + catch( Exception e ) + { + Penumbra.Log.Error( $"Could not move local data file {oldFile} to {newFile}:\n{e}" ); + } + } + } + + private ModDataChangeType UpdateTags( IEnumerable< string >? newModTags, IEnumerable< string >? newLocalTags ) + { + if( newModTags == null && newLocalTags == null ) + { + return 0; + } + + ModDataChangeType type = 0; + if( newModTags != null ) + { + var modTags = newModTags.Where( t => t.Length > 0 ).Distinct().ToArray(); + if( !modTags.SequenceEqual( ModTags ) ) + { + newLocalTags ??= LocalTags; + ModTags = modTags; + type |= ModDataChangeType.ModTags; + } + } + + if( newLocalTags != null ) + { + var localTags = newLocalTags!.Where( t => t.Length > 0 && !ModTags.Contains( t ) ).Distinct().ToArray(); + if( !localTags.SequenceEqual( LocalTags ) ) + { + LocalTags = localTags; + type |= ModDataChangeType.LocalTags; + } + } + + if( type != 0 ) + { + AllTagsLower = string.Join( '\0', ModTags.Concat( LocalTags ).Select( s => s.ToLowerInvariant() ) ); + } + + return type; + } +} \ No newline at end of file diff --git a/Penumbra/Mods/Mod.Meta.Migration.cs b/Penumbra/Mods/Mod.Meta.Migration.cs index 69e33628..3710896e 100644 --- a/Penumbra/Mods/Mod.Meta.Migration.cs +++ b/Penumbra/Mods/Mod.Meta.Migration.cs @@ -16,7 +16,20 @@ public sealed partial class Mod private static class Migration { public static bool Migrate( Mod mod, JObject json ) - => MigrateV0ToV1( mod, json ) || MigrateV1ToV2( mod ); + => MigrateV0ToV1( mod, json ) || MigrateV1ToV2( mod ) || MigrateV2ToV3( mod ); + + private static bool MigrateV2ToV3( Mod mod ) + { + if( mod.FileVersion > 2 ) + { + return false; + } + + // Remove import time. + mod.FileVersion = 3; + mod.SaveMeta(); + return true; + } private static bool MigrateV1ToV2( Mod mod ) { @@ -56,8 +69,8 @@ public sealed partial class Mod var swaps = json[ "FileSwaps" ]?.ToObject< Dictionary< Utf8GamePath, FullPath > >() ?? new Dictionary< Utf8GamePath, FullPath >(); - var groups = json[ "Groups" ]?.ToObject< Dictionary< string, OptionGroupV0 > >() ?? new Dictionary< string, OptionGroupV0 >(); - var priority = 1; + var groups = json[ "Groups" ]?.ToObject< Dictionary< string, OptionGroupV0 > >() ?? new Dictionary< string, OptionGroupV0 >(); + var priority = 1; var seenMetaFiles = new HashSet< FullPath >(); foreach( var group in groups.Values ) { @@ -187,7 +200,7 @@ public sealed partial class Mod private static SubMod SubModFromOption( Mod mod, OptionV0 option, HashSet< FullPath > seenMetaFiles ) { - var subMod = new SubMod(mod) { Name = option.OptionName }; + var subMod = new SubMod( mod ) { Name = option.OptionName }; AddFilesToSubMod( subMod, mod.ModPath, option, seenMetaFiles ); subMod.IncorporateMetaChanges( mod.ModPath, false ); return subMod; diff --git a/Penumbra/Mods/Mod.Meta.cs b/Penumbra/Mods/Mod.Meta.cs index 21db4857..a03377ec 100644 --- a/Penumbra/Mods/Mod.Meta.cs +++ b/Penumbra/Mods/Mod.Meta.cs @@ -1,5 +1,7 @@ using System; +using System.Collections.Generic; using System.IO; +using System.Linq; using Newtonsoft.Json; using Newtonsoft.Json.Linq; using OtterGui.Classes; @@ -7,17 +9,21 @@ using OtterGui.Classes; namespace Penumbra.Mods; [Flags] -public enum MetaChangeType : ushort +public enum ModDataChangeType : ushort { - None = 0x00, - Name = 0x01, - Author = 0x02, - Description = 0x04, - Version = 0x08, - Website = 0x10, - Deletion = 0x20, - Migration = 0x40, - ImportDate = 0x80, + None = 0x0000, + Name = 0x0001, + Author = 0x0002, + Description = 0x0004, + Version = 0x0008, + Website = 0x0010, + Deletion = 0x0020, + Migration = 0x0040, + ModTags = 0x0080, + ImportDate = 0x0100, + Favorite = 0x0200, + LocalTags = 0x0400, + Note = 0x0800, } public sealed partial class Mod @@ -29,25 +35,25 @@ public sealed partial class Mod Priority = int.MaxValue, }; - public const uint CurrentFileVersion = 1; + public const uint CurrentFileVersion = 3; public uint FileVersion { get; private set; } = CurrentFileVersion; public LowerString Name { get; private set; } = "New Mod"; public LowerString Author { get; private set; } = LowerString.Empty; public string Description { get; private set; } = string.Empty; public string Version { get; private set; } = string.Empty; public string Website { get; private set; } = string.Empty; - public long ImportDate { get; private set; } = DateTimeOffset.UtcNow.ToUnixTimeMilliseconds(); + public IReadOnlyList< string > ModTags { get; private set; } = Array.Empty< string >(); internal FileInfo MetaFile => new(Path.Combine( ModPath.FullName, "meta.json" )); - private MetaChangeType LoadMeta() + private ModDataChangeType LoadMeta() { var metaFile = MetaFile; if( !File.Exists( metaFile.FullName ) ) { Penumbra.Log.Debug( $"No mod meta found for {ModPath.Name}." ); - return MetaChangeType.Deletion; + return ModDataChangeType.Deletion; } try @@ -61,36 +67,37 @@ public sealed partial class Mod var newVersion = json[ nameof( Version ) ]?.Value< string >() ?? string.Empty; var newWebsite = json[ nameof( Website ) ]?.Value< string >() ?? string.Empty; var newFileVersion = json[ nameof( FileVersion ) ]?.Value< uint >() ?? 0; - var importDate = json[ nameof( ImportDate ) ]?.Value< long >() ?? DateTimeOffset.UtcNow.ToUnixTimeMilliseconds(); + var importDate = json[ nameof( ImportDate ) ]?.Value< long >(); + var modTags = json[ nameof( ModTags ) ]?.Values< string >().OfType< string >(); - MetaChangeType changes = 0; + ModDataChangeType changes = 0; if( Name != newName ) { - changes |= MetaChangeType.Name; + changes |= ModDataChangeType.Name; Name = newName; } if( Author != newAuthor ) { - changes |= MetaChangeType.Author; + changes |= ModDataChangeType.Author; Author = newAuthor; } if( Description != newDescription ) { - changes |= MetaChangeType.Description; + changes |= ModDataChangeType.Description; Description = newDescription; } if( Version != newVersion ) { - changes |= MetaChangeType.Version; + changes |= ModDataChangeType.Version; Version = newVersion; } if( Website != newWebsite ) { - changes |= MetaChangeType.Website; + changes |= ModDataChangeType.Website; Website = newWebsite; } @@ -99,22 +106,24 @@ public sealed partial class Mod FileVersion = newFileVersion; if( Migration.Migrate( this, json ) ) { - changes |= MetaChangeType.Migration; + changes |= ModDataChangeType.Migration; } } - if( ImportDate != importDate ) + if( importDate != null && ImportDate != importDate.Value ) { - ImportDate = importDate; - changes |= MetaChangeType.ImportDate; + ImportDate = importDate.Value; + changes |= ModDataChangeType.ImportDate; } + changes |= UpdateTags( modTags, null ); + return changes; } catch( Exception e ) { Penumbra.Log.Error( $"Could not load mod meta:\n{e}" ); - return MetaChangeType.Deletion; + return ModDataChangeType.Deletion; } } @@ -134,7 +143,7 @@ public sealed partial class Mod { nameof( Description ), JToken.FromObject( Description ) }, { nameof( Version ), JToken.FromObject( Version ) }, { nameof( Website ), JToken.FromObject( Website ) }, - { nameof( ImportDate ), JToken.FromObject( ImportDate ) }, + { nameof( ModTags ), JToken.FromObject( ModTags ) }, }; File.WriteAllText( metaFile.FullName, jObject.ToString( Formatting.Indented ) ); } diff --git a/Penumbra/Mods/ModFileSystem.cs b/Penumbra/Mods/ModFileSystem.cs index c8cbdf22..9b75dbc4 100644 --- a/Penumbra/Mods/ModFileSystem.cs +++ b/Penumbra/Mods/ModFileSystem.cs @@ -33,7 +33,7 @@ public sealed class ModFileSystem : FileSystem< Mod >, IDisposable ret.Changed += ret.OnChange; Penumbra.ModManager.ModDiscoveryFinished += ret.Reload; - Penumbra.ModManager.ModMetaChanged += ret.OnMetaChange; + Penumbra.ModManager.ModDataChanged += ret.OnDataChange; Penumbra.ModManager.ModPathChanged += ret.OnModPathChange; return ret; @@ -43,7 +43,7 @@ public sealed class ModFileSystem : FileSystem< Mod >, IDisposable { Penumbra.ModManager.ModPathChanged -= OnModPathChange; Penumbra.ModManager.ModDiscoveryFinished -= Reload; - Penumbra.ModManager.ModMetaChanged -= OnMetaChange; + Penumbra.ModManager.ModDataChanged -= OnDataChange; } public struct ImportDate : ISortMode< Mod > @@ -92,9 +92,9 @@ public sealed class ModFileSystem : FileSystem< Mod >, IDisposable } // Update sort order when defaulted mod names change. - private void OnMetaChange( MetaChangeType type, Mod mod, string? oldName ) + private void OnDataChange( ModDataChangeType type, Mod mod, string? oldName ) { - if( type.HasFlag( MetaChangeType.Name ) && oldName != null ) + if( type.HasFlag( ModDataChangeType.Name ) && oldName != null ) { var old = oldName.FixName(); if( Find( old, out var child ) && child is not Folder ) diff --git a/Penumbra/Penumbra.cs b/Penumbra/Penumbra.cs index 3bfb3568..57648940 100644 --- a/Penumbra/Penumbra.cs +++ b/Penumbra/Penumbra.cs @@ -454,6 +454,7 @@ public class Penumbra : IDalamudPlugin var list = Directory.Exists( collectionDir ) ? new DirectoryInfo( collectionDir ).EnumerateFiles( "*.json" ).ToList() : new List< FileInfo >(); + list.AddRange( Mod.LocalDataDirectory.Exists ? Mod.LocalDataDirectory.EnumerateFiles( "*.json" ) : Enumerable.Empty< FileInfo >() ); list.Add( Dalamud.PluginInterface.ConfigFile ); list.Add( new FileInfo( ModFileSystem.ModFileSystemFile ) ); list.Add( new FileInfo( ModCollection.Manager.ActiveCollectionFile ) ); diff --git a/Penumbra/UI/Classes/ModFileSystemSelector.Filters.cs b/Penumbra/UI/Classes/ModFileSystemSelector.Filters.cs index 79156cb5..93a3e9cd 100644 --- a/Penumbra/UI/Classes/ModFileSystemSelector.Filters.cs +++ b/Penumbra/UI/Classes/ModFileSystemSelector.Filters.cs @@ -29,8 +29,9 @@ public partial class ModFileSystemSelector private void SetFilterTooltip() { FilterTooltip = "Filter mods for those where their full paths or names contain the given substring.\n" - + "Enter n:[string] to filter only for mod names and no paths.\n" + "Enter c:[string] to filter for mods changing specific items.\n" + + "Enter t:[string] to filter for mods set to specific tags.\n" + + "Enter n:[string] to filter only for mod names and no paths.\n" + "Enter a:[string] to filter for mods by specific authors."; } @@ -49,6 +50,8 @@ public partial class ModFileSystemSelector 'A' => filterValue.Length == 2 ? ( LowerString.Empty, -1 ) : ( new LowerString( filterValue[ 2.. ] ), 2 ), 'c' => filterValue.Length == 2 ? ( LowerString.Empty, -1 ) : ( new LowerString( filterValue[ 2.. ] ), 3 ), 'C' => filterValue.Length == 2 ? ( LowerString.Empty, -1 ) : ( new LowerString( filterValue[ 2.. ] ), 3 ), + 't' => filterValue.Length == 2 ? ( LowerString.Empty, -1 ) : ( new LowerString( filterValue[ 2.. ] ), 4 ), + 'T' => filterValue.Length == 2 ? ( LowerString.Empty, -1 ) : ( new LowerString( filterValue[ 2.. ] ), 4 ), _ => ( new LowerString( filterValue ), 0 ), }, _ => ( new LowerString( filterValue ), 0 ), @@ -96,7 +99,8 @@ public partial class ModFileSystemSelector 0 => !( leaf.FullName().Contains( _modFilter.Lower, IgnoreCase ) || mod.Name.Contains( _modFilter ) ), 1 => !mod.Name.Contains( _modFilter ), 2 => !mod.Author.Contains( _modFilter ), - 3 => !mod.LowerChangedItemsString.Contains( _modFilter.Lower, IgnoreCase ), + 3 => !mod.LowerChangedItemsString.Contains( _modFilter.Lower ), + 4 => !mod.AllTagsLower.Contains( _modFilter.Lower ), _ => false, // Should never happen }; } @@ -143,6 +147,13 @@ public partial class ModFileSystemSelector return true; } + // Handle Favoritism + if( !_stateFilter.HasFlag( ModFilter.Favorite ) && mod.Favorite + || !_stateFilter.HasFlag( ModFilter.NotFavorite ) && !mod.Favorite ) + { + return true; + } + // Handle Inheritance if( collection == Penumbra.CollectionManager.Current ) { diff --git a/Penumbra/UI/Classes/ModFileSystemSelector.cs b/Penumbra/UI/Classes/ModFileSystemSelector.cs index 30d94f02..f5ffdcf9 100644 --- a/Penumbra/UI/Classes/ModFileSystemSelector.cs +++ b/Penumbra/UI/Classes/ModFileSystemSelector.cs @@ -43,7 +43,7 @@ public sealed partial class ModFileSystemSelector : FileSystemSelector< Mod, Mod Penumbra.CollectionManager.CollectionChanged += OnCollectionChange; Penumbra.CollectionManager.Current.ModSettingChanged += OnSettingChange; Penumbra.CollectionManager.Current.InheritanceChanged += OnInheritanceChange; - Penumbra.ModManager.ModMetaChanged += OnModMetaChange; + Penumbra.ModManager.ModDataChanged += OnModDataChange; Penumbra.ModManager.ModDiscoveryStarted += StoreCurrentSelection; Penumbra.ModManager.ModDiscoveryFinished += RestoreLastSelection; OnCollectionChange( CollectionType.Current, null, Penumbra.CollectionManager.Current, null ); @@ -54,7 +54,7 @@ public sealed partial class ModFileSystemSelector : FileSystemSelector< Mod, Mod base.Dispose(); Penumbra.ModManager.ModDiscoveryStarted -= StoreCurrentSelection; Penumbra.ModManager.ModDiscoveryFinished -= RestoreLastSelection; - Penumbra.ModManager.ModMetaChanged -= OnModMetaChange; + Penumbra.ModManager.ModDataChanged -= OnModDataChange; Penumbra.CollectionManager.Current.ModSettingChanged -= OnSettingChange; Penumbra.CollectionManager.Current.InheritanceChanged -= OnInheritanceChange; Penumbra.CollectionManager.CollectionChanged -= OnCollectionChange; @@ -120,7 +120,7 @@ public sealed partial class ModFileSystemSelector : FileSystemSelector< Mod, Mod var flags = selected ? ImGuiTreeNodeFlags.Selected | LeafFlags : LeafFlags; using var c = ImRaii.PushColor( ImGuiCol.Text, state.Color.Value() ); using var id = ImRaii.PushId( leaf.Value.Index ); - using var _ = ImRaii.TreeNode( leaf.Value.Name, flags ); + ImRaii.TreeNode( leaf.Value.Name, flags ).Dispose(); } @@ -347,12 +347,15 @@ public sealed partial class ModFileSystemSelector : FileSystemSelector< Mod, Mod } } - private void OnModMetaChange( MetaChangeType type, Mod mod, string? oldName ) + private void OnModDataChange( ModDataChangeType type, Mod mod, string? oldName ) { switch( type ) { - case MetaChangeType.Name: - case MetaChangeType.Author: + case ModDataChangeType.Name: + case ModDataChangeType.Author: + case ModDataChangeType.ModTags: + case ModDataChangeType.LocalTags: + case ModDataChangeType.Favorite: SetFilterDirty(); break; } diff --git a/Penumbra/UI/Classes/ModFilter.cs b/Penumbra/UI/Classes/ModFilter.cs index 8812a203..3c68f15c 100644 --- a/Penumbra/UI/Classes/ModFilter.cs +++ b/Penumbra/UI/Classes/ModFilter.cs @@ -7,33 +7,37 @@ public enum ModFilter { Enabled = 1 << 0, Disabled = 1 << 1, - NoConflict = 1 << 2, - SolvedConflict = 1 << 3, - UnsolvedConflict = 1 << 4, - HasNoMetaManipulations = 1 << 5, - HasMetaManipulations = 1 << 6, - HasNoFileSwaps = 1 << 7, - HasFileSwaps = 1 << 8, - HasConfig = 1 << 9, - HasNoConfig = 1 << 10, - HasNoFiles = 1 << 11, - HasFiles = 1 << 12, - IsNew = 1 << 13, - NotNew = 1 << 14, - Inherited = 1 << 15, - Uninherited = 1 << 16, - Undefined = 1 << 17, + Favorite = 1 << 2, + NotFavorite = 1 << 3, + NoConflict = 1 << 4, + SolvedConflict = 1 << 5, + UnsolvedConflict = 1 << 6, + HasNoMetaManipulations = 1 << 7, + HasMetaManipulations = 1 << 8, + HasNoFileSwaps = 1 << 9, + HasFileSwaps = 1 << 10, + HasConfig = 1 << 11, + HasNoConfig = 1 << 12, + HasNoFiles = 1 << 13, + HasFiles = 1 << 14, + IsNew = 1 << 15, + NotNew = 1 << 16, + Inherited = 1 << 17, + Uninherited = 1 << 18, + Undefined = 1 << 19, }; public static class ModFilterExtensions { - public const ModFilter UnfilteredStateMods = ( ModFilter )( ( 1 << 18 ) - 1 ); + public const ModFilter UnfilteredStateMods = ( ModFilter )( ( 1 << 20 ) - 1 ); public static string ToName( this ModFilter filter ) => filter switch { ModFilter.Enabled => "Enabled", ModFilter.Disabled => "Disabled", + ModFilter.Favorite => "Favorite", + ModFilter.NotFavorite => "No Favorite", ModFilter.NoConflict => "No Conflicts", ModFilter.SolvedConflict => "Solved Conflicts", ModFilter.UnsolvedConflict => "Unsolved Conflicts", diff --git a/Penumbra/UI/ConfigWindow.Changelog.cs b/Penumbra/UI/ConfigWindow.Changelog.cs index 75285eb3..138c1d64 100644 --- a/Penumbra/UI/ConfigWindow.Changelog.cs +++ b/Penumbra/UI/ConfigWindow.Changelog.cs @@ -22,12 +22,19 @@ public partial class ConfigWindow Add5_8_7( ret ); Add5_9_0( ret ); Add5_10_0( ret ); + Add5_11_0( ret ); return ret; } private static void Add5_11_0( Changelog log ) => log.NextVersion( "Version 0.5.11.0" ) + .RegisterEntry( + "Added local data storage for mods in the plugin config folder. This information is not exported together with your mod, but not dependent on collections." ) + .RegisterEntry( "Moved the import date from mod metadata to local data.", 1 ) + .RegisterEntry( "Added Favorites. You can declare mods as favorites and filter for them.", 1 ) + .RegisterEntry( "Added Local Tags. You can apply custom Tags to mods and filter for them.", 1 ) + .RegisterEntry( "Added Mod Tags. Mod Creators (and the Edit Mod tab) can set tags that are stored in the mod meta data and are thus exported." ) .RegisterEntry( "Add backface and transparency toggles to .mtrl editing, as well as a info section." ) .RegisterEntry( "Meta Manipulation editing now highlights if the selected ID is 0 or 1." ) .RegisterEntry( "Fixed a bug when manually adding EQP or EQDP entries to Mods." ) diff --git a/Penumbra/UI/ConfigWindow.ModPanel.Edit.cs b/Penumbra/UI/ConfigWindow.ModPanel.Edit.cs index e5948ee8..6b09d7dc 100644 --- a/Penumbra/UI/ConfigWindow.ModPanel.Edit.cs +++ b/Penumbra/UI/ConfigWindow.ModPanel.Edit.cs @@ -55,6 +55,13 @@ public partial class ConfigWindow } } + ImGui.Dummy( _window._defaultSpace ); + var tagIdx = _modTags.Draw( "Mod Tags: ", "Edit tags by clicking them, or add new tags. Empty tags are removed.", _mod.ModTags, out var editedTag ); + if( tagIdx >= 0 ) + { + Penumbra.ModManager.ChangeModTag( _mod.Index, tagIdx, editedTag ); + } + ImGui.Dummy( _window._defaultSpace ); AddOptionGroup.Draw( _window, _mod ); ImGui.Dummy( _window._defaultSpace ); @@ -566,7 +573,7 @@ public partial class ConfigWindow ImGui.TableNextColumn(); var canAddGroup = mod.Groups[ groupIdx ].Type != GroupType.Multi || mod.Groups[ groupIdx ].Count < IModGroup.MaxMultiOptions; - var validName = _newOptionName.Length > 0 && _newOptionNameIdx == groupIdx; + var validName = _newOptionName.Length > 0 && _newOptionNameIdx == groupIdx; var tt = canAddGroup ? validName ? "Add a new option to this group." : "Please enter a name for the new option." : $"Can not add more than {IModGroup.MaxMultiOptions} options to a multi group."; @@ -642,7 +649,7 @@ public partial class ConfigWindow { GroupType.Single => "Single Group", GroupType.Multi => "Multi Group", - _ => "Unknown", + _ => "Unknown", }; ImGui.SetNextItemWidth( _window._inputTextWidth.X - 3 * _window._iconButtonSize.X - 12 * ImGuiHelpers.GlobalScale ); diff --git a/Penumbra/UI/ConfigWindow.ModPanel.Settings.cs b/Penumbra/UI/ConfigWindow.ModPanel.Settings.cs index feacb0bc..dfe692b5 100644 --- a/Penumbra/UI/ConfigWindow.ModPanel.Settings.cs +++ b/Penumbra/UI/ConfigWindow.ModPanel.Settings.cs @@ -58,16 +58,20 @@ public partial class ConfigWindow DrawPriorityInput(); OpenTutorial( BasicTutorialSteps.Priority ); DrawRemoveSettings(); - ImGui.Dummy( _window._defaultSpace ); - for( var idx = 0; idx < _mod.Groups.Count; ++idx ) - { - DrawSingleGroup( _mod.Groups[ idx ], idx ); - } - ImGui.Dummy( _window._defaultSpace ); - for( var idx = 0; idx < _mod.Groups.Count; ++idx ) + if( _mod.Groups.Count > 0 ) { - DrawMultiGroup( _mod.Groups[ idx ], idx ); + ImGui.Dummy( _window._defaultSpace ); + for( var idx = 0; idx < _mod.Groups.Count; ++idx ) + { + DrawSingleGroup( _mod.Groups[ idx ], idx ); + } + + ImGui.Dummy( _window._defaultSpace ); + for( var idx = 0; idx < _mod.Groups.Count; ++idx ) + { + DrawMultiGroup( _mod.Groups[ idx ], idx ); + } } _window._penumbra.Api.InvokePostSettingsPanel( _mod.ModPath.Name ); diff --git a/Penumbra/UI/ConfigWindow.ModPanel.Tabs.cs b/Penumbra/UI/ConfigWindow.ModPanel.Tabs.cs index c920b366..5fc8dcea 100644 --- a/Penumbra/UI/ConfigWindow.ModPanel.Tabs.cs +++ b/Penumbra/UI/ConfigWindow.ModPanel.Tabs.cs @@ -1,9 +1,11 @@ using System; using System.Numerics; +using Dalamud.Interface; using ImGuiNET; using OtterGui; using OtterGui.Classes; using OtterGui.Raii; +using OtterGui.Widgets; using Penumbra.GameData.ByteString; using Penumbra.Meta.Manipulations; using Penumbra.Mods; @@ -36,10 +38,12 @@ public partial class ConfigWindow private static readonly Utf8String ChangedItemsTabHeader = Utf8String.FromStringUnsafe( "Changed Items", false ); private static readonly Utf8String EditModTabHeader = Utf8String.FromStringUnsafe( "Edit Mod", false ); + private readonly TagButtons _modTags = new(); + private void DrawTabBar() { - ImGui.Dummy( _window._defaultSpace ); - using var tabBar = ImRaii.TabBar( "##ModTabs" ); + var tabBarHeight = ImGui.GetCursorPosY(); + using var tabBar = ImRaii.TabBar( "##ModTabs" ); if( !tabBar ) { return; @@ -47,8 +51,8 @@ public partial class ConfigWindow _availableTabs = Tabs.Settings | ( _mod.ChangedItems.Count > 0 ? Tabs.ChangedItems : 0 ) - | ( _mod.Description.Length > 0 ? Tabs.Description : 0 ) - | ( _conflicts.Count > 0 ? Tabs.Conflicts : 0 ) + | Tabs.Description + | ( _conflicts.Count > 0 ? Tabs.Conflicts : 0 ) | Tabs.Edit; DrawSettingsTab(); @@ -56,6 +60,12 @@ public partial class ConfigWindow DrawChangedItemsTab(); DrawConflictsTab(); DrawEditModTab(); + DrawAdvancedEditingButton(); + DrawFavoriteButton( tabBarHeight ); + } + + private void DrawAdvancedEditingButton() + { if( ImGui.TabItemButton( "Advanced Editing", ImGuiTabItemFlags.Trailing | ImGuiTabItemFlags.NoTooltip ) ) { _window.ModEditPopup.ChangeMod( _mod ); @@ -73,6 +83,44 @@ public partial class ConfigWindow + "\t\t- textures" ); } + private void DrawFavoriteButton( float height ) + { + var oldPos = ImGui.GetCursorPos(); + + using( var font = ImRaii.PushFont( UiBuilder.IconFont ) ) + { + var size = ImGui.CalcTextSize( FontAwesomeIcon.Star.ToIconString() ) + ImGui.GetStyle().FramePadding * 2; + var newPos = new Vector2( ImGui.GetWindowWidth() - size.X - ImGui.GetStyle().ItemSpacing.X, height ); + if( ImGui.GetScrollMaxX() > 0 ) + { + newPos.X += ImGui.GetScrollX(); + } + + var rectUpper = ImGui.GetWindowPos() + newPos; + var color = ImGui.IsMouseHoveringRect( rectUpper, rectUpper + size ) ? ImGui.GetColorU32( ImGuiCol.Text ) : + _mod.Favorite ? 0xFF00FFFF : ImGui.GetColorU32( ImGuiCol.TextDisabled ); + using var c = ImRaii.PushColor( ImGuiCol.Text, color ) + .Push( ImGuiCol.Button, 0 ) + .Push( ImGuiCol.ButtonHovered, 0 ) + .Push( ImGuiCol.ButtonActive, 0 ); + + ImGui.SetCursorPos( newPos ); + if( ImGui.Button( FontAwesomeIcon.Star.ToIconString() ) ) + { + Penumbra.ModManager.ChangeModFavorite( _mod.Index, !_mod.Favorite ); + } + } + + var hovered = ImGui.IsItemHovered(); + OpenTutorial( BasicTutorialSteps.Favorites ); + + if( hovered ) + { + ImGui.SetTooltip( "Favorite" ); + } + } + + // Just a simple text box with the wrapped description, if it exists. private void DrawDescriptionTab() { @@ -88,6 +136,26 @@ public partial class ConfigWindow return; } + ImGui.Dummy( ImGuiHelpers.ScaledVector2( 2 ) ); + + ImGui.Dummy( ImGuiHelpers.ScaledVector2( 2 ) ); + var tagIdx = _localTags.Draw( "Local Tags: ", "Custom tags you can set personally that will not be exported to the mod data but only set for you.\n" + + "If the mod already contains a local tag in its own tags, the local tag will be ignored.", _mod.LocalTags, + out var editedTag ); + OpenTutorial( BasicTutorialSteps.Tags ); + if( tagIdx >= 0 ) + { + Penumbra.ModManager.ChangeLocalTag( _mod.Index, tagIdx, editedTag ); + } + if( _mod.ModTags.Count > 0 ) + { + _modTags.Draw( "Mod Tags: ", "Tags assigned by the mod creator and saved with the mod data. To edit these, look at Edit Mod.", _mod.ModTags, out var _, false, + ImGui.CalcTextSize( "Local " ).X - ImGui.CalcTextSize( "Mod " ).X ); + } + + ImGui.Dummy( ImGuiHelpers.ScaledVector2( 2 ) ); + ImGui.Separator(); + ImGuiUtil.TextWrapped( _mod.Description ); } diff --git a/Penumbra/UI/ConfigWindow.ModsTab.cs b/Penumbra/UI/ConfigWindow.ModsTab.cs index 57c48c9e..eb3a3a8a 100644 --- a/Penumbra/UI/ConfigWindow.ModsTab.cs +++ b/Penumbra/UI/ConfigWindow.ModsTab.cs @@ -8,8 +8,8 @@ using System; using System.Linq; using System.Numerics; using Dalamud.Interface; +using OtterGui.Widgets; using Penumbra.Api.Enums; -using Penumbra.GameData.Enums; namespace Penumbra.UI; @@ -198,9 +198,10 @@ public partial class ConfigWindow { private readonly ConfigWindow _window; - private bool _valid; - private ModFileSystem.Leaf _leaf = null!; - private Mod _mod = null!; + private bool _valid; + private ModFileSystem.Leaf _leaf = null!; + private Mod _mod = null!; + private readonly TagButtons _localTags = new(); public ModPanel( ConfigWindow window ) => _window = window; diff --git a/Penumbra/UI/ConfigWindow.Tutorial.cs b/Penumbra/UI/ConfigWindow.Tutorial.cs index a5b34840..5826d3f1 100644 --- a/Penumbra/UI/ConfigWindow.Tutorial.cs +++ b/Penumbra/UI/ConfigWindow.Tutorial.cs @@ -81,6 +81,8 @@ public partial class ConfigWindow Faq1, Faq2, Faq3, + Favorites, + Tags, } public static readonly Tutorial Tutorial = new Tutorial() @@ -159,5 +161,7 @@ public partial class ConfigWindow .Register( "FAQ 2", "It is advised to not use TexTools and Penumbra at the same time. Penumbra may refuse to work if TexTools broke your game indices." ) .Register( "FAQ 3", "Penumbra can change the skin material a mod uses. This is under advanced editing." ) + .Register( "Favorites", "You can now toggle mods as favorites using this button. You can filter for favorited mods in the mod selector. Favorites are stored locally, not within the mod, but independently of collections." ) + .Register( "Tags", "Mods can now have two types of tags:\n\n- Local Tags are those that you can set for yourself. They are stored locally and are not saved in any way in the mod directory itself.\n- Mod Tags are stored in the mod metadata, are set by the mod creator and are exported together with the mod, they can only be edited in the Edit Mod tab.\n\nIf a mod has a tag in its Mod Tags, this overwrites any identical Local Tags.\n\nYou can filter for tags in the mod selector via 't:text'." ) .EnsureSize( Enum.GetValues< BasicTutorialSteps >().Length ); } \ No newline at end of file