LocalPalettesFetcher.cs 12 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364
  1. using System.IO;
  2. using PixiEditor.DrawingApi.Core.ColorsImpl;
  3. using PixiEditor.Extensions.Common.Localization;
  4. using PixiEditor.Extensions.Common.UserPreferences;
  5. using PixiEditor.Extensions.Palettes;
  6. using PixiEditor.Extensions.Palettes.Parsers;
  7. using PixiEditor.Helpers;
  8. using PixiEditor.Models.DataHolders;
  9. using PixiEditor.Models.DataHolders.Palettes;
  10. using PixiEditor.Models.IO;
  11. using PixiEditor.Models.IO.PaletteParsers.JascPalFile;
  12. namespace PixiEditor.Models.DataProviders;
  13. internal delegate void CacheUpdate(RefreshType refreshType, Palette itemAffected, string oldName);
  14. internal class LocalPalettesFetcher : PaletteListDataSource
  15. {
  16. private List<Palette> cachedPalettes;
  17. public event CacheUpdate CacheUpdated;
  18. private List<string> cachedFavoritePalettes;
  19. private FileSystemWatcher watcher;
  20. public LocalPalettesFetcher() : base("LOCAL_PALETTE_SOURCE_NAME")
  21. {
  22. }
  23. public override void Initialize()
  24. {
  25. InitDir();
  26. watcher = new FileSystemWatcher(Paths.PathToPalettesFolder);
  27. watcher.Filter = "*.pal";
  28. watcher.Changed += FileSystemChanged;
  29. watcher.Deleted += FileSystemChanged;
  30. watcher.Renamed += RenamedFile;
  31. watcher.Created += FileSystemChanged;
  32. watcher.EnableRaisingEvents = true;
  33. cachedFavoritePalettes = IPreferences.Current.GetLocalPreference<List<string>>(PreferencesConstants.FavouritePalettes);
  34. IPreferences.Current.AddCallback(PreferencesConstants.FavouritePalettes, updated =>
  35. {
  36. cachedFavoritePalettes = (List<string>)updated;
  37. cachedPalettes.ForEach(x => x.IsFavourite = cachedFavoritePalettes.Contains(x.Name));
  38. });
  39. }
  40. public override async Task<List<IPalette>> FetchPaletteList(int startIndex, int count, FilteringSettings filtering)
  41. {
  42. if (cachedPalettes == null)
  43. {
  44. await RefreshCacheAll();
  45. }
  46. var filteredPalettes = cachedPalettes.Where(filtering.Filter).OrderByDescending(x => x.IsFavourite).ToArray();
  47. List<IPalette> result = new List<IPalette>();
  48. if (startIndex >= filteredPalettes.Length) return result;
  49. for (int i = 0; i < count; i++)
  50. {
  51. if (startIndex + i >= filteredPalettes.Length) break;
  52. Palette palette = filteredPalettes[startIndex + i];
  53. result.Add(palette);
  54. }
  55. return result;
  56. }
  57. public static bool PaletteExists(string paletteName)
  58. {
  59. string finalFileName = paletteName;
  60. if (!paletteName.EndsWith(".pal"))
  61. {
  62. finalFileName += ".pal";
  63. }
  64. return File.Exists(Path.Join(Paths.PathToPalettesFolder, finalFileName));
  65. }
  66. public static string GetNonExistingName(string currentName, bool appendExtension = false)
  67. {
  68. string newName = Path.GetFileNameWithoutExtension(currentName);
  69. if (File.Exists(Path.Join(Paths.PathToPalettesFolder, newName + ".pal")))
  70. {
  71. int number = 1;
  72. while (true)
  73. {
  74. string potentialName = $"{newName} ({number})";
  75. number++;
  76. if (File.Exists(Path.Join(Paths.PathToPalettesFolder, potentialName + ".pal")))
  77. continue;
  78. newName = potentialName;
  79. break;
  80. }
  81. }
  82. if (appendExtension)
  83. newName += ".pal";
  84. return newName;
  85. }
  86. public async Task SavePalette(string fileName, PaletteColor[] colors)
  87. {
  88. watcher.EnableRaisingEvents = false;
  89. string path = Path.Join(Paths.PathToPalettesFolder, fileName);
  90. InitDir();
  91. await JascFileParser.SaveFile(path, new PaletteFileData(colors));
  92. watcher.EnableRaisingEvents = true;
  93. await RefreshCache(RefreshType.Created, path);
  94. }
  95. public async Task DeletePalette(string name)
  96. {
  97. if (!Directory.Exists(Paths.PathToPalettesFolder)) return;
  98. string path = Path.Join(Paths.PathToPalettesFolder, name);
  99. if (!File.Exists(path)) return;
  100. watcher.EnableRaisingEvents = false;
  101. File.Delete(path);
  102. watcher.EnableRaisingEvents = true;
  103. await RefreshCache(RefreshType.Deleted, path);
  104. }
  105. public void RenamePalette(string oldFileName, string newFileName)
  106. {
  107. if (!Directory.Exists(Paths.PathToPalettesFolder))
  108. return;
  109. string oldPath = Path.Join(Paths.PathToPalettesFolder, oldFileName);
  110. string newPath = Path.Join(Paths.PathToPalettesFolder, newFileName);
  111. if (!File.Exists(oldPath) || File.Exists(newPath))
  112. return;
  113. watcher.EnableRaisingEvents = false;
  114. File.Move(oldPath, newPath);
  115. watcher.EnableRaisingEvents = true;
  116. RefreshCacheRenamed(newPath, oldPath);
  117. }
  118. public async Task RefreshCacheAll()
  119. {
  120. string[] files = DirectoryExtensions.GetFiles(
  121. Paths.PathToPalettesFolder,
  122. string.Join("|", AvailableParsers.SelectMany(x => x.SupportedFileExtensions).Distinct()),
  123. SearchOption.TopDirectoryOnly);
  124. cachedPalettes = await ParseAll(files);
  125. CacheUpdated?.Invoke(RefreshType.All, null, null);
  126. }
  127. private async void FileSystemChanged(object sender, FileSystemEventArgs e)
  128. {
  129. bool waitableExceptionOccured = false;
  130. do
  131. {
  132. try
  133. {
  134. switch (e.ChangeType)
  135. {
  136. case WatcherChangeTypes.Created:
  137. await RefreshCache(RefreshType.Created, e.FullPath);
  138. break;
  139. case WatcherChangeTypes.Deleted:
  140. await RefreshCache(RefreshType.Deleted, e.FullPath);
  141. break;
  142. case WatcherChangeTypes.Changed:
  143. await RefreshCache(RefreshType.Updated, e.FullPath);
  144. break;
  145. case WatcherChangeTypes.Renamed:
  146. // Handled by method below
  147. break;
  148. case WatcherChangeTypes.All:
  149. await RefreshCache(RefreshType.Created, e.FullPath);
  150. break;
  151. default:
  152. throw new ArgumentOutOfRangeException();
  153. }
  154. waitableExceptionOccured = false;
  155. }
  156. catch (IOException)
  157. {
  158. waitableExceptionOccured = true;
  159. await Task.Delay(100);
  160. }
  161. }
  162. while (waitableExceptionOccured);
  163. }
  164. private async Task RefreshCache(RefreshType refreshType, string file)
  165. {
  166. Palette updated = null;
  167. string affectedFileName = null;
  168. switch (refreshType)
  169. {
  170. case RefreshType.All:
  171. throw new ArgumentException("To handle refreshing all items, use RefreshCacheAll");
  172. case RefreshType.Created:
  173. updated = await RefreshCacheAdded(file);
  174. break;
  175. case RefreshType.Updated:
  176. updated = await RefreshCacheUpdated(file);
  177. break;
  178. case RefreshType.Deleted:
  179. affectedFileName = RefreshCacheDeleted(file);
  180. break;
  181. case RefreshType.Renamed:
  182. throw new ArgumentException("To handle renaming, use RefreshCacheRenamed");
  183. default:
  184. throw new ArgumentOutOfRangeException(nameof(refreshType), refreshType, null);
  185. }
  186. if (refreshType is RefreshType.Created or RefreshType.Updated && updated == null)
  187. {
  188. await RefreshCacheAll();
  189. // Using try-catch to generate stack trace
  190. try
  191. {
  192. throw new NullReferenceException($"The '{nameof(updated)}' was null even though the refresh type was '{refreshType}'.");
  193. }
  194. catch (Exception e)
  195. {
  196. await CrashHelper.SendExceptionInfoToWebhookAsync(e);
  197. }
  198. return;
  199. }
  200. CacheUpdated?.Invoke(refreshType, updated, affectedFileName);
  201. }
  202. private void RefreshCacheRenamed(string newFilePath, string oldFilePath)
  203. {
  204. string oldFileName = Path.GetFileName(oldFilePath);
  205. int index = cachedPalettes.FindIndex(p => p.FileName == oldFileName);
  206. if (index == -1) return;
  207. Palette palette = cachedPalettes[index];
  208. palette.FileName = Path.GetFileName(newFilePath);
  209. palette.Name = Path.GetFileNameWithoutExtension(newFilePath);
  210. CacheUpdated?.Invoke(RefreshType.Renamed, palette, oldFileName);
  211. }
  212. private string RefreshCacheDeleted(string filePath)
  213. {
  214. string fileName = Path.GetFileName(filePath);
  215. int index = cachedPalettes.FindIndex(p => p.FileName == fileName);
  216. if (index == -1) return null;
  217. cachedPalettes.RemoveAt(index);
  218. return fileName;
  219. }
  220. private async Task<Palette> RefreshCacheItem(string file, Action<Palette> action)
  221. {
  222. if (File.Exists(file))
  223. {
  224. string extension = Path.GetExtension(file);
  225. var foundParser = AvailableParsers.FirstOrDefault(x => x.SupportedFileExtensions.Contains(extension));
  226. if (foundParser != null)
  227. {
  228. var newPalette = await foundParser.Parse(file);
  229. if (newPalette is { IsCorrupted: false })
  230. {
  231. Palette pal = CreatePalette(newPalette, file,
  232. cachedFavoritePalettes?.Contains(newPalette.Title) ?? false);
  233. action(pal);
  234. return pal;
  235. }
  236. }
  237. }
  238. return null;
  239. }
  240. private async Task<Palette> RefreshCacheUpdated(string file)
  241. {
  242. return await RefreshCacheItem(file, palette =>
  243. {
  244. Palette existingPalette = cachedPalettes.FirstOrDefault(x => x.FileName == palette.FileName);
  245. if (existingPalette != null)
  246. {
  247. existingPalette.Colors = palette.Colors.ToList();
  248. existingPalette.Name = palette.Name;
  249. existingPalette.FileName = palette.FileName;
  250. }
  251. });
  252. }
  253. private async Task<Palette> RefreshCacheAdded(string file)
  254. {
  255. return await RefreshCacheItem(file, palette =>
  256. {
  257. string fileName = Path.GetFileName(file);
  258. int index = cachedPalettes.FindIndex(p => p.FileName == fileName);
  259. if (index != -1)
  260. {
  261. cachedPalettes.RemoveAt(index);
  262. }
  263. cachedPalettes.Add(palette);
  264. });
  265. }
  266. private async Task<List<Palette>> ParseAll(string[] files)
  267. {
  268. List<Palette> result = new List<Palette>();
  269. foreach (var file in files)
  270. {
  271. string extension = Path.GetExtension(file);
  272. if (!File.Exists(file)) continue;
  273. var foundParser = AvailableParsers.First(x => x.SupportedFileExtensions.Contains(extension));
  274. {
  275. PaletteFileData fileData = await foundParser.Parse(file);
  276. if (fileData.IsCorrupted) continue;
  277. var palette = CreatePalette(fileData, file, cachedFavoritePalettes?.Contains(fileData.Title) ?? false);
  278. result.Add(palette);
  279. }
  280. }
  281. return result;
  282. }
  283. private Palette CreatePalette(PaletteFileData fileData, string file, bool isFavourite)
  284. {
  285. var palette = new Palette(
  286. fileData.Title,
  287. new List<PaletteColor>(fileData.GetPaletteColors()),
  288. Path.GetFileName(file), this)
  289. {
  290. IsFavourite = isFavourite
  291. };
  292. return palette;
  293. }
  294. private void RenamedFile(object sender, RenamedEventArgs e)
  295. {
  296. RefreshCacheRenamed(e.FullPath, e.OldFullPath);
  297. }
  298. private static void InitDir()
  299. {
  300. if (!Directory.Exists(Paths.PathToPalettesFolder))
  301. {
  302. Directory.CreateDirectory(Paths.PathToPalettesFolder);
  303. }
  304. }
  305. }