├── Example.png ├── README.md ├── AssetOrganizer ├── ValueColorAttribute │ ├── ValueColorAttribute.cs │ └── ValueColorAttributeDrawer.cs ├── AssetOrganizerColors.cs ├── AssetOrganizerWatchedFolder.cs ├── AssetOrganizationConfig.cs └── AssetOrganizerWindow.cs └── LICENSE /Example.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/schwapo/AssetOrganizer/HEAD/Example.png -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # AssetOrganizer 2 | 3 | ### Requires [Odin Inspector] 4 | 5 | A tool to automatically organize your assets based on specific rules 6 | including **Prefix**, **Suffix**, **File Extension** and **Regex**. 7 | 8 | ![](Example.png) 9 | 10 | [Odin Inspector]: https://odininspector.com/ 11 | -------------------------------------------------------------------------------- /AssetOrganizer/ValueColorAttribute/ValueColorAttribute.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Diagnostics; 3 | using UnityEngine; 4 | 5 | [Conditional("UNITY_EDITOR")] 6 | [AttributeUsage(AttributeTargets.All, AllowMultiple = false, Inherited = true)] 7 | public class ValueColorAttribute : Attribute 8 | { 9 | public Color Color; 10 | public string GetColor; 11 | 12 | public ValueColorAttribute(string getColor) => GetColor = getColor; 13 | public ValueColorAttribute(float r, float g, float b, float a = 1f) => Color = new Color(r, g, b, a); 14 | } -------------------------------------------------------------------------------- /AssetOrganizer/AssetOrganizerColors.cs: -------------------------------------------------------------------------------- 1 | using UnityEditor; 2 | using UnityEngine; 3 | 4 | public static class AssetOrganizerColors 5 | { 6 | public static Color Success => EditorGUIUtility.isProSkin 7 | ? new Color(0.5f, 1f, 0.5f) 8 | : new Color(0.78f, 1f, 0.78f); 9 | 10 | public static Color Caution => EditorGUIUtility.isProSkin 11 | ? new Color(1f, 1f, 0.2f) 12 | : new Color(1f, 1f, 0.78f); 13 | 14 | public static Color Error => EditorGUIUtility.isProSkin 15 | ? new Color(1f, 0.5f, 0.5f) 16 | : new Color(1f, 0.78f, 0.78f); 17 | } 18 | -------------------------------------------------------------------------------- /AssetOrganizer/AssetOrganizerWatchedFolder.cs: -------------------------------------------------------------------------------- 1 | using Sirenix.OdinInspector; 2 | using System; 3 | using UnityEditor; 4 | using UnityEngine; 5 | 6 | [Serializable] 7 | public class WatchedFolder 8 | { 9 | [FolderPath(UseBackslashes = true)] 10 | [ValueColor(nameof(PathStatusColor))] 11 | [OnValueChanged(nameof(DisableSubdirectoriesIfNecessary))] 12 | public string Path; 13 | 14 | [DisableIf("@Path == \"Assets\"")] 15 | [TableColumnWidth(150, Resizable = false)] 16 | [ValueDropdown(nameof(subdirectoryOptions))] 17 | public bool IncludeSubdirectories; 18 | 19 | private readonly ValueDropdownList subdirectoryOptions = new ValueDropdownList 20 | { 21 | { "Yes", true }, 22 | { "No", false } 23 | }; 24 | 25 | private Color PathStatusColor => AssetDatabase.IsValidFolder(Path) 26 | ? AssetOrganizerColors.Success 27 | : AssetOrganizerColors.Error; 28 | 29 | private void DisableSubdirectoriesIfNecessary() 30 | { 31 | if (Path == "Assets") 32 | { 33 | IncludeSubdirectories = false; 34 | } 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2021 Antonio Rafael Antunes Miranda 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /AssetOrganizer/ValueColorAttribute/ValueColorAttributeDrawer.cs: -------------------------------------------------------------------------------- 1 | #if UNITY_EDITOR 2 | using Sirenix.OdinInspector.Editor; 3 | using Sirenix.OdinInspector.Editor.ValueResolvers; 4 | using Sirenix.Utilities.Editor; 5 | using UnityEngine; 6 | 7 | [DrawerPriority(0.4, 0.0, 0.0)] 8 | public class ValueColorAttributeDrawer : OdinAttributeDrawer 9 | { 10 | private ValueResolver colorResolver; 11 | 12 | protected override void Initialize() 13 | => colorResolver = ValueResolver.Get(Property, Attribute.GetColor, Attribute.Color); 14 | 15 | protected override void DrawPropertyLayout(GUIContent label) 16 | { 17 | if (colorResolver.HasError) 18 | { 19 | SirenixEditorGUI.ErrorMessageBox(colorResolver.ErrorMessage); 20 | CallNextDrawer(label); 21 | } 22 | else 23 | { 24 | var valueColor = colorResolver.GetValue(); 25 | var valueColorInverse = new Color( 26 | 1f / valueColor.r, 27 | 1f / valueColor.g, 28 | 1f / valueColor.b, 29 | 1f / valueColor.a); 30 | 31 | GUIHelper.PushColor(valueColor); 32 | GUIHelper.PushContentColor(valueColorInverse); 33 | CallNextDrawer(label); 34 | GUIHelper.PopContentColor(); 35 | GUIHelper.PopColor(); 36 | } 37 | } 38 | } 39 | #endif -------------------------------------------------------------------------------- /AssetOrganizer/AssetOrganizationConfig.cs: -------------------------------------------------------------------------------- 1 | using Sirenix.OdinInspector; 2 | using Sirenix.Utilities; 3 | using System; 4 | using System.Text.RegularExpressions; 5 | using UnityEditor; 6 | using UnityEngine; 7 | 8 | [Serializable] 9 | public class AssetOrganizationConfig 10 | { 11 | public enum FilterType 12 | { 13 | Prefix, 14 | Suffix, 15 | Extension, 16 | Regex 17 | } 18 | 19 | [TableColumnWidth(100, Resizable = false)] 20 | public FilterType Filter; 21 | 22 | [ValueColor(nameof(FilterStatusColor))] 23 | public string FilterString; 24 | 25 | [FolderPath(UseBackslashes = true)] 26 | [ValueColor(nameof(DestinationFolderStatusColor))] 27 | public string DestinationFolder; 28 | 29 | private Color FilterStatusColor() 30 | { 31 | if (FilterString.IsNullOrWhitespace()) 32 | { 33 | return AssetOrganizerColors.Error; 34 | } 35 | 36 | switch (Filter) 37 | { 38 | case FilterType.Prefix: 39 | case FilterType.Suffix: 40 | return AssetOrganizerColors.Success; 41 | 42 | case FilterType.Extension: 43 | return FilterString.StartsWith(".") 44 | ? AssetOrganizerColors.Error 45 | : AssetOrganizerColors.Success; 46 | 47 | case FilterType.Regex: 48 | return IsValidRegex(FilterString) 49 | ? AssetOrganizerColors.Success 50 | : AssetOrganizerColors.Error; 51 | 52 | default: 53 | return AssetOrganizerColors.Success; 54 | } 55 | } 56 | 57 | private Color DestinationFolderStatusColor() 58 | { 59 | if (DestinationFolder.IsNullOrWhitespace() || !DestinationFolder.StartsWith("Assets")) 60 | { 61 | return AssetOrganizerColors.Error; 62 | } 63 | 64 | return AssetDatabase.IsValidFolder(DestinationFolder) 65 | ? AssetOrganizerColors.Success 66 | : AssetOrganizerColors.Caution; 67 | } 68 | 69 | private static bool IsValidRegex(string pattern) 70 | { 71 | try 72 | { 73 | Regex.Match("", pattern); 74 | } 75 | catch (ArgumentException) 76 | { 77 | return false; 78 | } 79 | 80 | return true; 81 | } 82 | } 83 | -------------------------------------------------------------------------------- /AssetOrganizer/AssetOrganizerWindow.cs: -------------------------------------------------------------------------------- 1 | using Sirenix.OdinInspector; 2 | using Sirenix.OdinInspector.Editor; 3 | using Sirenix.Utilities; 4 | using System.Collections.Generic; 5 | using System.IO; 6 | using System.Linq; 7 | using System.Text.RegularExpressions; 8 | using UnityEditor; 9 | using UnityEngine; 10 | 11 | public class AssetOrganizerWindow : OdinEditorWindow 12 | { 13 | public enum Tab 14 | { 15 | Organization, 16 | Help 17 | } 18 | 19 | [HideLabel] 20 | [EnumToggleButtons] 21 | [PropertySpace(0f, 12f)] 22 | public Tab CurrentTab; 23 | 24 | [PropertySpace(0f, 11f)] 25 | [ShowIf("@CurrentTab == Tab.Organization")] 26 | [TableList(NumberOfItemsPerPage = 10, ShowPaging = true)] 27 | public List WatchedFolders = new List(); 28 | 29 | [ShowIf("@CurrentTab == Tab.Organization")] 30 | [TableList(NumberOfItemsPerPage = 10, ShowPaging = true)] 31 | public List AssetOrganizationConfigs = new List(); 32 | 33 | public void Organize() 34 | { 35 | foreach (var folder in WatchedFolders) 36 | { 37 | var searchOption = folder.IncludeSubdirectories 38 | ? SearchOption.AllDirectories 39 | : SearchOption.TopDirectoryOnly; 40 | 41 | var filePaths = Directory.GetFiles(AbsolutePath(folder.Path), "*", searchOption) 42 | .Where(path => Path.GetExtension(path) != ".meta") 43 | .Select(RelativePath); 44 | 45 | foreach (var filePath in filePaths) 46 | { 47 | var fileName = Path.GetFileNameWithoutExtension(filePath); 48 | var fileExtension = Path.GetExtension(filePath); 49 | 50 | foreach (var assetOrganizationConfig in AssetOrganizationConfigs) 51 | { 52 | if (assetOrganizationConfig.FilterString.IsNullOrWhitespace() 53 | || assetOrganizationConfig.DestinationFolder.IsNullOrWhitespace()) 54 | { 55 | return; 56 | } 57 | 58 | switch (assetOrganizationConfig.Filter) 59 | { 60 | case AssetOrganizationConfig.FilterType.Prefix: 61 | if (fileName.StartsWith(assetOrganizationConfig.FilterString)) 62 | { 63 | MoveAsset(filePath, assetOrganizationConfig.DestinationFolder); 64 | } 65 | break; 66 | 67 | case AssetOrganizationConfig.FilterType.Suffix: 68 | if (fileName.EndsWith(assetOrganizationConfig.FilterString)) 69 | { 70 | MoveAsset(filePath, assetOrganizationConfig.DestinationFolder); 71 | } 72 | break; 73 | 74 | case AssetOrganizationConfig.FilterType.Extension: 75 | if (fileExtension.ToLower() == $".{assetOrganizationConfig.FilterString.ToLower()}") 76 | { 77 | MoveAsset(filePath, assetOrganizationConfig.DestinationFolder); 78 | } 79 | break; 80 | 81 | case AssetOrganizationConfig.FilterType.Regex: 82 | if (Regex.Match($"{fileName}{fileExtension}", assetOrganizationConfig.FilterString).Success) 83 | { 84 | MoveAsset(filePath, assetOrganizationConfig.DestinationFolder); 85 | } 86 | break; 87 | } 88 | } 89 | } 90 | } 91 | } 92 | 93 | protected override void OnEnable() 94 | { 95 | var json = EditorPrefs.GetString(nameof(AssetOrganizerWindow)); 96 | JsonUtility.FromJsonOverwrite(json, this); 97 | } 98 | 99 | private void OnDisable() 100 | { 101 | var json = JsonUtility.ToJson(this); 102 | EditorPrefs.SetString(nameof(AssetOrganizerWindow), json); 103 | } 104 | 105 | protected override void OnGUI() 106 | { 107 | EditorGUILayout.BeginHorizontal(); 108 | GUILayout.Space(9); 109 | EditorGUILayout.BeginVertical(); 110 | GUILayout.Space(9); 111 | 112 | base.OnGUI(); 113 | 114 | if (CurrentTab == Tab.Organization) 115 | { 116 | if (GUILayout.Button("Organize")) 117 | { 118 | Organize(); 119 | } 120 | } 121 | 122 | GUILayout.Space(9); 123 | EditorGUILayout.EndVertical(); 124 | GUILayout.Space(9); 125 | EditorGUILayout.EndHorizontal(); 126 | } 127 | 128 | [OnInspectorGUI] 129 | [ShowIf("@CurrentTab == Tab.Help")] 130 | [FoldoutGroup("Watched folders")] 131 | [InfoBox("Under watched folders you will find all the folders that are taken into account when organizing your project so that you can ensure that you do not accidentally move files. Without this restriction, you could, for example, move files from other assets or important unity folders that should not be moved.", InfoMessageType.None)] 132 | public void WatchedFoldersInfo() { } 133 | 134 | [OnInspectorGUI] 135 | [ShowIf("@CurrentTab == Tab.Help")] 136 | [FoldoutGroup("Asset organization configs")] 137 | [InfoBox("Under Asset Organization Configs you can set the folder to which your assets should be moved. You can choose from the filter types Prefix, Suffix, Extension and Regular expression. Prefix, suffix and extension are relatively self-explanatory. Regex, on the other hand, can be very complex but also allows for more complex conditions. If you are not yet familiar with it, you should visit a reference website or use the buttons below.", InfoMessageType.None)] 138 | public void AssetOrganizationConfigsInfo() { } 139 | 140 | [Button] 141 | [PropertySpace(10f, 0f)] 142 | [ShowIf("@CurrentTab == Tab.Help")] 143 | [FoldoutGroup("Asset organization configs")] 144 | private void TestRegex() => Application.OpenURL("http://regexstorm.net/tester"); 145 | 146 | [Button] 147 | [ShowIf("@CurrentTab == Tab.Help")] 148 | [FoldoutGroup("Asset organization configs")] 149 | private void RegexReference() => Application.OpenURL("http://regexstorm.net/reference"); 150 | 151 | [OnInspectorGUI] 152 | [ShowIf("@CurrentTab == Tab.Help")] 153 | [FoldoutGroup("Color Meaning")] 154 | [InfoBox("Green means the filter string or path is a valid path that exists.\nYellow means that the path does not exist but will be created if needed.\nRed means the filter string or path is invalid.", InfoMessageType.None)] 155 | public void ColorMeaningInfo() { } 156 | 157 | private static string AbsolutePath(string relativePath) 158 | => Path.Combine(Path.GetDirectoryName(Application.dataPath), relativePath); 159 | 160 | private static string RelativePath(string absolutePath) 161 | => "Assets" + absolutePath.Substring(Application.dataPath.Length); 162 | 163 | private static void MoveAsset(string oldPath, string newPath) 164 | { 165 | if (!AssetDatabase.IsValidFolder(newPath)) 166 | { 167 | AssetDatabase.CreateFolder(Path.GetDirectoryName(newPath), Path.GetFileName(newPath)); 168 | } 169 | 170 | AssetDatabase.MoveAsset(oldPath, Path.Combine(newPath, Path.GetFileName(oldPath))); 171 | AssetDatabase.Refresh(); 172 | } 173 | 174 | [MenuItem("Tools/Schwapo/Asset Organizer")] 175 | private static void Open() => GetWindow("Asset Organizer"); 176 | } 177 | --------------------------------------------------------------------------------