Files
2026-07-13 12:26:32 +08:00

234 lines
8.4 KiB
C#

// Copyright 2025 Takuto Nakamura
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
using System.Diagnostics;
using System.Drawing.Drawing2D;
using System.Drawing.Imaging;
using System.Text.Json;
namespace RunCat365
{
internal class CustomRunnerRepository
{
internal const int MIN_FRAME_COUNT = 2;
internal const int MAX_FRAME_COUNT = 30;
private const int MAX_FRAME_HEIGHT = 32;
private const int MIN_FRAME_WIDTH = 10;
private const int MAX_FRAME_WIDTH = 32;
private const string PROFILES_FILE_NAME = "profiles.json";
private readonly string basePath;
private List<CustomRunnerProfile> profiles = [];
internal CustomRunnerRepository()
{
basePath = Path.Combine(
Environment.GetFolderPath(Environment.SpecialFolder.LocalApplicationData),
"RunCat365",
"CustomRunners"
);
Directory.CreateDirectory(basePath);
Load();
}
internal List<CustomRunnerProfile> GetAll()
{
return [.. profiles];
}
internal CustomRunnerProfile? GetByName(string name)
{
return profiles.Find(p => p.Name.Equals(name, StringComparison.OrdinalIgnoreCase));
}
internal List<Bitmap> LoadFrames(string name)
{
var profile = GetByName(name);
if (profile is null) return [];
var runnerDirectory = Path.Combine(basePath, SanitizeDirectoryName(name));
var frames = new List<Bitmap>();
foreach (var fileName in profile.FrameFileNames)
{
var filePath = Path.Combine(runnerDirectory, fileName);
if (!File.Exists(filePath)) continue;
var frame = TryLoadBitmap(filePath);
if (frame is not null) frames.Add(frame);
}
return frames;
}
internal Bitmap? LoadFirstFrame(string name)
{
var profile = GetByName(name);
if (profile is null || profile.FrameFileNames.Count == 0) return null;
var runnerDirectory = Path.Combine(basePath, SanitizeDirectoryName(name));
var filePath = Path.Combine(runnerDirectory, profile.FrameFileNames[0]);
return File.Exists(filePath) ? TryLoadBitmap(filePath) : null;
}
private static Bitmap? TryLoadBitmap(string filePath)
{
try
{
return new Bitmap(filePath);
}
catch (Exception ex) when (ex is ArgumentException or OutOfMemoryException or FileNotFoundException)
{
Debug.WriteLine($"Failed to load custom runner frame '{filePath}': {ex.Message}");
return null;
}
}
internal bool Save(string name, List<Bitmap> frames)
{
if (string.IsNullOrWhiteSpace(name)) return false;
if (frames.Count < MIN_FRAME_COUNT || frames.Count > MAX_FRAME_COUNT) return false;
var runnerDirectory = Path.Combine(basePath, SanitizeDirectoryName(name));
Directory.CreateDirectory(runnerDirectory);
var existingProfile = GetByName(name);
if (existingProfile is not null)
{
DeleteFrameFiles(existingProfile);
profiles.Remove(existingProfile);
}
var profile = new CustomRunnerProfile { Name = name };
for (int i = 0; i < frames.Count; i++)
{
using var resized = ResizeFrame(frames[i]);
var fileName = $"frame_{i}.png";
var filePath = Path.Combine(runnerDirectory, fileName);
resized.Save(filePath, ImageFormat.Png);
profile.FrameFileNames.Add(fileName);
}
profiles.Add(profile);
return TrySaveProfiles();
}
internal bool Delete(string name)
{
var profile = GetByName(name);
if (profile is null) return false;
DeleteFrameFiles(profile);
var runnerDirectory = Path.Combine(basePath, SanitizeDirectoryName(name));
try
{
if (Directory.Exists(runnerDirectory))
{
Directory.Delete(runnerDirectory, true);
}
}
catch (Exception ex) when (ex is IOException or UnauthorizedAccessException)
{
Debug.WriteLine($"Failed to delete runner directory '{runnerDirectory}': {ex.Message}");
}
profiles.Remove(profile);
TrySaveProfiles();
return true;
}
internal bool Exists(string name)
{
return GetByName(name) is not null;
}
private void Load()
{
var profilesPath = Path.Combine(basePath, PROFILES_FILE_NAME);
if (!File.Exists(profilesPath))
{
profiles = [];
return;
}
try
{
var json = File.ReadAllText(profilesPath);
profiles = JsonSerializer.Deserialize<List<CustomRunnerProfile>>(json) ?? [];
}
catch (Exception ex) when (ex is JsonException or IOException or UnauthorizedAccessException)
{
Debug.WriteLine($"Failed to load custom runner profiles: {ex.Message}");
profiles = [];
}
}
private bool TrySaveProfiles()
{
var profilesPath = Path.Combine(basePath, PROFILES_FILE_NAME);
try
{
var options = new JsonSerializerOptions { WriteIndented = true };
var json = JsonSerializer.Serialize(profiles, options);
File.WriteAllText(profilesPath, json);
return true;
}
catch (Exception ex) when (ex is IOException or UnauthorizedAccessException)
{
Debug.WriteLine($"Failed to write custom runner profiles: {ex.Message}");
return false;
}
}
private void DeleteFrameFiles(CustomRunnerProfile profile)
{
var runnerDirectory = Path.Combine(basePath, SanitizeDirectoryName(profile.Name));
foreach (var fileName in profile.FrameFileNames)
{
var filePath = Path.Combine(runnerDirectory, fileName);
try
{
File.Delete(filePath);
}
catch (Exception ex) when (ex is IOException or UnauthorizedAccessException)
{
Debug.WriteLine($"Failed to delete frame file '{filePath}': {ex.Message}");
}
}
}
private static Bitmap ResizeFrame(Bitmap original)
{
if (original.Height <= MAX_FRAME_HEIGHT &&
original.Width >= MIN_FRAME_WIDTH &&
original.Width <= MAX_FRAME_WIDTH)
{
return new Bitmap(original);
}
var scale = (float)MAX_FRAME_HEIGHT / original.Height;
var newWidth = Math.Clamp((int)(original.Width * scale), MIN_FRAME_WIDTH, MAX_FRAME_WIDTH);
var newHeight = MAX_FRAME_HEIGHT;
var resized = new Bitmap(newWidth, newHeight);
using var graphics = Graphics.FromImage(resized);
graphics.InterpolationMode = InterpolationMode.HighQualityBicubic;
graphics.DrawImage(original, 0, 0, newWidth, newHeight);
return resized;
}
private static string SanitizeDirectoryName(string name)
{
var invalidChars = Path.GetInvalidFileNameChars();
var sanitized = new string(name.Select(c => invalidChars.Contains(c) ? '_' : c).ToArray());
return sanitized.ToLowerInvariant();
}
}
}