diff --git a/Assemblies/AIImages.dll b/Assemblies/AIImages.dll index 9b40a6c..dc6df03 100644 Binary files a/Assemblies/AIImages.dll and b/Assemblies/AIImages.dll differ diff --git a/Languages/English/Keyed/AIImages.xml b/Languages/English/Keyed/AIImages.xml index 2c8fdbf..3f4b609 100644 --- a/Languages/English/Keyed/AIImages.xml +++ b/Languages/English/Keyed/AIImages.xml @@ -104,4 +104,18 @@ Clear All Generated Images Are you sure you want to delete all generated portrait images? This action cannot be undone. Successfully deleted {0} portrait image(s) + + AI Images Gallery + Total images: {0} + Gallery is empty + Load error + Delete selected + Delete all ({0}) + Delete selected image? + Delete all {0} images? This action cannot be undone. + Delete error: {0} + Image successfully deleted + Successfully deleted {0} images + Open gallery + ({0}) diff --git a/Languages/Russian/Keyed/AIImages.xml b/Languages/Russian/Keyed/AIImages.xml index c5dd326..0cd8ad0 100644 --- a/Languages/Russian/Keyed/AIImages.xml +++ b/Languages/Russian/Keyed/AIImages.xml @@ -104,4 +104,18 @@ Очистить все сгенерированные изображения Вы уверены, что хотите удалить все сгенерированные портреты? Это действие нельзя отменить. Успешно удалено {0} изображений портретов + + Галерея AI изображений + Всего изображений: {0} + В галерее пока нет изображений + Ошибка загрузки + Удалить выбранное + Удалить всё ({0}) + Удалить выбранное изображение? + Удалить все {0} изображений? Это действие нельзя отменить. + Ошибка удаления: {0} + Изображение успешно удалено + Успешно удалено {0} изображений + Открыть галерею + ({0}) diff --git a/README.md b/README.md index e69de29..626296c 100644 --- a/README.md +++ b/README.md @@ -0,0 +1,222 @@ +# AI Images - RimWorld Mod + +Мод для RimWorld, который генерирует AI-изображения персонажей (пешек) с помощью Stable Diffusion API. + +## 🌟 Возможности + +### 📸 Генерация AI-изображений +- **Автоматическое описание персонажей** на основе: + - Внешности (пол, возраст, тип тела, цвет кожи) + - Прически и цвета волос + - Одежды с материалами, качеством и цветами + - Черт характера + - Генов (биотехнологии) + - Состояний здоровья + +### 🎨 Гибкая настройка стилей +- **9 предустановленных художественных стилей**: + - Реалистичный (Photorealistic) + - Полуреалистичный (Semi-Realistic) + - Аниме + - Концепт-арт + - Цифровая живопись + - Масляная живопись + - Эскиз + - Cel-shaded + - Без стиля (кастомные промпты) + +- **Возможность создания собственных стилей** через XML-конфигурацию +- Базовые промпты и негативные промпты +- Автоматическое добавление качественных тегов + +### 📐 Размеры изображений +- **Предустановки**: квадратные, портретные, ландшафтные +- **Кастомные размеры**: полный контроль над шириной и высотой +- **Два типа генерации**: портрет или полное тело + +### 🖼️ Галерея изображений +- **Отдельная галерея** для каждого персонажа +- **Просмотр всех сгенерированных изображений** в удобной сетке +- **Выбор изображений** кликом по миниатюре +- **Удаление** отдельных или всех изображений +- **Отображение даты создания** и имени файла + +### ⚙️ Настройки Stable Diffusion +- **Полная интеграция с Stable Diffusion WebUI** +- Загрузка моделей, сэмплеров и планировщиков из API +- Настройка количества шагов, CFG Scale, seed +- Превью промптов с возможностью копирования +- Прогресс-бар генерации с ETA + +### 🎛️ Продвинутые функции +- **Автоматическое сохранение** изображений +- **История генераций** для каждого персонажа +- **Обратная совместимость** со старыми сохранениями +- **Двуязычный интерфейс** (русский/английский) +- **Отладочные логи** для диагностики + +## 🚀 Установка + +### Требования +- **RimWorld** версии 1.6 +- **Harmony** (автоматически подтягивается через зависимости) +- **Stable Diffusion WebUI** (локально или удаленно) + +### Зависимости +Мод использует следующие зависимости (автоматически подтягиваются): +- `brrainz.harmony` - Harmony +- `rim.job.world` - RimJobWorld (необязательно, но в зависимостях) + +### Шаги установки +1. Клонируйте репозиторий или скачайте релиз +2. Скопируйте папку `ai-images` в директорию `Mods` RimWorld +3. Запустите Stable Diffusion WebUI +4. В настройках RimWorld включите мод AI Images +5. В настройках мода укажите адрес API Stable Diffusion (по умолчанию `http://127.0.0.1:7860`) + +## 📖 Использование + +### Генерация изображений + +1. **Откройте окно AI Images**: + - Нажмите на кнопку "AI Портрет" в панели действий персонажа + +2. **Настройте параметры**: + - Выберите художественный стиль + - Выберите тип изображения (портрет/полное тело) + - Настройте размер изображения + - При необходимости измените другие параметры + +3. **Сгенерируйте изображение**: + - Нажмите кнопку "Сгенерировать изображение" + - Дождитесь завершения генерации + - Изображение автоматически сохранится + +### Просмотр галереи + +1. **Откройте галерею**: + - В окне генерации нажмите кнопку "Открыть галерею" + - Откроется окно со всеми изображениями персонажа + +2. **Управление изображениями**: + - Кликните по изображению для выбора + - Нажмите "Удалить выбранное" для удаления конкретного изображения + - Нажмите "Удалить всё" для очистки галереи + +### Настройка стилей + +Мод использует XML-конфигурацию для определения стилей. Вы можете создать свои собственные стили, редактируя файл `Defs/ArtStyleDefs.xml`. + +Пример создания нового стиля: + +```xml + + ArtStyle_MyCustomStyle + + Описание вашего стиля + ваши ключевые слова здесь + чего избегать + дополнительные теги качества + true + true + 100 + +``` + +Подробнее о конфигурации стилей в [Defs/README.md](Defs/README.md). + +## 🎯 Особенности + +### Умная генерация промптов +Мод автоматически анализирует персонажа и создает детальные промпты для Stable Diffusion: +- Описывает цвет кожи естественным языком +- Добавляет информацию об одежде с материалами и качествами +- Учитывает черты характера персонажа +- Включает данные о генах из Biotech DLC +- Адаптирует промпт в зависимости от типа изображения + +### Оптимизация +- Асинхронная генерация без блокировки игры +- Возможность отмены генерации +- Прогресс-бар с реальным временем выполнения +- Эффективное управление памятью для текстур + +### Обратная совместимость +Старые сохранения с одним портретом автоматически мигрируют в новую систему галереи. + +## 🛠️ Разработка + +### Структура проекта +``` +ai-images/ +├── About/ # Метаданные мода +├── Assemblies/ # Скомпилированные DLL +├── Defs/ # XML-конфигурации (стили, размеры) +├── Languages/ # Переводы +├── Source/ # Исходный код +│ └── AIImages/ +│ ├── Components/ # Компоненты пешек +│ ├── Defs/ # Классы определений +│ ├── Helpers/ # Вспомогательные классы +│ ├── Models/ # Модели данных +│ ├── Patches/ # Harmony патчи +│ ├── Services/ # Бизнес-логика +│ ├── Settings/ # Настройки мода +│ ├── UI/ # Пользовательский интерфейс +│ └── Window_AIImage.cs # Главное окно +└── Textures/ # Текстуры UI +``` + +### Сборка +```bash +cd Source/AIImages +dotnet build -c Release +``` + +### Языки +Мод поддерживает английский и русский языки. Переводы находятся в `Languages/`. + +## 📝 История версий + +### v1.0.0 +- Базовая генерация AI-изображений +- Интеграция с Stable Diffusion API +- 9 художественных стилей +- Галерея изображений +- Настройка размеров +- Двуязычный интерфейс + +## 🤝 Вклад + +Приветствуются любые вклады! Пожалуйста: +1. Форкните репозиторий +2. Создайте ветку для вашей функции +3. Закоммитьте изменения +4. Отправьте Pull Request + +## 📄 Лицензия + +См. файл [LICENSE](LICENSE) для деталей. + +## 🙏 Благодарности + +- Ludeon Studios за создание RimWorld +- Automattic1111 за Stable Diffusion WebUI +- Сообщество RimWorld за поддержку + +## ⚠️ Известные ограничения + +- Требует запущенный Stable Diffusion WebUI +- Генерация изображений может занимать время в зависимости от настроек +- Некоторые модели Stable Diffusion могут работать медленнее других + +## 🐛 Сообщение об ошибках + +Если вы нашли баг или хотите предложить улучшение, пожалуйста, создайте Issue на GitHub с подробным описанием. + +--- + +**Автор**: mrleo1nid +**Версия RimWorld**: 1.6 +**Версия мода**: 1.0.0 + diff --git a/Source/AIImages/Components/PawnPortraitComp.cs b/Source/AIImages/Components/PawnPortraitComp.cs index a97527b..20f17c5 100644 --- a/Source/AIImages/Components/PawnPortraitComp.cs +++ b/Source/AIImages/Components/PawnPortraitComp.cs @@ -1,22 +1,84 @@ +using System.Collections.Generic; +using System.Linq; using AIImages.Helpers; using Verse; namespace AIImages.Components { /// - /// Компонент для хранения данных AI-сгенерированного портрета пешки + /// Компонент для хранения данных AI-сгенерированных портретов пешки /// public class PawnPortraitComp : ThingComp { /// - /// Путь к сохраненному портрету + /// Список путей к сохраненным портретам (галерея) /// - public string PortraitPath { get; set; } + private List portraitPaths = new List(); /// - /// Есть ли сохраненный портрет + /// Есть ли сохраненные портреты /// - public bool HasPortrait => !string.IsNullOrEmpty(PortraitPath); + public bool HasPortrait => portraitPaths != null && portraitPaths.Count > 0; + + /// + /// Количество портретов в галерее + /// + public int PortraitCount => portraitPaths?.Count ?? 0; + + /// + /// Получить все пути к портретам + /// + public List GetAllPortraits() => portraitPaths?.ToList() ?? new List(); + + /// + /// Получить последний портрет (для обратной совместимости) + /// + public string PortraitPath => HasPortrait ? portraitPaths.Last() : null; + + /// + /// Добавить новый портрет в галерею + /// + public void AddPortrait(string path) + { + if (string.IsNullOrEmpty(path)) + return; + + if (portraitPaths == null) + portraitPaths = new List(); + + portraitPaths.Add(path); + DebugLogger.Log($"[AI Images] Added portrait to gallery: {path}"); + } + + /// + /// Удалить портрет из галереи + /// + public bool RemovePortrait(string path) + { + if (portraitPaths == null || string.IsNullOrEmpty(path)) + return false; + + bool removed = portraitPaths.Remove(path); + if (removed) + { + DebugLogger.Log($"[AI Images] Removed portrait from gallery: {path}"); + } + + return removed; + } + + /// + /// Очистить все портреты + /// + public void ClearPortraits() + { + if (portraitPaths != null) + { + int count = portraitPaths.Count; + portraitPaths.Clear(); + DebugLogger.Log($"[AI Images] Cleared {count} portraits from gallery"); + } + } /// /// Сохранение/загрузка данных @@ -25,33 +87,30 @@ namespace AIImages.Components { base.PostExposeData(); - string portraitPath = PortraitPath; bool isSaving = Scribe.mode == LoadSaveMode.Saving; bool isLoading = Scribe.mode == LoadSaveMode.LoadingVars; DebugLogger.Log( - $"[AI Images] PostExposeData for {parent?.LabelShort} - Mode: {Scribe.mode}, Current path: '{PortraitPath}'" + $"[AI Images] PostExposeData for {parent?.LabelShort} - Mode: {Scribe.mode}, Portrait count: {PortraitCount}" ); - Scribe_Values.Look(ref portraitPath, "aiPortraitPath", null); + // Сохраняем список портретов + Scribe_Collections.Look(ref portraitPaths, "aiPortraitPaths", LookMode.Value); - if (isSaving) + // Обратная совместимость: если есть старый формат с одним портретом, добавляем его в список + if (isLoading && (portraitPaths == null || portraitPaths.Count == 0)) { - DebugLogger.Log( - $"[AI Images] Saving portrait path for {parent?.LabelShort}: '{portraitPath}'" - ); + string oldPortraitPath = null; + Scribe_Values.Look(ref oldPortraitPath, "aiPortraitPath", null); + if (!string.IsNullOrEmpty(oldPortraitPath)) + { + portraitPaths = new List { oldPortraitPath }; + DebugLogger.Log($"[AI Images] Migrated old single portrait to gallery: {oldPortraitPath}"); + } } - else if (isLoading) - { - DebugLogger.Log( - $"[AI Images] Loading portrait path for {parent?.LabelShort}: '{portraitPath}'" - ); - } - - PortraitPath = portraitPath; DebugLogger.Log( - $"[AI Images] PostExposeData completed for {parent?.LabelShort} - Final path: '{PortraitPath}', HasPortrait: {HasPortrait}" + $"[AI Images] PostExposeData completed for {parent?.LabelShort} - Portrait count: {PortraitCount}, HasPortrait: {HasPortrait}" ); } } diff --git a/Source/AIImages/Helpers/PawnPortraitHelper.cs b/Source/AIImages/Helpers/PawnPortraitHelper.cs index 5eb5d3c..7a2c148 100644 --- a/Source/AIImages/Helpers/PawnPortraitHelper.cs +++ b/Source/AIImages/Helpers/PawnPortraitHelper.cs @@ -1,3 +1,4 @@ +using System.Collections.Generic; using System.IO; using System.Linq; using AIImages.Components; @@ -30,7 +31,7 @@ namespace AIImages.Helpers } /// - /// Сохранить путь к портрету на пешке + /// Сохранить путь к портрету на пешке (добавляет в галерею) /// public static void SavePortraitPath(Pawn pawn, string path) { @@ -41,14 +42,11 @@ namespace AIImages.Helpers var comp = GetPortraitComp(pawn); if (comp != null) { + comp.AddPortrait(path); DebugLogger.Log( - $"[AI Images] Found portrait component for {pawn.Name}, setting path from '{comp.PortraitPath}' to '{path}'" + $"[AI Images] Successfully added portrait path for {pawn.Name}: {path}" ); - comp.PortraitPath = path; - DebugLogger.Log( - $"[AI Images] Successfully saved portrait path for {pawn.Name}: {path}" - ); - DebugLogger.Log($"[AI Images] Component now has portrait: {comp.HasPortrait}"); + DebugLogger.Log($"[AI Images] Component now has {comp.PortraitCount} portraits"); } else { @@ -147,17 +145,44 @@ namespace AIImages.Helpers } /// - /// Очистить портрет пешки + /// Очистить портрет пешки (удаляет все портреты) /// public static void ClearPortrait(Pawn pawn) { var comp = GetPortraitComp(pawn); if (comp != null) { - comp.PortraitPath = null; + comp.ClearPortraits(); } } + /// + /// Получить все пути к портретам пешки (галерея) + /// + public static List GetAllPortraits(Pawn pawn) + { + var comp = GetPortraitComp(pawn); + return comp?.GetAllPortraits() ?? new List(); + } + + /// + /// Получить количество портретов в галерее + /// + public static int GetPortraitCount(Pawn pawn) + { + var comp = GetPortraitComp(pawn); + return comp?.PortraitCount ?? 0; + } + + /// + /// Удалить конкретный портрет из галереи + /// + public static bool RemovePortrait(Pawn pawn, string path) + { + var comp = GetPortraitComp(pawn); + return comp?.RemovePortrait(path) ?? false; + } + /// /// Очистить все сгенерированные портреты /// @@ -179,7 +204,7 @@ namespace AIImages.Helpers var comp = GetPortraitComp(pawn); if (comp != null && comp.HasPortrait) { - comp.PortraitPath = null; + comp.ClearPortraits(); } } } diff --git a/Source/AIImages/Window_AIGallery.cs b/Source/AIImages/Window_AIGallery.cs new file mode 100644 index 0000000..a124468 --- /dev/null +++ b/Source/AIImages/Window_AIGallery.cs @@ -0,0 +1,413 @@ +using System.Collections.Generic; +using System.IO; +using System.Linq; +using AIImages.Helpers; +using RimWorld; +using UnityEngine; +using Verse; + +#pragma warning disable IDE1006 // Naming Styles + +namespace AIImages +{ + /// + /// Окно галереи AI-сгенерированных изображений персонажа + /// + [System.Diagnostics.CodeAnalysis.SuppressMessage( + "Style", + "IDE1006:Naming Styles", + Justification = "RimWorld Window naming convention" + )] + [System.Diagnostics.CodeAnalysis.SuppressMessage( + "Minor Code Smell", + "S101:Types should be named in PascalCase", + Justification = "RimWorld Window naming convention" + )] + public class Window_AIGallery : Window + { + private Pawn pawn; + private List portraitPaths = new List(); + private List portraitTextures = new List(); + private Vector2 mainScrollPosition = Vector2.zero; + private int selectedIndex = 0; + + public Window_AIGallery(Pawn pawn) + { + this.pawn = pawn; + this.doCloseX = true; + this.doCloseButton = true; + this.forcePause = false; + this.absorbInputAroundWindow = false; + this.draggable = true; + this.preventCameraMotion = false; + + LoadGallery(); + } + + public override Vector2 InitialSize => new Vector2(900f, 700f); + + /// + /// Загружает галерею изображений персонажа + /// + private void LoadGallery() + { + DebugLogger.Log($"[AI Gallery] Loading gallery for {pawn?.Name}"); + + // Очищаем старые текстуры + UnloadTextures(); + + // Загружаем пути к портретам + portraitPaths = PawnPortraitHelper.GetAllPortraits(pawn); + + // Загружаем текстуры + foreach (var path in portraitPaths) + { + if (File.Exists(path)) + { + try + { + byte[] imageData = File.ReadAllBytes(path); + Texture2D texture = new Texture2D(2, 2); + texture.LoadImage(imageData); + portraitTextures.Add(texture); + DebugLogger.Log($"[AI Gallery] Loaded texture from: {path}"); + } + catch (System.Exception ex) + { + DebugLogger.Warning($"[AI Gallery] Failed to load texture: {path}, Error: {ex.Message}"); + portraitTextures.Add(null); + } + } + else + { + DebugLogger.Warning($"[AI Gallery] File not found: {path}"); + portraitTextures.Add(null); + } + } + + DebugLogger.Log($"[AI Gallery] Loaded {portraitTextures.Count} textures for {pawn?.Name}"); + } + + /// + /// Освобождает ресурсы текстур + /// + private void UnloadTextures() + { + foreach (var texture in portraitTextures) + { + if (texture != null) + { + Object.Destroy(texture); + } + } + portraitTextures.Clear(); + } + + /// + /// Освобождает ресурсы при закрытии окна + /// + public override void PreClose() + { + base.PreClose(); + UnloadTextures(); + } + + /// + /// Отрисовка окна + /// + public override void DoWindowContents(Rect inRect) + { + // Заголовок + Text.Font = GameFont.Medium; + Widgets.Label( + new Rect(0f, 0f, inRect.width, 40f), + "AIImages.Gallery.Title".Translate() + ); + + // Имя персонажа + Text.Font = GameFont.Small; + Widgets.Label( + new Rect(0f, 40f, inRect.width, 30f), + "AIImages.Window.PawnLabel".Translate(pawn.NameShortColored.Resolve()) + ); + + // Количество изображений + Widgets.Label( + new Rect(0f, 70f, inRect.width, 25f), + "AIImages.Gallery.Count".Translate(portraitTextures.Count) + ); + + // Разделитель + Widgets.DrawLineHorizontal(0f, 100f, inRect.width); + + // Область для галереи + Rect galleryRect = new Rect(0f, 110f, inRect.width, inRect.height - 180f); + DrawGallery(galleryRect); + + // Кнопка обновления + if ( + Widgets.ButtonText( + new Rect(0f, inRect.height - 70f, inRect.width * 0.3f, 35f), + "AIImages.Window.Refresh".Translate() + ) + ) + { + LoadGallery(); + } + + // Кнопка удаления выбранного изображения + if ( + portraitTextures.Count > 0 + && selectedIndex >= 0 + && selectedIndex < portraitTextures.Count + && Widgets.ButtonText( + new Rect(inRect.width * 0.32f, inRect.height - 70f, inRect.width * 0.3f, 35f), + "AIImages.Gallery.DeleteSelected".Translate() + ) + ) + { + DeleteSelectedImage(); + } + + // Кнопка удаления всех изображений + if ( + portraitTextures.Count > 0 + && Widgets.ButtonText( + new Rect(inRect.width * 0.64f, inRect.height - 70f, inRect.width * 0.36f, 35f), + "AIImages.Gallery.DeleteAll".Translate(portraitTextures.Count) + ) + ) + { + DeleteAllImages(); + } + } + + /// + /// Отрисовка галереи изображений + /// + private void DrawGallery(Rect rect) + { + if (portraitTextures.Count == 0) + { + Text.Anchor = TextAnchor.MiddleCenter; + GUI.color = new Color(1f, 1f, 1f, 0.5f); + Widgets.Label(rect, "AIImages.Gallery.Empty".Translate()); + GUI.color = Color.white; + Text.Anchor = TextAnchor.UpperLeft; + return; + } + + // Параметры сетки + int columns = 3; + int rows = Mathf.CeilToInt((float)portraitTextures.Count / columns); + float cellWidth = (rect.width - 40f) / columns; + float cellHeight = cellWidth + 40f; // Высота ячейки (изображение + кнопки) + float spacing = 10f; + float viewHeight = rows * cellHeight + spacing; + + Rect viewRect = new Rect(0f, 0f, rect.width - 20f, viewHeight); + + Widgets.BeginScrollView(rect, ref mainScrollPosition, viewRect); + + for (int i = 0; i < portraitTextures.Count; i++) + { + int row = i / columns; + int col = i % columns; + + float x = col * (cellWidth + spacing); + float y = row * cellHeight; + + Rect cellRect = new Rect(x, y, cellWidth, cellHeight); + DrawGalleryItem(cellRect, i); + } + + Widgets.EndScrollView(); + } + + /// + /// Отрисовка одного элемента галереи + /// + private void DrawGalleryItem(Rect rect, int index) + { + // Подсветка выбранного элемента + if (index == selectedIndex) + { + Widgets.DrawBoxSolid(rect, new Color(0.3f, 0.5f, 0.8f, 0.2f)); + Widgets.DrawBox(rect, 2); + } + else + { + Widgets.DrawBox(rect); + } + + // Изображение + Texture2D texture = portraitTextures[index]; + if (texture != null) + { + Rect imageRect = new Rect( + rect.x + 5f, + rect.y + 5f, + rect.width - 10f, + rect.width - 10f + ); + + // Клик по изображению для выбора + if (Widgets.ButtonInvisible(imageRect)) + { + selectedIndex = index; + } + + GUI.DrawTexture(imageRect, texture); + } + else + { + Rect errorRect = new Rect(rect.x + 5f, rect.y + 5f, rect.width - 10f, rect.width - 10f); + Widgets.DrawBoxSolid(errorRect, new Color(0.5f, 0f, 0f, 0.5f)); + Text.Anchor = TextAnchor.MiddleCenter; + GUI.color = Color.white; + Widgets.Label(errorRect, "AIImages.Gallery.LoadError".Translate()); + GUI.color = Color.white; + Text.Anchor = TextAnchor.UpperLeft; + } + + // Информация под изображением + Text.Font = GameFont.Tiny; + Text.Anchor = TextAnchor.UpperLeft; + float infoY = rect.y + rect.width - 5f; + + // Имя файла + string fileName = Path.GetFileName(portraitPaths[index]); + if (fileName.Length > 30) + { + fileName = fileName.Substring(0, 27) + "..."; + } + + Widgets.Label( + new Rect(rect.x + 5f, infoY, rect.width - 10f, 20f), + fileName + ); + + // Дата создания + if (File.Exists(portraitPaths[index])) + { + try + { + var fileInfo = new FileInfo(portraitPaths[index]); + string dateStr = fileInfo.CreationTime.ToString("dd.MM.yyyy HH:mm"); + Widgets.Label( + new Rect(rect.x + 5f, infoY + 15f, rect.width - 10f, 20f), + dateStr + ); + } + catch + { + // Игнорируем ошибки чтения даты + } + } + } + + /// + /// Удаляет выбранное изображение + /// + private void DeleteSelectedImage() + { + if (selectedIndex < 0 || selectedIndex >= portraitPaths.Count) + return; + + string path = portraitPaths[selectedIndex]; + + // Подтверждение + Find.WindowStack.Add( + Dialog_MessageBox.CreateConfirmation( + "AIImages.Gallery.ConfirmDelete".Translate(), + delegate + { + // Удаляем из файловой системы + if (File.Exists(path)) + { + try + { + File.Delete(path); + DebugLogger.Log($"[AI Gallery] Deleted file: {path}"); + } + catch (System.Exception ex) + { + Messages.Message( + "AIImages.Gallery.DeleteError".Translate(ex.Message), + MessageTypeDefOf.RejectInput + ); + return; + } + } + + // Удаляем из компонента пешки + PawnPortraitHelper.RemovePortrait(pawn, path); + + // Обновляем галерею + LoadGallery(); + + // Сбрасываем выбор + if (selectedIndex >= portraitTextures.Count) + { + selectedIndex = portraitTextures.Count - 1; + } + + Messages.Message( + "AIImages.Gallery.Deleted".Translate(), + MessageTypeDefOf.PositiveEvent + ); + }, + destructive: true + ) + ); + } + + /// + /// Удаляет все изображения + /// + private void DeleteAllImages() + { + // Подтверждение + Find.WindowStack.Add( + Dialog_MessageBox.CreateConfirmation( + "AIImages.Gallery.ConfirmDeleteAll".Translate(portraitPaths.Count), + delegate + { + // Удаляем все файлы + int deletedCount = 0; + foreach (var path in portraitPaths) + { + if (File.Exists(path)) + { + try + { + File.Delete(path); + deletedCount++; + } + catch (System.Exception ex) + { + DebugLogger.Warning( + $"[AI Gallery] Failed to delete file: {path}, Error: {ex.Message}" + ); + } + } + } + + // Очищаем компонент пешки + PawnPortraitHelper.ClearPortrait(pawn); + + // Обновляем галерею + LoadGallery(); + selectedIndex = 0; + + Messages.Message( + "AIImages.Gallery.AllDeleted".Translate(deletedCount), + MessageTypeDefOf.PositiveEvent + ); + }, + destructive: true + ) + ); + } + } +} diff --git a/Source/AIImages/Window_AIImage.cs b/Source/AIImages/Window_AIImage.cs index 2e8cce9..6b5f9e9 100644 --- a/Source/AIImages/Window_AIImage.cs +++ b/Source/AIImages/Window_AIImage.cs @@ -1293,8 +1293,8 @@ namespace AIImages ? "AIImages.Generation.Cancel".Translate() : "AIImages.Generation.Generate".Translate(); - // Основная кнопка генерации (занимает 70% ширины) - float buttonWidth = rect.width * 0.7f; + // Основная кнопка генерации (занимает 40% ширины) + float buttonWidth = rect.width * 0.4f; if (Widgets.ButtonText(new Rect(rect.x, curY, buttonWidth, 35f), buttonLabel)) { if (isGenerating) @@ -1303,9 +1303,25 @@ namespace AIImages StartGeneration(); } - // Отладочная кнопка (занимает 25% ширины) - float debugButtonWidth = rect.width * 0.25f; - float debugButtonX = rect.x + buttonWidth + 10f; + // Кнопка галереи + float galleryButtonWidth = rect.width * 0.35f; + float galleryButtonX = rect.x + buttonWidth + 10f; + int imageCount = PawnPortraitHelper.GetPortraitCount(pawn); + string galleryLabel = "AIImages.Gallery.OpenGallery".Translate(); + if (imageCount > 0) + { + galleryLabel += " " + "AIImages.Gallery.ImagesCount".Translate(imageCount); + } + + if (Widgets.ButtonText(new Rect(galleryButtonX, curY, galleryButtonWidth, 35f), galleryLabel)) + { + var galleryWindow = new Window_AIGallery(pawn); + Find.WindowStack.Add(galleryWindow); + } + + // Отладочная кнопка (занимает 20% ширины) + float debugButtonWidth = rect.width * 0.2f; + float debugButtonX = galleryButtonX + galleryButtonWidth + 10f; if (Widgets.ButtonText(new Rect(debugButtonX, curY, debugButtonWidth, 35f), "Debug")) { DebugAllPawns();