"""AI 图片对话面板:输入提示 → 生成 → 显示 → 列表切换/保存/删除。""" from __future__ import annotations import os import tkinter as tk from tkinter import filedialog, messagebox from typing import List, Optional import ttkbootstrap as ttk from PIL import Image, ImageTk from app.services import ai_image as _svc # ---------------- 面板创建 ---------------- def create_ai_image_panel(self): """创建 AI 图片对话面板,并注册到面板管理。""" frame = ttk.Frame(self.content_frame) self.ai_image_frame = frame # 内部状态 self.ai_image_records: List[_svc.AIImageRecord] = [] self.ai_image_current: Optional[_svc.AIImageRecord] = None self.ai_image_photo = None # 防止 ImageTk.PhotoImage 被 GC self._ai_image_requesting = False container = ttk.Frame(frame, padding=10) container.pack(fill=tk.BOTH, expand=True) # 左列:图片列表 left = ttk.Frame(container) left.pack(side=tk.LEFT, fill=tk.Y, padx=(0, 10)) ttk.Label(left, text="历史图片", font=("微软雅黑", 10, "bold")).pack( anchor=tk.W, pady=(0, 4) ) list_wrap = ttk.Frame(left) list_wrap.pack(fill=tk.Y, expand=False) scroll = ttk.Scrollbar(list_wrap, orient=tk.VERTICAL) self.ai_image_listbox = tk.Listbox( list_wrap, width=28, height=22, activestyle="dotbox", yscrollcommand=scroll.set, ) scroll.config(command=self.ai_image_listbox.yview) self.ai_image_listbox.pack(side=tk.LEFT, fill=tk.Y) scroll.pack(side=tk.RIGHT, fill=tk.Y) self.ai_image_listbox.bind("<>", lambda e: _on_list_select(self)) btn_row = ttk.Frame(left) btn_row.pack(fill=tk.X, pady=(6, 0)) ttk.Button( btn_row, text="保存", bootstyle="success-outline", width=8, command=lambda: _save_current(self), ).pack(side=tk.LEFT, padx=(0, 4)) ttk.Button( btn_row, text="删除", bootstyle="danger-outline", width=8, command=lambda: _delete_current(self), ).pack(side=tk.LEFT, padx=(0, 4)) ttk.Button( btn_row, text="刷新", bootstyle="secondary-outline", width=8, command=lambda: reload_ai_image_list(self), ).pack(side=tk.LEFT) # 右列:预览 + 输入 right = ttk.Frame(container) right.pack(side=tk.LEFT, fill=tk.BOTH, expand=True) preview_frame = ttk.LabelFrame(right, text="图片预览", padding=6) preview_frame.pack(fill=tk.BOTH, expand=True) self.ai_image_canvas = tk.Canvas( preview_frame, bg="#1e1e1e", highlightthickness=0 ) self.ai_image_canvas.pack(fill=tk.BOTH, expand=True) self.ai_image_canvas.bind("", lambda e: _redraw_preview(self)) meta_row = ttk.Frame(right) meta_row.pack(fill=tk.X, pady=(4, 4)) self.ai_image_meta_var = tk.StringVar(value="未选择图片") ttk.Label( meta_row, textvariable=self.ai_image_meta_var, foreground="#666", font=("微软雅黑", 9), ).pack(side=tk.LEFT) # 输入区 input_frame = ttk.LabelFrame(right, text="提示输入(Ctrl+Enter 发送)", padding=6) input_frame.pack(fill=tk.X, pady=(4, 0)) self.ai_image_input = tk.Text(input_frame, height=3, wrap=tk.WORD) self.ai_image_input.pack(fill=tk.X, side=tk.TOP) self.ai_image_input.bind("", lambda e: (_send_prompt(self), "break")) send_row = ttk.Frame(input_frame) send_row.pack(fill=tk.X, pady=(4, 0)) self.ai_image_status_var = tk.StringVar(value="就绪") ttk.Label( send_row, textvariable=self.ai_image_status_var, foreground="#888", font=("微软雅黑", 9), ).pack(side=tk.LEFT) self.ai_image_send_btn = ttk.Button( send_row, text="发送", bootstyle="primary", width=10, command=lambda: _send_prompt(self), ) self.ai_image_send_btn.pack(side=tk.RIGHT) # 注册面板 self.register_panel("ai_image", frame, None, "ai_image_visible") self.ai_image_visible = False # 初次加载缓存 reload_ai_image_list(self) def toggle_ai_image_panel(self): """切换 AI 图片面板显隐。""" self.show_panel("ai_image") # ---------------- 列表 / 选中 ---------------- def reload_ai_image_list(self): """重新扫描缓存并刷新列表。""" self.ai_image_records = _svc.list_records() self.ai_image_listbox.delete(0, tk.END) for rec in self.ai_image_records: label = _format_list_label(rec) self.ai_image_listbox.insert(tk.END, label) if self.ai_image_records: self.ai_image_listbox.selection_clear(0, tk.END) self.ai_image_listbox.selection_set(0) self.ai_image_listbox.activate(0) _select_record(self, self.ai_image_records[0]) else: self.ai_image_current = None self.ai_image_photo = None self.ai_image_canvas.delete("all") self.ai_image_meta_var.set("暂无缓存图片") def _format_list_label(rec: _svc.AIImageRecord) -> str: when = (rec.created_at or "")[:19].replace("T", " ") preview = (rec.prompt or "(无提示)").strip().splitlines()[0] if len(preview) > 18: preview = preview[:18] + "…" return f"{when} {preview}" if when else preview def _on_list_select(self): sel = self.ai_image_listbox.curselection() if not sel: return idx = sel[0] if 0 <= idx < len(self.ai_image_records): _select_record(self, self.ai_image_records[idx]) def _select_record(self, rec: _svc.AIImageRecord): self.ai_image_current = rec self.ai_image_meta_var.set( f"{os.path.basename(rec.image_path)} | {rec.created_at}" ) _redraw_preview(self) # ---------------- 预览绘制 ---------------- def _redraw_preview(self): rec = getattr(self, "ai_image_current", None) canvas = self.ai_image_canvas canvas.delete("all") if rec is None or not os.path.isfile(rec.image_path): return cw = canvas.winfo_width() or 1 ch = canvas.winfo_height() or 1 try: img = Image.open(rec.image_path) except Exception as exc: canvas.create_text(cw // 2, ch // 2, text=f"加载失败: {exc}", fill="#f66") return iw, ih = img.size scale = min(cw / iw, ch / ih, 1.0) nw, nh = max(1, int(iw * scale)), max(1, int(ih * scale)) img_resized = img.resize((nw, nh), Image.LANCZOS) self.ai_image_photo = ImageTk.PhotoImage(img_resized) canvas.create_image(cw // 2, ch // 2, image=self.ai_image_photo, anchor="center") # ---------------- 发送 / 保存 / 删除 ---------------- def _send_prompt(self): if getattr(self, "_ai_image_requesting", False): return prompt = self.ai_image_input.get("1.0", tk.END).strip() if not prompt: messagebox.showinfo("提示", "请输入内容") return if not _svc.has_api(): messagebox.showerror( "API 未配置", "AI 图片 API 尚未接入。\n请在启动时通过 " "app.services.ai_image.set_api_caller(...) 注入真实实现。", ) return _set_requesting(self, True) self.ai_image_status_var.set("请求中…") def _success(record): self.root.after(0, lambda: _on_request_done(self, record, None)) def _error(exc): self.root.after(0, lambda: _on_request_done(self, None, exc)) _svc.request_image_async(prompt, on_success=_success, on_error=_error) def _set_requesting(self, flag: bool): self._ai_image_requesting = flag try: self.ai_image_send_btn.configure(state=tk.DISABLED if flag else tk.NORMAL) except Exception: pass def _on_request_done(self, record: Optional[_svc.AIImageRecord], exc: Optional[Exception]): _set_requesting(self, False) if exc is not None: self.ai_image_status_var.set(f"失败: {exc}") messagebox.showerror("生成失败", str(exc)) return self.ai_image_status_var.set("完成") self.ai_image_input.delete("1.0", tk.END) reload_ai_image_list(self) # 定位到新生成项(最新在前) if record is not None and self.ai_image_records: for i, r in enumerate(self.ai_image_records): if r.id == record.id: self.ai_image_listbox.selection_clear(0, tk.END) self.ai_image_listbox.selection_set(i) self.ai_image_listbox.activate(i) _select_record(self, r) break def _save_current(self): rec = getattr(self, "ai_image_current", None) if rec is None: messagebox.showinfo("提示", "请先选择一张图片") return ext = os.path.splitext(rec.image_path)[1] or ".png" dest = filedialog.asksaveasfilename( title="另存为", defaultextension=ext, initialfile=os.path.basename(rec.image_path), filetypes=[("图片", "*.png;*.jpg;*.jpeg;*.bmp;*.webp"), ("所有文件", "*.*")], ) if not dest: return try: _svc.export_record(rec, dest) messagebox.showinfo("成功", f"已保存到:\n{dest}") except Exception as exc: messagebox.showerror("保存失败", str(exc)) def _delete_current(self): rec = getattr(self, "ai_image_current", None) if rec is None: messagebox.showinfo("提示", "请先选择一张图片") return if not messagebox.askyesno("确认删除", f"确定删除这张缓存图片吗?\n{os.path.basename(rec.image_path)}"): return _svc.delete_record(rec) reload_ai_image_list(self)