afae62c5 by 柴进

打包windows版本内容

1 parent 15a2dba1
...@@ -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)
......