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();