打包windows版本内容
Showing
7 changed files
with
1093 additions
and
44 deletions
| ... | @@ -10,7 +10,6 @@ tags: [openspec, change] | ... | @@ -10,7 +10,6 @@ tags: [openspec, change] |
| 10 | - Keep changes tightly scoped to the requested outcome. | 10 | - Keep changes tightly scoped to the requested outcome. |
| 11 | - Refer to `openspec/AGENTS.md` (located inside the `openspec/` directory—run `ls openspec` or `openspec update` if you don't see it) if you need additional OpenSpec conventions or clarifications. | 11 | - Refer to `openspec/AGENTS.md` (located inside the `openspec/` directory—run `ls openspec` or `openspec update` if you don't see it) if you need additional OpenSpec conventions or clarifications. |
| 12 | - Identify any vague or ambiguous details and ask the necessary follow-up questions before editing files. | 12 | - Identify any vague or ambiguous details and ask the necessary follow-up questions before editing files. |
| 13 | - Do not write any code during the proposal stage. Only create design documents (proposal.md, tasks.md, design.md, and spec deltas). Implementation happens in the apply stage after approval. | ||
| 14 | 13 | ||
| 15 | **Steps** | 14 | **Steps** |
| 16 | 1. Review `openspec/project.md`, run `openspec list` and `openspec list --specs`, and inspect related code or docs (e.g., via `rg`/`ls`) to ground the proposal in current behaviour; note any gaps that require clarification. | 15 | 1. Review `openspec/project.md`, run `openspec list` and `openspec list --specs`, and inspect related code or docs (e.g., via `rg`/`ls`) to ground the proposal in current behaviour; note any gaps that require clarification. | ... | ... |
| ... | @@ -4,7 +4,7 @@ | ... | @@ -4,7 +4,7 @@ |
| 4 | <content url="file://$MODULE_DIR$"> | 4 | <content url="file://$MODULE_DIR$"> |
| 5 | <excludeFolder url="file://$MODULE_DIR$/.venv" /> | 5 | <excludeFolder url="file://$MODULE_DIR$/.venv" /> |
| 6 | </content> | 6 | </content> |
| 7 | <orderEntry type="jdk" jdkName="Python 3.9 (GoogleNanoBananaApp) (2)" jdkType="Python SDK" /> | 7 | <orderEntry type="jdk" jdkName="Python 3.11 (GoogleNanoBananaApp)" jdkType="Python SDK" /> |
| 8 | <orderEntry type="sourceFolder" forTests="false" /> | 8 | <orderEntry type="sourceFolder" forTests="false" /> |
| 9 | </component> | 9 | </component> |
| 10 | <component name="PyDocumentationSettings"> | 10 | <component name="PyDocumentationSettings"> | ... | ... |
| ... | @@ -3,5 +3,5 @@ | ... | @@ -3,5 +3,5 @@ |
| 3 | <component name="Black"> | 3 | <component name="Black"> |
| 4 | <option name="sdkName" value="Python 3.9 (GoogleNanoBananaApp) (2)" /> | 4 | <option name="sdkName" value="Python 3.9 (GoogleNanoBananaApp) (2)" /> |
| 5 | </component> | 5 | </component> |
| 6 | <component name="ProjectRootManager" version="2" project-jdk-name="Python 3.9 (GoogleNanoBananaApp) (2)" project-jdk-type="Python SDK" /> | 6 | <component name="ProjectRootManager" version="2" project-jdk-name="Python 3.11 (GoogleNanoBananaApp)" project-jdk-type="Python SDK" /> |
| 7 | </project> | 7 | </project> |
| ... | \ No newline at end of file | ... | \ No newline at end of file | ... | ... |
| ... | @@ -5,11 +5,7 @@ a = Analysis( | ... | @@ -5,11 +5,7 @@ a = Analysis( |
| 5 | ['image_generator.py'], | 5 | ['image_generator.py'], |
| 6 | pathex=[], | 6 | pathex=[], |
| 7 | binaries=[], | 7 | binaries=[], |
| 8 | datas=[ | 8 | datas=[('config.json', '.'), ('zb100_windows.ico', '.')], |
| 9 | ('config.json', '.'), | ||
| 10 | ('zb100_windows.ico', '.'), | ||
| 11 | ('zb100_mac.icns', '.') | ||
| 12 | ], | ||
| 13 | hiddenimports=[], | 9 | hiddenimports=[], |
| 14 | hookspath=[], | 10 | hookspath=[], |
| 15 | hooksconfig={}, | 11 | hooksconfig={}, | ... | ... |
| ... | @@ -33,6 +33,7 @@ pyinstaller --name="ZB100ImageGenerator" ^ | ... | @@ -33,6 +33,7 @@ pyinstaller --name="ZB100ImageGenerator" ^ |
| 33 | --windowed ^ | 33 | --windowed ^ |
| 34 | --icon=zb100_windows.ico ^ | 34 | --icon=zb100_windows.ico ^ |
| 35 | --add-data "config.json;." ^ | 35 | --add-data "config.json;." ^ |
| 36 | --add-data "zb100_windows.ico;." ^ | ||
| 36 | image_generator.py | 37 | image_generator.py |
| 37 | 38 | ||
| 38 | REM Check if build was successful | 39 | REM Check if build was successful | ... | ... |
| ... | @@ -15,5 +15,13 @@ | ... | @@ -15,5 +15,13 @@ |
| 15 | "table": "nano_banana_users" | 15 | "table": "nano_banana_users" |
| 16 | }, | 16 | }, |
| 17 | "last_user": "testuser", | 17 | "last_user": "testuser", |
| 18 | "saved_password_hash": "50630320e4a550f2dba371820dad9d9301d456d101aca4d5ad8f4f3bcc9c1ed9" | 18 | "saved_password_hash": "50630320e4a550f2dba371820dad9d9301d456d101aca4d5ad8f4f3bcc9c1ed9", |
| 19 | "logging_config": { | ||
| 20 | "enabled": true, | ||
| 21 | "level": "INFO", | ||
| 22 | "log_to_console": true | ||
| 23 | }, | ||
| 24 | "history_config": { | ||
| 25 | "max_history_count": 100 | ||
| 26 | } | ||
| 19 | } | 27 | } |
| ... | \ No newline at end of file | ... | \ No newline at end of file | ... | ... |
| ... | @@ -8,10 +8,12 @@ from PySide6.QtWidgets import ( | ... | @@ -8,10 +8,12 @@ from PySide6.QtWidgets import ( |
| 8 | QApplication, QMainWindow, QDialog, QWidget, | 8 | QApplication, QMainWindow, QDialog, QWidget, |
| 9 | QVBoxLayout, QHBoxLayout, QFormLayout, QGridLayout, | 9 | QVBoxLayout, QHBoxLayout, QFormLayout, QGridLayout, |
| 10 | QLabel, QLineEdit, QPushButton, QCheckBox, QTextEdit, | 10 | QLabel, QLineEdit, QPushButton, QCheckBox, QTextEdit, |
| 11 | QComboBox, QScrollArea, QGroupBox, QFileDialog, QMessageBox | 11 | QComboBox, QScrollArea, QGroupBox, QFileDialog, QMessageBox, |
| 12 | QListWidget, QListWidgetItem, QTabWidget, QSplitter, | ||
| 13 | QMenu, QProgressBar | ||
| 12 | ) | 14 | ) |
| 13 | from PySide6.QtCore import Qt, QThread, Signal, QSize | 15 | from PySide6.QtCore import Qt, QThread, Signal, QSize, QTimer |
| 14 | from PySide6.QtGui import QPixmap, QFont, QIcon, QDesktopServices | 16 | from PySide6.QtGui import QPixmap, QFont, QIcon, QDesktopServices, QAction, QImage |
| 15 | from PySide6.QtCore import QUrl | 17 | from PySide6.QtCore import QUrl |
| 16 | 18 | ||
| 17 | import base64 | 19 | import base64 |
| ... | @@ -22,6 +24,7 @@ import sys | ... | @@ -22,6 +24,7 @@ import sys |
| 22 | import shutil | 24 | import shutil |
| 23 | import tempfile | 25 | import tempfile |
| 24 | import platform | 26 | import platform |
| 27 | import logging | ||
| 25 | from pathlib import Path | 28 | from pathlib import Path |
| 26 | from google import genai | 29 | from google import genai |
| 27 | from google.genai import types | 30 | from google.genai import types |
| ... | @@ -30,6 +33,73 @@ import pymysql | ... | @@ -30,6 +33,73 @@ import pymysql |
| 30 | import socket | 33 | import socket |
| 31 | import requests | 34 | import requests |
| 32 | from datetime import datetime | 35 | from datetime import datetime |
| 36 | from dataclasses import dataclass, asdict | ||
| 37 | from typing import List, Optional, Dict, Any | ||
| 38 | |||
| 39 | |||
| 40 | def init_logging(log_level=logging.INFO): | ||
| 41 | """ | ||
| 42 | 初始化简单的日志系统 | ||
| 43 | 创建logs目录并配置基本的文件日志记录 | ||
| 44 | """ | ||
| 45 | try: | ||
| 46 | # 获取脚本所在目录 | ||
| 47 | script_dir = Path(__file__).parent | ||
| 48 | |||
| 49 | # 尝试加载日志配置 | ||
| 50 | config_path = script_dir / "config.json" | ||
| 51 | logging_config = { | ||
| 52 | "enabled": True, | ||
| 53 | "level": "INFO", | ||
| 54 | "log_to_console": True | ||
| 55 | } | ||
| 56 | |||
| 57 | if config_path.exists(): | ||
| 58 | try: | ||
| 59 | with open(config_path, 'r', encoding='utf-8') as f: | ||
| 60 | config = json.load(f) | ||
| 61 | logging_config = config.get("logging_config", logging_config) | ||
| 62 | except Exception as e: | ||
| 63 | print(f"加载日志配置失败: {e}") | ||
| 64 | |||
| 65 | # 如果日志被禁用,直接返回成功 | ||
| 66 | if not logging_config.get("enabled", True): | ||
| 67 | print("日志系统已禁用") | ||
| 68 | return True | ||
| 69 | |||
| 70 | # 解析日志级别 | ||
| 71 | level_str = logging_config.get("level", "INFO").upper() | ||
| 72 | log_level = getattr(logging, level_str, logging.INFO) | ||
| 73 | |||
| 74 | # 创建logs目录 | ||
| 75 | logs_dir = script_dir / "logs" | ||
| 76 | logs_dir.mkdir(exist_ok=True) | ||
| 77 | |||
| 78 | # 配置日志文件路径 | ||
| 79 | log_file = logs_dir / "app.log" | ||
| 80 | |||
| 81 | # 配置日志格式 | ||
| 82 | log_format = "%(asctime)s - %(name)s - %(levelname)s - %(message)s" | ||
| 83 | |||
| 84 | # 配置处理器 | ||
| 85 | handlers = [logging.FileHandler(log_file, encoding='utf-8')] | ||
| 86 | if logging_config.get("log_to_console", True): | ||
| 87 | handlers.append(logging.StreamHandler()) | ||
| 88 | |||
| 89 | # 配置日志系统 | ||
| 90 | logging.basicConfig( | ||
| 91 | level=log_level, | ||
| 92 | format=log_format, | ||
| 93 | handlers=handlers, | ||
| 94 | force=True # 强制重新配置 | ||
| 95 | ) | ||
| 96 | |||
| 97 | logging.info(f"日志系统初始化完成 - 级别: {level_str}, 文件: {log_file}") | ||
| 98 | return True | ||
| 99 | |||
| 100 | except Exception as e: | ||
| 101 | print(f"日志系统初始化失败: {e}") | ||
| 102 | return False | ||
| 33 | 103 | ||
| 34 | 104 | ||
| 35 | def hash_password(password: str) -> str: | 105 | def hash_password(password: str) -> str: |
| ... | @@ -41,6 +111,7 @@ class DatabaseManager: | ... | @@ -41,6 +111,7 @@ class DatabaseManager: |
| 41 | """数据库连接管理类""" | 111 | """数据库连接管理类""" |
| 42 | def __init__(self, db_config): | 112 | def __init__(self, db_config): |
| 43 | self.config = db_config | 113 | self.config = db_config |
| 114 | self.logger = logging.getLogger(__name__) | ||
| 44 | 115 | ||
| 45 | def authenticate(self, username, password): | 116 | def authenticate(self, username, password): |
| 46 | """ | 117 | """ |
| ... | @@ -48,10 +119,13 @@ class DatabaseManager: | ... | @@ -48,10 +119,13 @@ class DatabaseManager: |
| 48 | 返回: (success: bool, message: str) | 119 | 返回: (success: bool, message: str) |
| 49 | """ | 120 | """ |
| 50 | try: | 121 | try: |
| 122 | self.logger.info(f"开始用户认证: {username}") | ||
| 123 | |||
| 51 | # 计算密码哈希 | 124 | # 计算密码哈希 |
| 52 | password_hash = hash_password(password) | 125 | password_hash = hash_password(password) |
| 53 | 126 | ||
| 54 | # 连接数据库 | 127 | # 连接数据库 |
| 128 | self.logger.debug(f"连接数据库: {self.config['host']}:{self.config.get('port', 3306)}") | ||
| 55 | conn = pymysql.connect( | 129 | conn = pymysql.connect( |
| 56 | host=self.config['host'], | 130 | host=self.config['host'], |
| 57 | port=self.config.get('port', 3306), | 131 | port=self.config.get('port', 3306), |
| ... | @@ -69,16 +143,325 @@ class DatabaseManager: | ... | @@ -69,16 +143,325 @@ class DatabaseManager: |
| 69 | result = cursor.fetchone() | 143 | result = cursor.fetchone() |
| 70 | 144 | ||
| 71 | if result: | 145 | if result: |
| 146 | self.logger.info(f"用户认证成功: {username}") | ||
| 72 | return True, "认证成功" | 147 | return True, "认证成功" |
| 73 | else: | 148 | else: |
| 149 | self.logger.warning(f"用户认证失败: {username} - 用户名或密码错误") | ||
| 74 | return False, "用户名或密码错误" | 150 | return False, "用户名或密码错误" |
| 75 | finally: | 151 | finally: |
| 76 | conn.close() | 152 | conn.close() |
| 77 | 153 | ||
| 78 | except pymysql.OperationalError as e: | 154 | except pymysql.OperationalError as e: |
| 79 | return False, "无法连接到服务器,请检查网络连接" | 155 | error_msg = "无法连接到服务器,请检查网络连接" |
| 156 | self.logger.error(f"数据库连接失败: {e}") | ||
| 157 | return False, error_msg | ||
| 158 | except Exception as e: | ||
| 159 | error_msg = f"认证失败: {str(e)}" | ||
| 160 | self.logger.error(f"认证过程异常: {e}") | ||
| 161 | return False, error_msg | ||
| 162 | |||
| 163 | |||
| 164 | @dataclass | ||
| 165 | class HistoryItem: | ||
| 166 | """历史记录项数据结构""" | ||
| 167 | timestamp: str | ||
| 168 | prompt: str | ||
| 169 | generated_image_path: Path | ||
| 170 | reference_image_paths: List[Path] | ||
| 171 | aspect_ratio: str | ||
| 172 | image_size: str | ||
| 173 | model: str | ||
| 174 | created_at: datetime | ||
| 175 | |||
| 176 | def to_dict(self) -> Dict[str, Any]: | ||
| 177 | """转换为字典格式""" | ||
| 178 | return { | ||
| 179 | 'timestamp': self.timestamp, | ||
| 180 | 'prompt': self.prompt, | ||
| 181 | 'generated_image_path': str(self.generated_image_path), | ||
| 182 | 'reference_image_paths': [str(p) for p in self.reference_image_paths], | ||
| 183 | 'aspect_ratio': self.aspect_ratio, | ||
| 184 | 'image_size': self.image_size, | ||
| 185 | 'model': self.model, | ||
| 186 | 'created_at': self.created_at.isoformat() | ||
| 187 | } | ||
| 188 | |||
| 189 | @classmethod | ||
| 190 | def from_dict(cls, data: Dict[str, Any]) -> 'HistoryItem': | ||
| 191 | """从字典创建实例""" | ||
| 192 | return cls( | ||
| 193 | timestamp=data['timestamp'], | ||
| 194 | prompt=data['prompt'], | ||
| 195 | generated_image_path=Path(data['generated_image_path']), | ||
| 196 | reference_image_paths=[Path(p) for p in data['reference_image_paths']], | ||
| 197 | aspect_ratio=data['aspect_ratio'], | ||
| 198 | image_size=data['image_size'], | ||
| 199 | model=data['model'], | ||
| 200 | created_at=datetime.fromisoformat(data['created_at']) | ||
| 201 | ) | ||
| 202 | |||
| 203 | |||
| 204 | def get_app_data_path() -> Path: | ||
| 205 | """获取应用数据存储路径 - 智能选择,优先使用当前目录""" | ||
| 206 | |||
| 207 | # 定义多个备选路径,按优先级排序 | ||
| 208 | def get_candidate_paths(): | ||
| 209 | """获取候选路径列表""" | ||
| 210 | system = platform.system() | ||
| 211 | candidates = [] | ||
| 212 | |||
| 213 | # 1. 优先尝试:当前目录的images文件夹 | ||
| 214 | if getattr(sys, 'frozen', False): | ||
| 215 | # 打包环境:使用可执行文件所在目录 | ||
| 216 | candidates.append(Path(sys.executable).parent / "images") | ||
| 217 | else: | ||
| 218 | # 开发环境:使用脚本所在目录 | ||
| 219 | candidates.append(Path(__file__).parent / "images") | ||
| 220 | |||
| 221 | # 2. 备选方案:用户目录 | ||
| 222 | if system == "Darwin": # macOS | ||
| 223 | candidates.append(Path.home() / "Library/Application Support/ZB100ImageGenerator/images") | ||
| 224 | candidates.append(Path.home() / "Documents/ZB100ImageGenerator/images") | ||
| 225 | elif system == "Windows": | ||
| 226 | candidates.append(Path(os.environ.get("APPDATA", "")) / "ZB100ImageGenerator/images") | ||
| 227 | candidates.append(Path.home() / "Documents/ZB100ImageGenerator/images") | ||
| 228 | else: # Linux | ||
| 229 | candidates.append(Path.home() / ".config/zb100imagegenerator/images") | ||
| 230 | candidates.append(Path.home() / "Documents/ZB100ImageGenerator/images") | ||
| 231 | |||
| 232 | return candidates | ||
| 233 | |||
| 234 | # 测试路径可用性 | ||
| 235 | def test_path_write_access(path: Path) -> bool: | ||
| 236 | """测试路径是否有写入权限""" | ||
| 237 | try: | ||
| 238 | # 尝试创建目录 | ||
| 239 | path.mkdir(parents=True, exist_ok=True) | ||
| 240 | |||
| 241 | # 测试写入权限 | ||
| 242 | test_file = path / ".write_test" | ||
| 243 | test_file.write_text("test") | ||
| 244 | test_file.unlink() # 删除测试文件 | ||
| 245 | |||
| 246 | return True | ||
| 247 | except (PermissionError, OSError) as e: | ||
| 248 | print(f"路径 {path} 无写入权限: {e}") | ||
| 249 | return False | ||
| 250 | except Exception as e: | ||
| 251 | print(f"路径 {path} 测试失败: {e}") | ||
| 252 | return False | ||
| 253 | |||
| 254 | # 按优先级测试每个候选路径 | ||
| 255 | candidates = get_candidate_paths() | ||
| 256 | |||
| 257 | for path in candidates: | ||
| 258 | if test_path_write_access(path): | ||
| 259 | print(f"使用图片存储路径: {path}") | ||
| 260 | return path | ||
| 261 | |||
| 262 | # 如果所有路径都失败,使用最后的备选方案 | ||
| 263 | fallback_path = get_candidate_paths()[0] # 使用第一个候选路径 | ||
| 264 | try: | ||
| 265 | fallback_path.mkdir(parents=True, exist_ok=True) | ||
| 266 | print(f"使用备选路径: {fallback_path}") | ||
| 267 | return fallback_path | ||
| 268 | except Exception as e: | ||
| 269 | print(f"警告: 无法创建存储路径,将在当前目录操作: {e}") | ||
| 270 | return Path.cwd() / "images" | ||
| 271 | |||
| 272 | |||
| 273 | class HistoryManager: | ||
| 274 | """历史记录管理器""" | ||
| 275 | |||
| 276 | def __init__(self, base_path: Optional[Path] = None): | ||
| 277 | """初始化历史记录管理器 | ||
| 278 | |||
| 279 | Args: | ||
| 280 | base_path: 历史记录存储基础路径,默认使用get_app_data_path()的结果 | ||
| 281 | """ | ||
| 282 | self.logger = logging.getLogger(__name__) | ||
| 283 | self.base_path = base_path or get_app_data_path() | ||
| 284 | self.base_path.mkdir(parents=True, exist_ok=True) | ||
| 285 | self.history_index_file = self.base_path / "history_index.json" | ||
| 286 | self.max_history_count = 100 # 默认最大历史记录数量 | ||
| 287 | |||
| 288 | self.logger.debug(f"历史记录管理器初始化完成,存储路径: {self.base_path}") | ||
| 289 | |||
| 290 | def save_generation(self, image_bytes: bytes, prompt: str, reference_images: List[bytes], | ||
| 291 | aspect_ratio: str, image_size: str, model: str) -> str: | ||
| 292 | """保存生成的图片到历史记录 | ||
| 293 | |||
| 294 | Args: | ||
| 295 | image_bytes: 生成的图片字节数据 | ||
| 296 | prompt: 使用的提示词 | ||
| 297 | reference_images: 参考图片字节数据列表 | ||
| 298 | aspect_ratio: 宽高比 | ||
| 299 | image_size: 图片尺寸 | ||
| 300 | model: 使用的模型 | ||
| 301 | |||
| 302 | Returns: | ||
| 303 | 历史记录的时间戳 | ||
| 304 | """ | ||
| 305 | self.logger.info(f"开始保存历史记录 - 模型: {model}, 尺寸: {image_size}") | ||
| 306 | |||
| 307 | # 生成时间戳目录 | ||
| 308 | timestamp = datetime.now().strftime("%Y%m%d%H%M%S") | ||
| 309 | record_dir = self.base_path / timestamp | ||
| 310 | record_dir.mkdir(exist_ok=True) | ||
| 311 | |||
| 312 | # 保存生成的图片 | ||
| 313 | generated_image_path = record_dir / "generated.png" | ||
| 314 | with open(generated_image_path, 'wb') as f: | ||
| 315 | f.write(image_bytes) | ||
| 316 | |||
| 317 | # 保存参考图片 | ||
| 318 | reference_image_paths = [] | ||
| 319 | for i, ref_img_bytes in enumerate(reference_images): | ||
| 320 | ref_path = record_dir / f"reference_{i+1}.png" | ||
| 321 | with open(ref_path, 'wb') as f: | ||
| 322 | f.write(ref_img_bytes) | ||
| 323 | reference_image_paths.append(ref_path) | ||
| 324 | |||
| 325 | # 保存元数据 | ||
| 326 | metadata = { | ||
| 327 | 'timestamp': timestamp, | ||
| 328 | 'prompt': prompt, | ||
| 329 | 'aspect_ratio': aspect_ratio, | ||
| 330 | 'image_size': image_size, | ||
| 331 | 'model': model, | ||
| 332 | 'created_at': datetime.now().isoformat() | ||
| 333 | } | ||
| 334 | |||
| 335 | metadata_path = record_dir / "metadata.json" | ||
| 336 | with open(metadata_path, 'w', encoding='utf-8') as f: | ||
| 337 | json.dump(metadata, f, ensure_ascii=False, indent=2) | ||
| 338 | |||
| 339 | # 更新历史记录索引 | ||
| 340 | history_item = HistoryItem( | ||
| 341 | timestamp=timestamp, | ||
| 342 | prompt=prompt, | ||
| 343 | generated_image_path=generated_image_path, | ||
| 344 | reference_image_paths=reference_image_paths, | ||
| 345 | aspect_ratio=aspect_ratio, | ||
| 346 | image_size=image_size, | ||
| 347 | model=model, | ||
| 348 | created_at=datetime.now() | ||
| 349 | ) | ||
| 350 | |||
| 351 | self._update_history_index(history_item) | ||
| 352 | |||
| 353 | # 清理旧记录 | ||
| 354 | self._cleanup_old_records() | ||
| 355 | |||
| 356 | return timestamp | ||
| 357 | |||
| 358 | def load_history_index(self) -> List[HistoryItem]: | ||
| 359 | """加载历史记录索引 | ||
| 360 | |||
| 361 | Returns: | ||
| 362 | 历史记录项列表,按时间戳倒序排列 | ||
| 363 | """ | ||
| 364 | if not self.history_index_file.exists(): | ||
| 365 | return [] | ||
| 366 | |||
| 367 | try: | ||
| 368 | with open(self.history_index_file, 'r', encoding='utf-8') as f: | ||
| 369 | data = json.load(f) | ||
| 370 | |||
| 371 | history_items = [HistoryItem.from_dict(item) for item in data] | ||
| 372 | # 按时间戳倒序排列 | ||
| 373 | history_items.sort(key=lambda x: x.timestamp, reverse=True) | ||
| 374 | return history_items | ||
| 375 | except Exception as e: | ||
| 376 | print(f"加载历史记录索引失败: {e}") | ||
| 377 | return [] | ||
| 378 | |||
| 379 | def get_history_item(self, timestamp: str) -> Optional[HistoryItem]: | ||
| 380 | """获取指定时间戳的历史记录项 | ||
| 381 | |||
| 382 | Args: | ||
| 383 | timestamp: 时间戳 | ||
| 384 | |||
| 385 | Returns: | ||
| 386 | 历史记录项,如果不存在则返回None | ||
| 387 | """ | ||
| 388 | history_items = self.load_history_index() | ||
| 389 | for item in history_items: | ||
| 390 | if item.timestamp == timestamp: | ||
| 391 | return item | ||
| 392 | return None | ||
| 393 | |||
| 394 | def delete_history_item(self, timestamp: str) -> bool: | ||
| 395 | """删除指定的历史记录 | ||
| 396 | |||
| 397 | Args: | ||
| 398 | timestamp: 要删除的时间戳 | ||
| 399 | |||
| 400 | Returns: | ||
| 401 | 删除是否成功 | ||
| 402 | """ | ||
| 403 | try: | ||
| 404 | # 删除文件目录 | ||
| 405 | record_dir = self.base_path / timestamp | ||
| 406 | if record_dir.exists(): | ||
| 407 | shutil.rmtree(record_dir) | ||
| 408 | |||
| 409 | # 更新索引文件 | ||
| 410 | history_items = self.load_history_index() | ||
| 411 | history_items = [item for item in history_items if item.timestamp != timestamp] | ||
| 412 | self._save_history_index(history_items) | ||
| 413 | |||
| 414 | return True | ||
| 415 | except Exception as e: | ||
| 416 | print(f"删除历史记录失败: {e}") | ||
| 417 | return False | ||
| 418 | |||
| 419 | def _update_history_index(self, history_item: HistoryItem): | ||
| 420 | """更新历史记录索引 | ||
| 421 | |||
| 422 | Args: | ||
| 423 | history_item: 要添加的历史记录项 | ||
| 424 | """ | ||
| 425 | history_items = self.load_history_index() | ||
| 426 | |||
| 427 | # 检查是否已存在相同时间戳的记录,如果存在则替换 | ||
| 428 | history_items = [item for item in history_items if item.timestamp != history_item.timestamp] | ||
| 429 | history_items.insert(0, history_item) # 插入到开头 | ||
| 430 | |||
| 431 | self._save_history_index(history_items) | ||
| 432 | |||
| 433 | def _save_history_index(self, history_items: List[HistoryItem]): | ||
| 434 | """保存历史记录索引到文件 | ||
| 435 | |||
| 436 | Args: | ||
| 437 | history_items: 历史记录项列表 | ||
| 438 | """ | ||
| 439 | try: | ||
| 440 | data = [item.to_dict() for item in history_items] | ||
| 441 | with open(self.history_index_file, 'w', encoding='utf-8') as f: | ||
| 442 | json.dump(data, f, ensure_ascii=False, indent=2) | ||
| 443 | except Exception as e: | ||
| 444 | print(f"保存历史记录索引失败: {e}") | ||
| 445 | |||
| 446 | def _cleanup_old_records(self): | ||
| 447 | """清理旧的历史记录,保持最大数量限制""" | ||
| 448 | history_items = self.load_history_index() | ||
| 449 | if len(history_items) > self.max_history_count: | ||
| 450 | # 保留最新的记录 | ||
| 451 | items_to_keep = history_items[:self.max_history_count] | ||
| 452 | items_to_remove = history_items[self.max_history_count:] | ||
| 453 | |||
| 454 | # 删除多余记录的文件 | ||
| 455 | for item in items_to_remove: | ||
| 456 | record_dir = self.base_path / item.timestamp | ||
| 457 | if record_dir.exists(): | ||
| 458 | try: | ||
| 459 | shutil.rmtree(record_dir) | ||
| 80 | except Exception as e: | 460 | except Exception as e: |
| 81 | return False, f"认证失败: {str(e)}" | 461 | print(f"删除旧历史记录失败 {item.timestamp}: {e}") |
| 462 | |||
| 463 | # 更新索引文件 | ||
| 464 | self._save_history_index(items_to_keep) | ||
| 82 | 465 | ||
| 83 | 466 | ||
| 84 | class LoginDialog(QDialog): | 467 | class LoginDialog(QDialog): |
| ... | @@ -101,6 +484,9 @@ class LoginDialog(QDialog): | ... | @@ -101,6 +484,9 @@ class LoginDialog(QDialog): |
| 101 | 484 | ||
| 102 | def set_window_icon(self): | 485 | def set_window_icon(self): |
| 103 | """Set window icon based on platform""" | 486 | """Set window icon based on platform""" |
| 487 | try: | ||
| 488 | icon_path = None | ||
| 489 | |||
| 104 | if getattr(sys, 'frozen', False): | 490 | if getattr(sys, 'frozen', False): |
| 105 | # Running as compiled executable | 491 | # Running as compiled executable |
| 106 | if platform.system() == 'Windows': | 492 | if platform.system() == 'Windows': |
| ... | @@ -108,22 +494,24 @@ class LoginDialog(QDialog): | ... | @@ -108,22 +494,24 @@ class LoginDialog(QDialog): |
| 108 | elif platform.system() == 'Darwin': | 494 | elif platform.system() == 'Darwin': |
| 109 | icon_path = os.path.join(sys._MEIPASS, 'zb100_mac.icns') | 495 | icon_path = os.path.join(sys._MEIPASS, 'zb100_mac.icns') |
| 110 | else: | 496 | else: |
| 111 | icon_path = None | ||
| 112 | else: | ||
| 113 | # Running as script | 497 | # Running as script |
| 114 | if platform.system() == 'Windows': | 498 | if platform.system() == 'Windows': |
| 115 | icon_path = 'zb100_windows.ico' | 499 | icon_path = 'zb100_windows.ico' |
| 116 | elif platform.system() == 'Darwin': | 500 | elif platform.system() == 'Darwin': |
| 117 | icon_path = 'zb100_mac.icns' | 501 | icon_path = 'zb100_mac.icns' |
| 118 | else: | ||
| 119 | icon_path = None | ||
| 120 | 502 | ||
| 121 | if icon_path and os.path.exists(icon_path): | 503 | if icon_path and os.path.exists(icon_path): |
| 122 | self.setWindowIcon(QIcon(icon_path)) | 504 | app_icon = QIcon(icon_path) |
| 505 | if not app_icon.isNull(): | ||
| 506 | self.setWindowIcon(app_icon) | ||
| 507 | else: | ||
| 508 | print(f"警告:图标文件无效: {icon_path}") | ||
| 509 | except Exception as e: | ||
| 510 | print(f"设置窗口图标失败: {e}") | ||
| 123 | 511 | ||
| 124 | def setup_ui(self): | 512 | def setup_ui(self): |
| 125 | """Build login dialog UI""" | 513 | """Build login dialog UI""" |
| 126 | self.setWindowTitle("登录 - AI 图像生成器") | 514 | self.setWindowTitle("登录 - 珠宝壹佰图像生成器") |
| 127 | self.setFixedSize(400, 400) | 515 | self.setFixedSize(400, 400) |
| 128 | 516 | ||
| 129 | # Main layout | 517 | # Main layout |
| ... | @@ -447,6 +835,9 @@ class ImageGeneratorWindow(QMainWindow): | ... | @@ -447,6 +835,9 @@ class ImageGeneratorWindow(QMainWindow): |
| 447 | 835 | ||
| 448 | def __init__(self): | 836 | def __init__(self): |
| 449 | super().__init__() | 837 | super().__init__() |
| 838 | self.logger = logging.getLogger(__name__) | ||
| 839 | self.logger.info("应用程序启动") | ||
| 840 | |||
| 450 | self.api_key = "" | 841 | self.api_key = "" |
| 451 | self.uploaded_images = [] # List of file paths | 842 | self.uploaded_images = [] # List of file paths |
| 452 | self.generated_image_data = None | 843 | self.generated_image_data = None |
| ... | @@ -458,11 +849,20 @@ class ImageGeneratorWindow(QMainWindow): | ... | @@ -458,11 +849,20 @@ class ImageGeneratorWindow(QMainWindow): |
| 458 | 849 | ||
| 459 | self.load_config() | 850 | self.load_config() |
| 460 | self.set_window_icon() | 851 | self.set_window_icon() |
| 852 | |||
| 853 | # Initialize history manager | ||
| 854 | self.history_manager = HistoryManager() | ||
| 855 | |||
| 461 | self.setup_ui() | 856 | self.setup_ui() |
| 462 | self.apply_styles() | 857 | self.apply_styles() |
| 463 | 858 | ||
| 859 | self.logger.info("应用程序初始化完成") | ||
| 860 | |||
| 464 | def set_window_icon(self): | 861 | def set_window_icon(self): |
| 465 | """Set window icon based on platform""" | 862 | """Set window icon based on platform""" |
| 863 | try: | ||
| 864 | icon_path = None | ||
| 865 | |||
| 466 | if getattr(sys, 'frozen', False): | 866 | if getattr(sys, 'frozen', False): |
| 467 | # Running as compiled executable | 867 | # Running as compiled executable |
| 468 | if platform.system() == 'Windows': | 868 | if platform.system() == 'Windows': |
| ... | @@ -470,18 +870,24 @@ class ImageGeneratorWindow(QMainWindow): | ... | @@ -470,18 +870,24 @@ class ImageGeneratorWindow(QMainWindow): |
| 470 | elif platform.system() == 'Darwin': | 870 | elif platform.system() == 'Darwin': |
| 471 | icon_path = os.path.join(sys._MEIPASS, 'zb100_mac.icns') | 871 | icon_path = os.path.join(sys._MEIPASS, 'zb100_mac.icns') |
| 472 | else: | 872 | else: |
| 473 | icon_path = None | ||
| 474 | else: | ||
| 475 | # Running as script | 873 | # Running as script |
| 476 | if platform.system() == 'Windows': | 874 | if platform.system() == 'Windows': |
| 477 | icon_path = 'zb100_windows.ico' | 875 | icon_path = 'zb100_windows.ico' |
| 478 | elif platform.system() == 'Darwin': | 876 | elif platform.system() == 'Darwin': |
| 479 | icon_path = 'zb100_mac.icns' | 877 | icon_path = 'zb100_mac.icns' |
| 480 | else: | ||
| 481 | icon_path = None | ||
| 482 | 878 | ||
| 483 | if icon_path and os.path.exists(icon_path): | 879 | if icon_path and os.path.exists(icon_path): |
| 484 | self.setWindowIcon(QIcon(icon_path)) | 880 | app_icon = QIcon(icon_path) |
| 881 | if not app_icon.isNull(): | ||
| 882 | self.setWindowIcon(app_icon) | ||
| 883 | self.logger.debug(f"主窗口图标设置成功: {icon_path}") | ||
| 884 | else: | ||
| 885 | self.logger.warning(f"图标文件无效: {icon_path}") | ||
| 886 | else: | ||
| 887 | self.logger.debug(f"图标文件不存在,跳过设置: {icon_path}") | ||
| 888 | |||
| 889 | except Exception as e: | ||
| 890 | self.logger.error(f"设置窗口图标失败: {e}") | ||
| 485 | 891 | ||
| 486 | def get_config_dir(self): | 892 | def get_config_dir(self): |
| 487 | """Get the appropriate directory for config files based on platform and mode""" | 893 | """Get the appropriate directory for config files based on platform and mode""" |
| ... | @@ -506,7 +912,10 @@ class ImageGeneratorWindow(QMainWindow): | ... | @@ -506,7 +912,10 @@ class ImageGeneratorWindow(QMainWindow): |
| 506 | def load_config(self): | 912 | def load_config(self): |
| 507 | """Load API key, saved prompts, and db config from config file""" | 913 | """Load API key, saved prompts, and db config from config file""" |
| 508 | config_path = self.get_config_path() | 914 | config_path = self.get_config_path() |
| 915 | self.logger.debug(f"加载配置文件: {config_path}") | ||
| 509 | 916 | ||
| 917 | # Try to load from user directory first | ||
| 918 | config_loaded = False | ||
| 510 | if config_path.exists(): | 919 | if config_path.exists(): |
| 511 | try: | 920 | try: |
| 512 | with open(config_path, 'r', encoding='utf-8') as f: | 921 | with open(config_path, 'r', encoding='utf-8') as f: |
| ... | @@ -516,9 +925,80 @@ class ImageGeneratorWindow(QMainWindow): | ... | @@ -516,9 +925,80 @@ class ImageGeneratorWindow(QMainWindow): |
| 516 | self.db_config = config.get("db_config") | 925 | self.db_config = config.get("db_config") |
| 517 | self.last_user = config.get("last_user", "") | 926 | self.last_user = config.get("last_user", "") |
| 518 | self.saved_password_hash = config.get("saved_password_hash", "") | 927 | self.saved_password_hash = config.get("saved_password_hash", "") |
| 928 | |||
| 929 | # Load history configuration | ||
| 930 | history_config = config.get("history_config", {}) | ||
| 931 | if hasattr(self, 'history_manager'): | ||
| 932 | self.history_manager.max_history_count = history_config.get("max_history_count", 100) | ||
| 933 | |||
| 934 | self.logger.info("配置文件加载成功") | ||
| 935 | config_loaded = True | ||
| 519 | except Exception as e: | 936 | except Exception as e: |
| 937 | self.logger.error(f"配置文件加载失败: {e}") | ||
| 520 | print(f"Failed to load config from {config_path}: {e}") | 938 | print(f"Failed to load config from {config_path}: {e}") |
| 521 | 939 | ||
| 940 | # If user config doesn't exist or failed to load, try bundled config | ||
| 941 | if not config_loaded and getattr(sys, 'frozen', False): | ||
| 942 | bundled_config_paths = [ | ||
| 943 | Path(sys.executable).parent / 'config.json', # Same directory as exe | ||
| 944 | Path(sys._MEIPASS) / 'config.json', # PyInstaller temp directory | ||
| 945 | ] | ||
| 946 | |||
| 947 | for bundled_path in bundled_config_paths: | ||
| 948 | if bundled_path.exists(): | ||
| 949 | try: | ||
| 950 | with open(bundled_path, 'r', encoding='utf-8') as f: | ||
| 951 | config = json.load(f) | ||
| 952 | self.api_key = config.get("api_key", "") | ||
| 953 | self.saved_prompts = config.get("saved_prompts", []) | ||
| 954 | self.db_config = config.get("db_config") | ||
| 955 | self.last_user = config.get("last_user", "") | ||
| 956 | self.saved_password_hash = config.get("saved_password_hash", "") | ||
| 957 | |||
| 958 | # Load history configuration | ||
| 959 | history_config = config.get("history_config", {}) | ||
| 960 | if hasattr(self, 'history_manager'): | ||
| 961 | self.history_manager.max_history_count = history_config.get("max_history_count", 100) | ||
| 962 | |||
| 963 | self.logger.info(f"从打包配置文件加载成功: {bundled_path}") | ||
| 964 | config_loaded = True | ||
| 965 | break | ||
| 966 | except Exception as e: | ||
| 967 | self.logger.error(f"打包配置文件加载失败 {bundled_path}: {e}") | ||
| 968 | continue | ||
| 969 | |||
| 970 | # If still no config loaded, try current directory | ||
| 971 | if not config_loaded: | ||
| 972 | current_config = Path('.') / 'config.json' | ||
| 973 | if current_config.exists(): | ||
| 974 | try: | ||
| 975 | with open(current_config, 'r', encoding='utf-8') as f: | ||
| 976 | config = json.load(f) | ||
| 977 | self.api_key = config.get("api_key", "") | ||
| 978 | self.saved_prompts = config.get("saved_prompts", []) | ||
| 979 | self.db_config = config.get("db_config") | ||
| 980 | self.last_user = config.get("last_user", "") | ||
| 981 | self.saved_password_hash = config.get("saved_password_hash", "") | ||
| 982 | |||
| 983 | # Load history configuration | ||
| 984 | history_config = config.get("history_config", {}) | ||
| 985 | if hasattr(self, 'history_manager'): | ||
| 986 | self.history_manager.max_history_count = history_config.get("max_history_count", 100) | ||
| 987 | |||
| 988 | self.logger.info(f"从当前目录配置文件加载成功: {current_config}") | ||
| 989 | config_loaded = True | ||
| 990 | except Exception as e: | ||
| 991 | self.logger.error(f"当前目录配置文件加载失败: {e}") | ||
| 992 | |||
| 993 | if not config_loaded: | ||
| 994 | self.logger.warning("未找到任何有效的配置文件") | ||
| 995 | print("警告:未找到配置文件,某些功能可能无法正常工作") | ||
| 996 | |||
| 997 | # 即使没有db_config也继续运行,让用户在UI中配置 | ||
| 998 | if not self.db_config: | ||
| 999 | self.logger.info("未找到数据库配置,将使用UI配置模式") | ||
| 1000 | self.db_config = None | ||
| 1001 | |||
| 522 | if not self.api_key and getattr(sys, 'frozen', False): | 1002 | if not self.api_key and getattr(sys, 'frozen', False): |
| 523 | try: | 1003 | try: |
| 524 | bundle_dir = Path(sys._MEIPASS) | 1004 | bundle_dir = Path(sys._MEIPASS) |
| ... | @@ -560,6 +1040,12 @@ class ImageGeneratorWindow(QMainWindow): | ... | @@ -560,6 +1040,12 @@ class ImageGeneratorWindow(QMainWindow): |
| 560 | else: | 1040 | else: |
| 561 | config["saved_password_hash"] = "" | 1041 | config["saved_password_hash"] = "" |
| 562 | 1042 | ||
| 1043 | # Save history configuration | ||
| 1044 | if hasattr(self, 'history_manager'): | ||
| 1045 | config["history_config"] = { | ||
| 1046 | "max_history_count": self.history_manager.max_history_count | ||
| 1047 | } | ||
| 1048 | |||
| 563 | config_path.parent.mkdir(parents=True, exist_ok=True) | 1049 | config_path.parent.mkdir(parents=True, exist_ok=True) |
| 564 | 1050 | ||
| 565 | with open(config_path, 'w', encoding='utf-8') as f: | 1051 | with open(config_path, 'w', encoding='utf-8') as f: |
| ... | @@ -569,7 +1055,7 @@ class ImageGeneratorWindow(QMainWindow): | ... | @@ -569,7 +1055,7 @@ class ImageGeneratorWindow(QMainWindow): |
| 569 | 1055 | ||
| 570 | def setup_ui(self): | 1056 | def setup_ui(self): |
| 571 | """Setup the user interface""" | 1057 | """Setup the user interface""" |
| 572 | self.setWindowTitle("AI 图像生成器") | 1058 | self.setWindowTitle("珠宝壹佰图像生成器") |
| 573 | self.setGeometry(100, 100, 1200, 850) | 1059 | self.setGeometry(100, 100, 1200, 850) |
| 574 | self.setMinimumSize(1000, 700) | 1060 | self.setMinimumSize(1000, 700) |
| 575 | 1061 | ||
| ... | @@ -578,7 +1064,30 @@ class ImageGeneratorWindow(QMainWindow): | ... | @@ -578,7 +1064,30 @@ class ImageGeneratorWindow(QMainWindow): |
| 578 | self.setCentralWidget(central_widget) | 1064 | self.setCentralWidget(central_widget) |
| 579 | 1065 | ||
| 580 | main_layout = QVBoxLayout() | 1066 | main_layout = QVBoxLayout() |
| 581 | main_layout.setContentsMargins(20, 20, 20, 20) | 1067 | main_layout.setContentsMargins(10, 10, 10, 10) |
| 1068 | main_layout.setSpacing(10) | ||
| 1069 | |||
| 1070 | # Create tab widget | ||
| 1071 | self.tab_widget = QTabWidget() | ||
| 1072 | |||
| 1073 | # Create generation tab | ||
| 1074 | generation_tab = self.setup_generation_tab() | ||
| 1075 | self.tab_widget.addTab(generation_tab, "图片生成") | ||
| 1076 | |||
| 1077 | # Create history tab | ||
| 1078 | history_tab = self.setup_history_tab() | ||
| 1079 | self.tab_widget.addTab(history_tab, "历史记录") | ||
| 1080 | |||
| 1081 | main_layout.addWidget(self.tab_widget) | ||
| 1082 | central_widget.setLayout(main_layout) | ||
| 1083 | |||
| 1084 | self.check_favorite_status() | ||
| 1085 | |||
| 1086 | def setup_generation_tab(self): | ||
| 1087 | """Setup the image generation tab""" | ||
| 1088 | tab_widget = QWidget() | ||
| 1089 | main_layout = QVBoxLayout() | ||
| 1090 | main_layout.setContentsMargins(10, 10, 10, 10) | ||
| 582 | main_layout.setSpacing(15) | 1091 | main_layout.setSpacing(15) |
| 583 | 1092 | ||
| 584 | # Reference images section | 1093 | # Reference images section |
| ... | @@ -666,6 +1175,8 @@ class ImageGeneratorWindow(QMainWindow): | ... | @@ -666,6 +1175,8 @@ class ImageGeneratorWindow(QMainWindow): |
| 666 | self.image_size.setCurrentIndex(1) # Default to 2K | 1175 | self.image_size.setCurrentIndex(1) # Default to 2K |
| 667 | settings_layout.addWidget(self.image_size) | 1176 | settings_layout.addWidget(self.image_size) |
| 668 | 1177 | ||
| 1178 | settings_layout.addSpacing(10) | ||
| 1179 | |||
| 669 | settings_layout.addStretch() | 1180 | settings_layout.addStretch() |
| 670 | settings_group.setLayout(settings_layout) | 1181 | settings_group.setLayout(settings_layout) |
| 671 | content_row.addWidget(settings_group, 1) | 1182 | content_row.addWidget(settings_group, 1) |
| ... | @@ -703,9 +1214,160 @@ class ImageGeneratorWindow(QMainWindow): | ... | @@ -703,9 +1214,160 @@ class ImageGeneratorWindow(QMainWindow): |
| 703 | preview_group.setLayout(preview_layout) | 1214 | preview_group.setLayout(preview_layout) |
| 704 | main_layout.addWidget(preview_group, 1) | 1215 | main_layout.addWidget(preview_group, 1) |
| 705 | 1216 | ||
| 706 | central_widget.setLayout(main_layout) | 1217 | tab_widget.setLayout(main_layout) |
| 1218 | return tab_widget | ||
| 707 | 1219 | ||
| 708 | self.check_favorite_status() | 1220 | def setup_history_tab(self): |
| 1221 | """Setup the history tab""" | ||
| 1222 | tab_widget = QWidget() | ||
| 1223 | main_layout = QVBoxLayout() | ||
| 1224 | main_layout.setContentsMargins(10, 10, 10, 10) | ||
| 1225 | main_layout.setSpacing(10) | ||
| 1226 | |||
| 1227 | # History toolbar | ||
| 1228 | toolbar_layout = QHBoxLayout() | ||
| 1229 | |||
| 1230 | refresh_btn = QPushButton("🔄 刷新") | ||
| 1231 | refresh_btn.clicked.connect(self.refresh_history) | ||
| 1232 | toolbar_layout.addWidget(refresh_btn) | ||
| 1233 | |||
| 1234 | clear_btn = QPushButton("🗑️ 清空历史") | ||
| 1235 | clear_btn.clicked.connect(self.clear_history) | ||
| 1236 | toolbar_layout.addWidget(clear_btn) | ||
| 1237 | |||
| 1238 | toolbar_layout.addStretch() | ||
| 1239 | |||
| 1240 | self.history_count_label = QLabel("共 0 条历史记录") | ||
| 1241 | toolbar_layout.addWidget(self.history_count_label) | ||
| 1242 | |||
| 1243 | main_layout.addLayout(toolbar_layout) | ||
| 1244 | |||
| 1245 | # Create splitter for list and details | ||
| 1246 | from PySide6.QtWidgets import QSplitter | ||
| 1247 | splitter = QSplitter(Qt.Vertical) | ||
| 1248 | |||
| 1249 | # History list (upper part) | ||
| 1250 | self.history_list = QListWidget() | ||
| 1251 | self.history_list.setIconSize(QSize(120, 120)) | ||
| 1252 | self.history_list.setResizeMode(QListWidget.Adjust) | ||
| 1253 | self.history_list.setViewMode(QListWidget.IconMode) | ||
| 1254 | self.history_list.setSpacing(10) | ||
| 1255 | self.history_list.setMinimumHeight(200) # Give more space for history list | ||
| 1256 | self.history_list.itemClicked.connect(self.load_history_item) | ||
| 1257 | self.history_list.setContextMenuPolicy(Qt.CustomContextMenu) | ||
| 1258 | self.history_list.customContextMenuRequested.connect(self.show_history_context_menu) | ||
| 1259 | |||
| 1260 | splitter.addWidget(self.history_list) | ||
| 1261 | |||
| 1262 | # Details panel (lower part) | ||
| 1263 | self.details_panel = self.create_details_panel() | ||
| 1264 | splitter.addWidget(self.details_panel) | ||
| 1265 | |||
| 1266 | # Set splitter proportions (40% for list, 60% for details) | ||
| 1267 | splitter.setSizes([300, 450]) | ||
| 1268 | splitter.setChildrenCollapsible(False) # Prevent panels from being collapsed completely | ||
| 1269 | |||
| 1270 | main_layout.addWidget(splitter) | ||
| 1271 | tab_widget.setLayout(main_layout) | ||
| 1272 | |||
| 1273 | # Load initial history | ||
| 1274 | self.refresh_history() | ||
| 1275 | |||
| 1276 | return tab_widget | ||
| 1277 | |||
| 1278 | def create_details_panel(self): | ||
| 1279 | """Create the details panel for displaying selected history item""" | ||
| 1280 | panel = QWidget() | ||
| 1281 | layout = QVBoxLayout() | ||
| 1282 | layout.setContentsMargins(10, 10, 10, 10) | ||
| 1283 | layout.setSpacing(10) | ||
| 1284 | |||
| 1285 | # Prompt section | ||
| 1286 | prompt_group = QGroupBox("提示词") | ||
| 1287 | prompt_layout = QVBoxLayout() | ||
| 1288 | |||
| 1289 | # Prompt text area with copy button | ||
| 1290 | prompt_header = QHBoxLayout() | ||
| 1291 | prompt_header.addWidget(QLabel("完整提示词:")) | ||
| 1292 | self.copy_prompt_btn = QPushButton("📋 复制") | ||
| 1293 | self.copy_prompt_btn.clicked.connect(self.copy_prompt_text) | ||
| 1294 | self.copy_prompt_btn.setEnabled(False) | ||
| 1295 | prompt_header.addWidget(self.copy_prompt_btn) | ||
| 1296 | prompt_header.addStretch() | ||
| 1297 | |||
| 1298 | prompt_layout.addLayout(prompt_header) | ||
| 1299 | |||
| 1300 | self.prompt_display = QLabel("请选择一个历史记录查看详情") | ||
| 1301 | self.prompt_display.setWordWrap(True) | ||
| 1302 | self.prompt_display.setStyleSheet("QLabel { padding: 8px; background-color: #f9f9f9; border: 1px solid #ddd; border-radius: 4px; }") | ||
| 1303 | prompt_layout.addWidget(self.prompt_display) | ||
| 1304 | |||
| 1305 | prompt_group.setLayout(prompt_layout) | ||
| 1306 | layout.addWidget(prompt_group) | ||
| 1307 | |||
| 1308 | # Parameters section - compressed to one line | ||
| 1309 | params_group = QGroupBox("生成参数") | ||
| 1310 | params_layout = QHBoxLayout() | ||
| 1311 | |||
| 1312 | params_layout.addWidget(QLabel("生成时间:")) | ||
| 1313 | self.time_label = QLabel("-") | ||
| 1314 | params_layout.addWidget(self.time_label) | ||
| 1315 | |||
| 1316 | params_layout.addWidget(QLabel(" | 宽高比:")) | ||
| 1317 | self.aspect_ratio_label = QLabel("-") | ||
| 1318 | params_layout.addWidget(self.aspect_ratio_label) | ||
| 1319 | |||
| 1320 | params_layout.addWidget(QLabel(" | 图片尺寸:")) | ||
| 1321 | self.image_size_label = QLabel("-") | ||
| 1322 | params_layout.addWidget(self.image_size_label) | ||
| 1323 | |||
| 1324 | params_layout.addStretch() | ||
| 1325 | params_group.setLayout(params_layout) | ||
| 1326 | layout.addWidget(params_group) | ||
| 1327 | |||
| 1328 | # Images section - left (reference) and right (generated) layout | ||
| 1329 | images_group = QGroupBox("图片预览") | ||
| 1330 | images_layout = QHBoxLayout() | ||
| 1331 | |||
| 1332 | # Left side - Reference images | ||
| 1333 | ref_group = QGroupBox("参考图片") | ||
| 1334 | ref_layout = QVBoxLayout() | ||
| 1335 | |||
| 1336 | self.ref_images_scroll = QScrollArea() | ||
| 1337 | self.ref_images_scroll.setWidgetResizable(True) | ||
| 1338 | self.ref_images_scroll.setMinimumHeight(200) | ||
| 1339 | self.ref_images_widget = QWidget() | ||
| 1340 | self.ref_images_layout = QVBoxLayout() # Changed to vertical for better layout | ||
| 1341 | self.ref_images_layout.setAlignment(Qt.AlignCenter) | ||
| 1342 | self.ref_images_widget.setLayout(self.ref_images_layout) | ||
| 1343 | self.ref_images_scroll.setWidget(self.ref_images_widget) | ||
| 1344 | |||
| 1345 | ref_layout.addWidget(self.ref_images_scroll) | ||
| 1346 | ref_group.setLayout(ref_layout) | ||
| 1347 | images_layout.addWidget(ref_group, 1) # 1:1 stretch | ||
| 1348 | |||
| 1349 | # Right side - Generated image (larger) | ||
| 1350 | gen_group = QGroupBox("生成图片") | ||
| 1351 | gen_layout = QVBoxLayout() | ||
| 1352 | gen_layout.setAlignment(Qt.AlignCenter) | ||
| 1353 | |||
| 1354 | self.generated_image_label = QLabel("请选择一个历史记录查看生成图片") | ||
| 1355 | self.generated_image_label.setAlignment(Qt.AlignCenter) | ||
| 1356 | self.generated_image_label.setMinimumSize(200, 200) # Larger size for generated image | ||
| 1357 | self.generated_image_label.setMaximumSize(300, 300) | ||
| 1358 | self.generated_image_label.setStyleSheet("QLabel { background-color: #f5f5f5; border: 1px solid #ddd; color: #999; }") | ||
| 1359 | self.generated_image_label.mouseDoubleClickEvent = self.open_generated_image_from_history | ||
| 1360 | |||
| 1361 | gen_layout.addWidget(self.generated_image_label) | ||
| 1362 | gen_group.setLayout(gen_layout) | ||
| 1363 | images_layout.addWidget(gen_group, 1) # 1:1 stretch | ||
| 1364 | |||
| 1365 | images_group.setLayout(images_layout) | ||
| 1366 | layout.addWidget(images_group) | ||
| 1367 | |||
| 1368 | layout.addStretch() | ||
| 1369 | panel.setLayout(layout) | ||
| 1370 | return panel | ||
| 709 | 1371 | ||
| 710 | def apply_styles(self): | 1372 | def apply_styles(self): |
| 711 | """Apply QSS stylesheet""" | 1373 | """Apply QSS stylesheet""" |
| ... | @@ -921,7 +1583,8 @@ class ImageGeneratorWindow(QMainWindow): | ... | @@ -921,7 +1583,8 @@ class ImageGeneratorWindow(QMainWindow): |
| 921 | self.prompt_text.toPlainText().strip(), | 1583 | self.prompt_text.toPlainText().strip(), |
| 922 | self.uploaded_images, | 1584 | self.uploaded_images, |
| 923 | self.aspect_ratio.currentText(), | 1585 | self.aspect_ratio.currentText(), |
| 924 | self.image_size.currentText() | 1586 | self.image_size.currentText(), |
| 1587 | "gemini-3-pro-image-preview" # 锁死模型 | ||
| 925 | ) | 1588 | ) |
| 926 | self.worker.finished.connect(self.on_image_generated) | 1589 | self.worker.finished.connect(self.on_image_generated) |
| 927 | self.worker.error.connect(self.on_generation_error) | 1590 | self.worker.error.connect(self.on_generation_error) |
| ... | @@ -934,7 +1597,7 @@ class ImageGeneratorWindow(QMainWindow): | ... | @@ -934,7 +1597,7 @@ class ImageGeneratorWindow(QMainWindow): |
| 934 | 1597 | ||
| 935 | self.worker.start() | 1598 | self.worker.start() |
| 936 | 1599 | ||
| 937 | def on_image_generated(self, image_bytes): | 1600 | def on_image_generated(self, image_bytes, prompt, reference_images, aspect_ratio, image_size, model): |
| 938 | """Handle successful image generation""" | 1601 | """Handle successful image generation""" |
| 939 | self.generated_image_bytes = image_bytes | 1602 | self.generated_image_bytes = image_bytes |
| 940 | self.display_image() | 1603 | self.display_image() |
| ... | @@ -943,6 +1606,23 @@ class ImageGeneratorWindow(QMainWindow): | ... | @@ -943,6 +1606,23 @@ class ImageGeneratorWindow(QMainWindow): |
| 943 | self.status_label.setText("● 图片生成成功") | 1606 | self.status_label.setText("● 图片生成成功") |
| 944 | self.status_label.setStyleSheet("QLabel { color: #34C759; }") | 1607 | self.status_label.setStyleSheet("QLabel { color: #34C759; }") |
| 945 | 1608 | ||
| 1609 | # 自动保存到历史记录 | ||
| 1610 | try: | ||
| 1611 | self.history_manager.save_generation( | ||
| 1612 | image_bytes=image_bytes, | ||
| 1613 | prompt=prompt, | ||
| 1614 | reference_images=reference_images, | ||
| 1615 | aspect_ratio=aspect_ratio, | ||
| 1616 | image_size=image_size, | ||
| 1617 | model=model | ||
| 1618 | ) | ||
| 1619 | self.status_label.setText("● 图片生成成功,已保存到历史记录") | ||
| 1620 | # 刷新历史记录列表 | ||
| 1621 | self.refresh_history() | ||
| 1622 | except Exception as e: | ||
| 1623 | print(f"保存到历史记录失败: {e}") | ||
| 1624 | # 不影响主要功能,静默处理错误 | ||
| 1625 | |||
| 946 | def on_generation_error(self, error_msg): | 1626 | def on_generation_error(self, error_msg): |
| 947 | """Handle image generation error""" | 1627 | """Handle image generation error""" |
| 948 | QMessageBox.critical(self, "错误", f"生成失败: {error_msg}") | 1628 | QMessageBox.critical(self, "错误", f"生成失败: {error_msg}") |
| ... | @@ -1028,32 +1708,358 @@ class ImageGeneratorWindow(QMainWindow): | ... | @@ -1028,32 +1708,358 @@ class ImageGeneratorWindow(QMainWindow): |
| 1028 | QMessageBox.critical(self, "错误", f"保存失败: {str(e)}") | 1708 | QMessageBox.critical(self, "错误", f"保存失败: {str(e)}") |
| 1029 | 1709 | ||
| 1030 | 1710 | ||
| 1711 | def refresh_history(self): | ||
| 1712 | """Refresh the history list""" | ||
| 1713 | self.history_list.clear() | ||
| 1714 | history_items = self.history_manager.load_history_index() | ||
| 1715 | |||
| 1716 | for item in history_items: | ||
| 1717 | # Create list item with icon | ||
| 1718 | list_item = QListWidgetItem() | ||
| 1719 | |||
| 1720 | # Set item data | ||
| 1721 | list_item.setData(Qt.UserRole, item.timestamp) | ||
| 1722 | |||
| 1723 | # Create enhanced tooltip with details | ||
| 1724 | tooltip = f"时间: {item.created_at.strftime('%Y-%m-%d %H:%M:%S')}\n" | ||
| 1725 | tooltip += f"提示词: {item.prompt}\n" | ||
| 1726 | tooltip += f"宽高比: {item.aspect_ratio}\n" | ||
| 1727 | tooltip += f"尺寸: {item.image_size}" | ||
| 1728 | list_item.setToolTip(tooltip) | ||
| 1729 | |||
| 1730 | # Try to load thumbnail | ||
| 1731 | if item.generated_image_path.exists(): | ||
| 1732 | try: | ||
| 1733 | pixmap = QPixmap(str(item.generated_image_path)) | ||
| 1734 | if not pixmap.isNull(): | ||
| 1735 | # Scale to thumbnail size | ||
| 1736 | scaled_pixmap = pixmap.scaled(120, 120, Qt.KeepAspectRatio, Qt.SmoothTransformation) | ||
| 1737 | list_item.setIcon(QIcon(scaled_pixmap)) | ||
| 1738 | else: | ||
| 1739 | # Create placeholder icon | ||
| 1740 | list_item.setIcon(self.create_placeholder_icon("图片\n加载失败")) | ||
| 1741 | except Exception as e: | ||
| 1742 | print(f"Failed to load thumbnail for {item.timestamp}: {e}") | ||
| 1743 | list_item.setIcon(self.create_placeholder_icon("图片\n错误")) | ||
| 1744 | else: | ||
| 1745 | list_item.setIcon(self.create_placeholder_icon("图片\n不存在")) | ||
| 1746 | |||
| 1747 | # Add text info below the icon | ||
| 1748 | # Get prompt preview (first 20 characters) | ||
| 1749 | prompt_preview = item.prompt[:20] + "..." if len(item.prompt) > 20 else item.prompt | ||
| 1750 | list_item.setText(f"{item.timestamp}\n{prompt_preview}") | ||
| 1751 | |||
| 1752 | # Add to list | ||
| 1753 | self.history_list.addItem(list_item) | ||
| 1754 | |||
| 1755 | # Update count label | ||
| 1756 | self.history_count_label.setText(f"共 {len(history_items)} 条历史记录") | ||
| 1757 | |||
| 1758 | # Clear details panel if no items | ||
| 1759 | if not history_items: | ||
| 1760 | self.clear_details_panel() | ||
| 1761 | |||
| 1762 | def create_placeholder_icon(self, text): | ||
| 1763 | """Create a placeholder icon with text""" | ||
| 1764 | # Create a 120x120 pixmap | ||
| 1765 | pixmap = QPixmap(120, 120) | ||
| 1766 | pixmap.fill(Qt.lightGray) | ||
| 1767 | |||
| 1768 | # Create a painter to draw text | ||
| 1769 | from PySide6.QtGui import QPainter, QFont | ||
| 1770 | painter = QPainter(pixmap) | ||
| 1771 | painter.setPen(Qt.black) | ||
| 1772 | painter.setFont(QFont("Arial", 10)) | ||
| 1773 | |||
| 1774 | # Draw text in center | ||
| 1775 | rect = pixmap.rect() | ||
| 1776 | painter.drawText(rect, Qt.AlignCenter, text) | ||
| 1777 | |||
| 1778 | painter.end() | ||
| 1779 | return QIcon(pixmap) | ||
| 1780 | |||
| 1781 | def load_history_item(self, item): | ||
| 1782 | """Display history item details when selected""" | ||
| 1783 | timestamp = item.data(Qt.UserRole) | ||
| 1784 | if not timestamp: | ||
| 1785 | return | ||
| 1786 | |||
| 1787 | history_item = self.history_manager.get_history_item(timestamp) | ||
| 1788 | if not history_item: | ||
| 1789 | return | ||
| 1790 | |||
| 1791 | # Display details in the details panel | ||
| 1792 | self.display_history_details(history_item) | ||
| 1793 | |||
| 1794 | def show_history_context_menu(self, position): | ||
| 1795 | """Show context menu for history items""" | ||
| 1796 | item = self.history_list.itemAt(position) | ||
| 1797 | if not item: | ||
| 1798 | return | ||
| 1799 | |||
| 1800 | timestamp = item.data(Qt.UserRole) | ||
| 1801 | if not timestamp: | ||
| 1802 | return | ||
| 1803 | |||
| 1804 | # Create context menu | ||
| 1805 | menu = QMenu(self) | ||
| 1806 | |||
| 1807 | # Delete action | ||
| 1808 | delete_action = QAction("删除此项", self) | ||
| 1809 | delete_action.triggered.connect(lambda: self.delete_history_item(timestamp)) | ||
| 1810 | menu.addAction(delete_action) | ||
| 1811 | |||
| 1812 | # Open in file manager action | ||
| 1813 | open_action = QAction("在文件管理器中显示", self) | ||
| 1814 | open_action.triggered.connect(lambda: self.open_in_file_manager(timestamp)) | ||
| 1815 | menu.addAction(open_action) | ||
| 1816 | |||
| 1817 | # Show menu | ||
| 1818 | menu.exec_(self.history_list.mapToGlobal(position)) | ||
| 1819 | |||
| 1820 | def delete_history_item(self, timestamp): | ||
| 1821 | """Delete a history item""" | ||
| 1822 | reply = QMessageBox.question( | ||
| 1823 | self, "确认删除", "确定要删除这条历史记录吗?\n这将删除相关的所有文件。", | ||
| 1824 | QMessageBox.Yes | QMessageBox.No, QMessageBox.No | ||
| 1825 | ) | ||
| 1826 | |||
| 1827 | if reply == QMessageBox.Yes: | ||
| 1828 | success = self.history_manager.delete_history_item(timestamp) | ||
| 1829 | if success: | ||
| 1830 | self.refresh_history() | ||
| 1831 | self.status_label.setText("● 历史记录已删除") | ||
| 1832 | self.status_label.setStyleSheet("QLabel { color: #34C759; }") | ||
| 1833 | else: | ||
| 1834 | QMessageBox.critical(self, "错误", "删除历史记录失败") | ||
| 1835 | |||
| 1836 | def open_in_file_manager(self, timestamp): | ||
| 1837 | """Open the history item directory in file manager""" | ||
| 1838 | history_item = self.history_manager.get_history_item(timestamp) | ||
| 1839 | if not history_item: | ||
| 1840 | return | ||
| 1841 | |||
| 1842 | record_dir = history_item.generated_image_path.parent | ||
| 1843 | if record_dir.exists(): | ||
| 1844 | import subprocess | ||
| 1845 | import platform | ||
| 1846 | |||
| 1847 | try: | ||
| 1848 | if platform.system() == "Windows": | ||
| 1849 | subprocess.run(["explorer", str(record_dir)]) | ||
| 1850 | elif platform.system() == "Darwin": # macOS | ||
| 1851 | subprocess.run(["open", str(record_dir)]) | ||
| 1852 | else: # Linux | ||
| 1853 | subprocess.run(["xdg-open", str(record_dir)]) | ||
| 1854 | except Exception as e: | ||
| 1855 | QMessageBox.critical(self, "错误", f"无法打开文件管理器: {str(e)}") | ||
| 1856 | |||
| 1857 | def clear_history(self): | ||
| 1858 | """Clear all history""" | ||
| 1859 | reply = QMessageBox.question( | ||
| 1860 | self, "确认清空", "确定要清空所有历史记录吗?\n这将删除所有历史图片文件,且无法恢复。", | ||
| 1861 | QMessageBox.Yes | QMessageBox.No, QMessageBox.No | ||
| 1862 | ) | ||
| 1863 | |||
| 1864 | if reply == QMessageBox.Yes: | ||
| 1865 | try: | ||
| 1866 | # Remove entire history directory | ||
| 1867 | import shutil | ||
| 1868 | if self.history_manager.base_path.exists(): | ||
| 1869 | shutil.rmtree(self.history_manager.base_path) | ||
| 1870 | # Recreate empty directory | ||
| 1871 | self.history_manager.base_path.mkdir(parents=True, exist_ok=True) | ||
| 1872 | |||
| 1873 | self.refresh_history() | ||
| 1874 | self.status_label.setText("● 历史记录已清空") | ||
| 1875 | self.status_label.setStyleSheet("QLabel { color: #34C759; }") | ||
| 1876 | |||
| 1877 | except Exception as e: | ||
| 1878 | QMessageBox.critical(self, "错误", f"清空历史记录失败: {str(e)}") | ||
| 1879 | |||
| 1880 | def display_history_details(self, history_item): | ||
| 1881 | """Display history item details in the details panel""" | ||
| 1882 | try: | ||
| 1883 | # Update prompt display | ||
| 1884 | self.prompt_display.setText(history_item.prompt) | ||
| 1885 | self.copy_prompt_btn.setEnabled(True) | ||
| 1886 | self.current_history_prompt = history_item.prompt # Store for copying | ||
| 1887 | |||
| 1888 | # Update parameters | ||
| 1889 | self.time_label.setText(history_item.created_at.strftime('%Y-%m-%d %H:%M:%S')) | ||
| 1890 | self.aspect_ratio_label.setText(history_item.aspect_ratio) | ||
| 1891 | self.image_size_label.setText(history_item.image_size) | ||
| 1892 | |||
| 1893 | # Display reference images | ||
| 1894 | self.display_reference_images(history_item.reference_image_paths) | ||
| 1895 | |||
| 1896 | # Display generated image | ||
| 1897 | self.display_generated_image(history_item.generated_image_path) | ||
| 1898 | |||
| 1899 | except Exception as e: | ||
| 1900 | print(f"Error displaying history details: {e}") | ||
| 1901 | |||
| 1902 | def display_reference_images(self, reference_paths): | ||
| 1903 | """Display reference images in the details panel with adaptive sizing""" | ||
| 1904 | # Clear existing images | ||
| 1905 | for i in reversed(range(self.ref_images_layout.count())): | ||
| 1906 | child = self.ref_images_layout.itemAt(i).widget() | ||
| 1907 | if child: | ||
| 1908 | child.setParent(None) | ||
| 1909 | |||
| 1910 | if not reference_paths: | ||
| 1911 | no_images_label = QLabel("无参考图片") | ||
| 1912 | no_images_label.setStyleSheet("color: #999;") | ||
| 1913 | self.ref_images_layout.addWidget(no_images_label) | ||
| 1914 | return | ||
| 1915 | |||
| 1916 | # Calculate adaptive size based on number of images | ||
| 1917 | num_images = len(reference_paths) | ||
| 1918 | if num_images == 1: | ||
| 1919 | size = 180 # Single image gets more space | ||
| 1920 | elif num_images == 2: | ||
| 1921 | size = 140 # Two images get medium space | ||
| 1922 | else: | ||
| 1923 | size = 100 # Multiple images get smaller space | ||
| 1924 | |||
| 1925 | for ref_path in reference_paths: | ||
| 1926 | if ref_path.exists(): | ||
| 1927 | try: | ||
| 1928 | pixmap = QPixmap(str(ref_path)) | ||
| 1929 | if not pixmap.isNull(): | ||
| 1930 | # Create adaptive thumbnail | ||
| 1931 | thumbnail = pixmap.scaled(size, size, Qt.KeepAspectRatio, Qt.SmoothTransformation) | ||
| 1932 | image_label = QLabel() | ||
| 1933 | image_label.setPixmap(thumbnail) | ||
| 1934 | image_label.setFixedSize(size, size) | ||
| 1935 | image_label.setAlignment(Qt.AlignCenter) | ||
| 1936 | image_label.setStyleSheet("border: 1px solid #ddd; margin: 5px;") | ||
| 1937 | image_label.mouseDoubleClickEvent = lambda e, path=ref_path: self.open_reference_image(path) | ||
| 1938 | self.ref_images_layout.addWidget(image_label) | ||
| 1939 | except Exception as e: | ||
| 1940 | print(f"Failed to load reference image {ref_path}: {e}") | ||
| 1941 | |||
| 1942 | def display_generated_image(self, image_path): | ||
| 1943 | """Display the generated image in the details panel""" | ||
| 1944 | if image_path.exists(): | ||
| 1945 | try: | ||
| 1946 | pixmap = QPixmap(str(image_path)) | ||
| 1947 | if not pixmap.isNull(): | ||
| 1948 | # Scale to larger size while maintaining aspect ratio | ||
| 1949 | available_size = self.generated_image_label.size() | ||
| 1950 | scaled_pixmap = pixmap.scaled( | ||
| 1951 | available_size.width(), | ||
| 1952 | available_size.height(), | ||
| 1953 | Qt.KeepAspectRatio, | ||
| 1954 | Qt.SmoothTransformation | ||
| 1955 | ) | ||
| 1956 | self.generated_image_label.setPixmap(scaled_pixmap) | ||
| 1957 | self.generated_image_label.setStyleSheet("QLabel { border: 1px solid #ddd; background-color: white; }") | ||
| 1958 | self.current_generated_image_path = image_path | ||
| 1959 | else: | ||
| 1960 | self.generated_image_label.setText("图片加载失败") | ||
| 1961 | self.generated_image_label.setStyleSheet("QLabel { background-color: #f5f5f5; border: 1px solid #ddd; color: #999; }") | ||
| 1962 | except Exception as e: | ||
| 1963 | print(f"Failed to load generated image {image_path}: {e}") | ||
| 1964 | self.generated_image_label.setText("图片加载失败") | ||
| 1965 | self.generated_image_label.setStyleSheet("QLabel { background-color: #f5f5f5; border: 1px solid #ddd; color: #999; }") | ||
| 1966 | else: | ||
| 1967 | self.generated_image_label.setText("图片文件不存在") | ||
| 1968 | self.generated_image_label.setStyleSheet("QLabel { background-color: #f5f5f5; border: 1px solid #ddd; color: #999; }") | ||
| 1969 | |||
| 1970 | def clear_details_panel(self): | ||
| 1971 | """Clear the details panel""" | ||
| 1972 | self.prompt_display.setText("请选择一个历史记录查看详情") | ||
| 1973 | self.copy_prompt_btn.setEnabled(False) | ||
| 1974 | self.time_label.setText("-") | ||
| 1975 | self.aspect_ratio_label.setText("-") | ||
| 1976 | self.image_size_label.setText("-") | ||
| 1977 | |||
| 1978 | # Clear reference images | ||
| 1979 | for i in reversed(range(self.ref_images_layout.count())): | ||
| 1980 | child = self.ref_images_layout.itemAt(i).widget() | ||
| 1981 | if child: | ||
| 1982 | child.setParent(None) | ||
| 1983 | no_images_label = QLabel("无参考图片") | ||
| 1984 | no_images_label.setStyleSheet("color: #999;") | ||
| 1985 | self.ref_images_layout.addWidget(no_images_label) | ||
| 1986 | |||
| 1987 | self.generated_image_label.setText("请选择一个历史记录查看生成图片") | ||
| 1988 | self.generated_image_label.setPixmap(QPixmap()) | ||
| 1989 | self.generated_image_label.setStyleSheet("QLabel { background-color: #f5f5f5; border: 1px solid #ddd; color: #999; }") | ||
| 1990 | |||
| 1991 | def copy_prompt_text(self): | ||
| 1992 | """Copy the prompt text to clipboard""" | ||
| 1993 | if hasattr(self, 'current_history_prompt'): | ||
| 1994 | from PySide6.QtWidgets import QApplication | ||
| 1995 | clipboard = QApplication.clipboard() | ||
| 1996 | clipboard.setText(self.current_history_prompt) | ||
| 1997 | |||
| 1998 | # Show success message briefly | ||
| 1999 | original_text = self.copy_prompt_btn.text() | ||
| 2000 | self.copy_prompt_btn.setText("✅ 已复制") | ||
| 2001 | self.copy_prompt_btn.setStyleSheet("QPushButton { background-color: #34C759; color: white; }") | ||
| 2002 | |||
| 2003 | # Reset after 2 seconds | ||
| 2004 | QTimer.singleShot(2000, lambda: self.reset_copy_button()) | ||
| 2005 | |||
| 2006 | def reset_copy_button(self): | ||
| 2007 | """Reset the copy button appearance""" | ||
| 2008 | self.copy_prompt_btn.setText("📋 复制") | ||
| 2009 | self.copy_prompt_btn.setStyleSheet("") | ||
| 2010 | |||
| 2011 | def open_generated_image_from_history(self, event): | ||
| 2012 | """Open the generated image from history in system viewer""" | ||
| 2013 | if hasattr(self, 'current_generated_image_path') and self.current_generated_image_path: | ||
| 2014 | self.open_image_in_system_viewer(self.current_generated_image_path) | ||
| 2015 | |||
| 2016 | def open_reference_image(self, image_path): | ||
| 2017 | """Open a reference image in system viewer""" | ||
| 2018 | self.open_image_in_system_viewer(image_path) | ||
| 2019 | |||
| 2020 | def open_image_in_system_viewer(self, image_path): | ||
| 2021 | """Open an image in the system default viewer""" | ||
| 2022 | try: | ||
| 2023 | QDesktopServices.openUrl(QUrl.fromLocalFile(str(image_path))) | ||
| 2024 | except Exception as e: | ||
| 2025 | QMessageBox.critical(self, "错误", f"无法打开图片: {str(e)}") | ||
| 2026 | |||
| 2027 | |||
| 1031 | class ImageGenerationWorker(QThread): | 2028 | class ImageGenerationWorker(QThread): |
| 1032 | """Worker thread for image generation""" | 2029 | """Worker thread for image generation""" |
| 1033 | finished = Signal(bytes) | 2030 | finished = Signal(bytes, str, list, str, str, str) # image_bytes, prompt, reference_images, aspect_ratio, image_size, model |
| 1034 | error = Signal(str) | 2031 | error = Signal(str) |
| 1035 | progress = Signal(str) | 2032 | progress = Signal(str) |
| 1036 | 2033 | ||
| 1037 | def __init__(self, api_key, prompt, images, aspect_ratio, image_size): | 2034 | def __init__(self, api_key, prompt, images, aspect_ratio, image_size, model="gemini-3-pro-image-preview"): |
| 1038 | super().__init__() | 2035 | super().__init__() |
| 2036 | self.logger = logging.getLogger(__name__) | ||
| 1039 | self.api_key = api_key | 2037 | self.api_key = api_key |
| 1040 | self.prompt = prompt | 2038 | self.prompt = prompt |
| 1041 | self.images = images | 2039 | self.images = images |
| 1042 | self.aspect_ratio = aspect_ratio | 2040 | self.aspect_ratio = aspect_ratio |
| 1043 | self.image_size = image_size | 2041 | self.image_size = image_size |
| 2042 | self.model = model | ||
| 2043 | |||
| 2044 | self.logger.info(f"图片生成任务初始化 - 模型: {model}, 尺寸: {image_size}, 宽高比: {aspect_ratio}") | ||
| 1044 | 2045 | ||
| 1045 | def run(self): | 2046 | def run(self): |
| 1046 | """Execute image generation in background thread""" | 2047 | """Execute image generation in background thread""" |
| 1047 | try: | 2048 | try: |
| 2049 | self.logger.info("开始图片生成任务") | ||
| 2050 | |||
| 1048 | if not self.prompt: | 2051 | if not self.prompt: |
| 2052 | self.logger.error("图片描述为空") | ||
| 1049 | self.error.emit("请输入图片描述!") | 2053 | self.error.emit("请输入图片描述!") |
| 1050 | return | 2054 | return |
| 1051 | 2055 | ||
| 1052 | if not self.api_key: | 2056 | if not self.api_key: |
| 2057 | self.logger.error("API密钥为空") | ||
| 1053 | self.error.emit("未找到API密钥,请在config.json中配置!") | 2058 | self.error.emit("未找到API密钥,请在config.json中配置!") |
| 1054 | return | 2059 | return |
| 1055 | 2060 | ||
| 1056 | self.progress.emit("正在连接 Gemini API...") | 2061 | self.progress.emit("正在连接 Gemini API...") |
| 2062 | self.logger.debug("正在连接 Gemini API") | ||
| 1057 | 2063 | ||
| 1058 | client = genai.Client(api_key=self.api_key) | 2064 | client = genai.Client(api_key=self.api_key) |
| 1059 | 2065 | ||
| ... | @@ -1102,17 +2108,36 @@ class ImageGenerationWorker(QThread): | ... | @@ -1102,17 +2108,36 @@ class ImageGenerationWorker(QThread): |
| 1102 | else: | 2108 | else: |
| 1103 | image_bytes = base64.b64decode(part.inline_data.data) | 2109 | image_bytes = base64.b64decode(part.inline_data.data) |
| 1104 | 2110 | ||
| 1105 | self.finished.emit(image_bytes) | 2111 | # Convert reference images to bytes for history saving |
| 2112 | reference_images_bytes = [] | ||
| 2113 | for img_path in self.images: | ||
| 2114 | if img_path and os.path.exists(img_path): | ||
| 2115 | with open(img_path, 'rb') as f: | ||
| 2116 | reference_images_bytes.append(f.read()) | ||
| 2117 | else: | ||
| 2118 | reference_images_bytes.append(b'') | ||
| 2119 | |||
| 2120 | self.logger.info(f"图片生成成功 - 模型: {self.model}, 尺寸: {self.image_size}") | ||
| 2121 | self.finished.emit(image_bytes, self.prompt, reference_images_bytes, | ||
| 2122 | self.aspect_ratio, self.image_size, self.model) | ||
| 1106 | return | 2123 | return |
| 1107 | 2124 | ||
| 1108 | self.error.emit("响应中没有图片数据") | 2125 | error_msg = "响应中没有图片数据" |
| 2126 | self.logger.error(error_msg) | ||
| 2127 | self.error.emit(error_msg) | ||
| 1109 | 2128 | ||
| 1110 | except Exception as e: | 2129 | except Exception as e: |
| 1111 | self.error.emit(str(e)) | 2130 | error_msg = f"图片生成异常: {e}" |
| 2131 | self.logger.error(error_msg) | ||
| 2132 | self.error.emit(error_msg) | ||
| 1112 | 2133 | ||
| 1113 | 2134 | ||
| 1114 | def main(): | 2135 | def main(): |
| 1115 | """Main application entry point""" | 2136 | """Main application entry point""" |
| 2137 | # 初始化日志系统 | ||
| 2138 | if not init_logging(): | ||
| 2139 | print("警告:日志系统初始化失败,将继续运行但不记录日志") | ||
| 2140 | |||
| 1116 | # Load config for database info | 2141 | # Load config for database info |
| 1117 | config_dir = Path('.') | 2142 | config_dir = Path('.') |
| 1118 | if getattr(sys, 'frozen', False): | 2143 | if getattr(sys, 'frozen', False): |
| ... | @@ -1127,10 +2152,12 @@ def main(): | ... | @@ -1127,10 +2152,12 @@ def main(): |
| 1127 | config_dir.mkdir(parents=True, exist_ok=True) | 2152 | config_dir.mkdir(parents=True, exist_ok=True) |
| 1128 | config_path = config_dir / 'config.json' | 2153 | config_path = config_dir / 'config.json' |
| 1129 | 2154 | ||
| 1130 | # If config doesn't exist in user directory, copy from bundled resources | 2155 | # Always try to ensure user config exists - copy from bundled if needed |
| 1131 | if not config_path.exists(): | 2156 | if not config_path.exists(): |
| 1132 | if getattr(sys, 'frozen', False): | 2157 | if getattr(sys, 'frozen', False): |
| 1133 | # Running as bundled app - look in Resources folder (macOS .app bundle) | 2158 | # Running as bundled app - look for bundled config |
| 2159 | bundled_config = None | ||
| 2160 | |||
| 1134 | if platform.system() == 'Darwin': | 2161 | if platform.system() == 'Darwin': |
| 1135 | # macOS: Contents/Resources/config.json | 2162 | # macOS: Contents/Resources/config.json |
| 1136 | bundled_config = Path(sys.executable).parent.parent / 'Resources' / 'config.json' | 2163 | bundled_config = Path(sys.executable).parent.parent / 'Resources' / 'config.json' |
| ... | @@ -1138,14 +2165,31 @@ def main(): | ... | @@ -1138,14 +2165,31 @@ def main(): |
| 1138 | # Windows/Linux: same directory as executable | 2165 | # Windows/Linux: same directory as executable |
| 1139 | bundled_config = Path(sys.executable).parent / 'config.json' | 2166 | bundled_config = Path(sys.executable).parent / 'config.json' |
| 1140 | 2167 | ||
| 1141 | if bundled_config.exists(): | 2168 | # Also try _MEIPASS directory (PyInstaller temp directory) |
| 2169 | if not bundled_config.exists(): | ||
| 2170 | meipass_bundled = Path(sys._MEIPASS) / 'config.json' | ||
| 2171 | if meipass_bundled.exists(): | ||
| 2172 | bundled_config = meipass_bundled | ||
| 2173 | |||
| 2174 | if bundled_config and bundled_config.exists(): | ||
| 1142 | try: | 2175 | try: |
| 2176 | # Create config directory if it doesn't exist | ||
| 2177 | config_path.parent.mkdir(parents=True, exist_ok=True) | ||
| 1143 | shutil.copy2(bundled_config, config_path) | 2178 | shutil.copy2(bundled_config, config_path) |
| 1144 | print(f"✓ Copied config from {bundled_config} to {config_path}") | 2179 | print(f"✓ Copied config from {bundled_config} to {config_path}") |
| 1145 | except Exception as e: | 2180 | except Exception as e: |
| 1146 | print(f"✗ Failed to copy bundled config: {e}") | 2181 | print(f"✗ Failed to copy bundled config: {e}") |
| 2182 | # If copy fails, try to use bundled config directly | ||
| 2183 | config_path = bundled_config | ||
| 1147 | else: | 2184 | else: |
| 1148 | print(f"✗ Bundled config not found at {bundled_config}") | 2185 | print(f"✗ Bundled config not found at {bundled_config}") |
| 2186 | # Try to use current directory config as fallback | ||
| 2187 | current_dir_config = Path('.') / 'config.json' | ||
| 2188 | if current_dir_config.exists(): | ||
| 2189 | config_path = current_dir_config | ||
| 2190 | print(f"✓ Using current directory config: {config_path}") | ||
| 2191 | else: | ||
| 2192 | print(f"✗ No config file found at all") | ||
| 1149 | 2193 | ||
| 1150 | db_config = None | 2194 | db_config = None |
| 1151 | last_user = "" | 2195 | last_user = "" |
| ... | @@ -1186,12 +2230,13 @@ def main(): | ... | @@ -1186,12 +2230,13 @@ def main(): |
| 1186 | app_icon = QIcon(icon_path) | 2230 | app_icon = QIcon(icon_path) |
| 1187 | app.setWindowIcon(app_icon) | 2231 | app.setWindowIcon(app_icon) |
| 1188 | 2232 | ||
| 1189 | # Check database config | 2233 | # Check database config - if missing, start app without database authentication |
| 1190 | if not db_config: | 2234 | if not db_config: |
| 1191 | QMessageBox.critical(None, "配置错误", | 2235 | print("警告:未找到数据库配置,将跳过数据库认证") |
| 1192 | f"未找到数据库配置\n配置文件: {config_path}\n\n" | 2236 | # Create main window directly without login |
| 1193 | "请确保 config.json 包含 db_config 字段") | 2237 | main_window = ImageGeneratorWindow() |
| 1194 | return | 2238 | main_window.show() |
| 2239 | sys.exit(app.exec()) | ||
| 1195 | 2240 | ||
| 1196 | # Show login dialog | 2241 | # Show login dialog |
| 1197 | login_dialog = LoginDialog(db_config, last_user, saved_password_hash) | 2242 | login_dialog = LoginDialog(db_config, last_user, saved_password_hash) | ... | ... |
-
Please register or sign in to post a comment