Merge branch 'zanllp:main' into fix/comfyui-populated-text
commit
a4e503f214
|
|
@ -27,7 +27,4 @@ iib.db-wal
|
|||
|
||||
CLAUDE.md
|
||||
.claude/*
|
||||
videos/启动&添加文件夹构建索引.mp4
|
||||
videos/图像搜索和链接跳转.mp4
|
||||
videos/ai智能文件整理.mp4
|
||||
videos/skills安装&启动.mp4
|
||||
videos/*
|
||||
|
|
|
|||
|
|
@ -1,6 +1,81 @@
|
|||
[跳到中文](#中文)
|
||||
# English
|
||||
|
||||
## 2026-03-21
|
||||
### 📁 Proper Filename When Saving Images
|
||||
Fixed the issue where images saved via right-click menu were named "file" instead of their actual filenames. Now when you right-click to save or drag images to other apps, they keep their original filenames.
|
||||
|
||||
## 2026-03-19
|
||||
### ⚙️ Customizable Panel Settings
|
||||
Added the ability to customize which feature blocks are displayed in the full-screen info panel.
|
||||
|
||||
**Features:**
|
||||
- **Panel Settings**: Click the settings icon to toggle visibility of individual feature blocks
|
||||
- **Flexible Display**: Show/hide action buttons, file info tags, custom tags, layout controls, draggable image, and prompt tabs
|
||||
- **Persistent Storage**: Settings are automatically saved to the backend and restored on next visit
|
||||
|
||||
**How to use:**
|
||||
1. Open the full-screen info panel for any image
|
||||
2. Click the settings icon (⚙️) in the top-right corner
|
||||
3. Toggle the switches to show/hide different feature blocks
|
||||
4. Settings are automatically saved and applied
|
||||
|
||||
**Use Case:**
|
||||
Perfect for users who want a cleaner interface or only use specific features. Each user can customize their panel to match their workflow, showing only the blocks they need and hiding unused sections to reduce visual clutter.
|
||||
|
||||
**Note:** Settings are stored per-user in the backend, so your custom layout will be preserved across sessions and devices.
|
||||
|
||||
<img width="400" alt="Panel settings" src="docs/imgs/panel-settings.png" />
|
||||
|
||||
## 2026-03-17
|
||||
### 🖼️ ComfyUI-Ready Image Dragging
|
||||
Added drag-to-transfer functionality specifically designed for ComfyUI workflow integration.
|
||||
|
||||
**Features:**
|
||||
- **One-Click Image Access**: Click to display original images ready for dragging
|
||||
- **ComfyUI Integration**: Drag images directly into ComfyUI to restore complete workflows
|
||||
- **Dual Placement**: Available in both full-screen info panel and file grid overlays
|
||||
|
||||
|
||||
**How to use:**
|
||||
1. Click the "Click to get original image, grab and drag to other apps" button
|
||||
2. The original image appears instantly
|
||||
3. Drag the image directly into ComfyUI
|
||||
4. ComfyUI will automatically extract metadata and reconstruct the workflow
|
||||
|
||||
**Use Case:**
|
||||
Perfect for moving images from IIB to ComfyUI. When you drag an image into ComfyUI:
|
||||
- The original image is loaded
|
||||
- Generation parameters are extracted from metadata
|
||||
- The complete workflow is reconstructed automatically
|
||||
- No manual parameter input required
|
||||
|
||||
**Note:** This feature preserves all generation metadata in the image, enabling seamless workflow transfer to ComfyUI and other compatible tools.
|
||||
|
||||
[Video Demo](https://github.com/user-attachments/assets/86ee87c8-887a-49ce-a3ed-07a4a2321253)
|
||||
|
||||
## 2026-03-15
|
||||
### 📝 Editable Generation Information
|
||||
Added the ability to edit image generation prompts and metadata directly in the UI.
|
||||
|
||||
**Features:**
|
||||
- **Prompt Editor Modal**: Edit positive/negative prompts and generation parameters with a user-friendly interface
|
||||
- **Key-Value Editor**: Add custom metadata fields with support for both string and JSON value modes
|
||||
- **Smart Caching**: EXIF data is now cached in the database for faster subsequent retrieval
|
||||
- **Edit Tracking**: Manually edited prompts are marked and preserved separately from original file metadata
|
||||
- **Validation**: Built-in validation for required fields and unique key constraints
|
||||
|
||||
**How to use:**
|
||||
1. Click the "Edit" button on any image's generation info panel
|
||||
2. Modify positive/negative prompts and other parameters in the modal
|
||||
3. Add custom metadata using the key-value editor if needed
|
||||
4. Click "Save Prompt" to update the database
|
||||
5. Edited prompts are marked and will override original file metadata
|
||||
|
||||
**Note:** Edited prompts are stored in the database and won't modify the original image files.
|
||||
|
||||
<img width="400" alt="Prompt editing" src="docs/imgs/prompt-edit.png" />
|
||||
|
||||
## 2026-02-23
|
||||
### 🎬 Inline Video Playback
|
||||
Added inline video playback feature for video items wider than 400px.
|
||||
|
|
@ -9,7 +84,6 @@ Added inline video playback feature for video items wider than 400px.
|
|||
- **Play Here Button**: Hover over a video to see the "Play Here" button in the bottom-left corner
|
||||
- **Inline Playback**: Click to play the video directly in the grid item without opening a modal
|
||||
- **Auto-Stop Others**: Automatically stops any other playing videos when starting a new one
|
||||
- **Multi-language Support**: Button text is fully internationalized (English, Chinese, German)
|
||||
- **Smart Reset**: Automatically stops playback when the list is reordered or refreshed
|
||||
|
||||
**How to use:**
|
||||
|
|
@ -727,6 +801,80 @@ Triggered under the same circumstances as above, there will be a button to updat
|
|||
|
||||
# 中文
|
||||
|
||||
## 2026-03-21
|
||||
### 📁 修复右键保存图片文件名问题
|
||||
修复了通过右键菜单保存图片时,文件名显示为"file"而不是实际文件名的问题。现在右键保存图片或拖拽到其他应用时,都会保留原始文件名。
|
||||
|
||||
## 2026-03-19
|
||||
### ⚙️ 自定义面板设置
|
||||
新增了自定义全屏信息面板功能区块显示的功能。
|
||||
|
||||
**功能特性:**
|
||||
- **面板设置**:点击设置图标即可开关各个功能区块的显示
|
||||
- **灵活显示**:可显示/隐藏操作按钮、文件信息标签、自定义标签、布局控制、可拖拽图片、提示词标签页
|
||||
- **持久化存储**:设置自动保存到后端,下次访问时恢复
|
||||
|
||||
**使用方法:**
|
||||
1. 打开任意图片的全屏信息面板
|
||||
2. 点击右上角的设置图标(⚙️)
|
||||
3. 切换开关来显示/隐藏不同的功能区块
|
||||
4. 设置会自动保存并生效
|
||||
|
||||
**使用场景:**
|
||||
非常适合需要简洁界面或只使用特定功能的用户。每个用户都可以根据自己的工作流程自定义面板,只显示需要的区块,隐藏不使用的部分以减少视觉干扰。
|
||||
|
||||
**注意:** 设置按用户存储在后端,因此您的自定义布局将在不同会话和设备间保持。
|
||||
|
||||
<img width="400" alt="面板设置" src="docs/imgs/panel-settings.png" />
|
||||
|
||||
## 2026-03-17
|
||||
### 🖼️ 专为ComfyUI设计的图片拖拽功能
|
||||
新增了专为ComfyUI工作流还原设计的图片拖拽传输功能。
|
||||
|
||||
**功能特性:**
|
||||
- **一键获取原图**:点击即可显示原始图片,随时准备拖拽
|
||||
- **ComfyUI集成**:可直接将图片拖拽到ComfyUI中,自动还原完整工作流
|
||||
- **双重入口**:在全屏信息面板和文件网格中均可使用)
|
||||
|
||||
**使用方法:**
|
||||
1. 点击"点击获取原图,抓取拖拽至其他应用"按钮
|
||||
2. 原始图片立即显示
|
||||
3. 将图片直接拖拽到ComfyUI界面中
|
||||
4. ComfyUI会自动提取元数据并重建工作流
|
||||
|
||||
**使用场景:**
|
||||
完美支持从IIB到ComfyUI的图片转移。当您将图片拖拽到ComfyUI时:
|
||||
- 原始图片被加载
|
||||
- 从元数据中提取生成参数
|
||||
- 自动重建完整工作流
|
||||
- 无需手动输入参数
|
||||
|
||||
**注意:** 此功能保留图片中的所有生成元数据,实现与ComfyUI及其他兼容工具的无缝工作流传输。
|
||||
|
||||
[视频演示](https://github.com/user-attachments/assets/86ee87c8-887a-49ce-a3ed-07a4a2321253)
|
||||
|
||||
## 2026-03-15
|
||||
### 📝 可编辑的生成信息
|
||||
新增了在界面中直接编辑图片生成提示词和元数据的功能。
|
||||
|
||||
**功能特性:**
|
||||
- **提示词编辑器模态框**:通过友好的界面编辑正负向提示词和生成参数
|
||||
- **键值对编辑器**:添加自定义元数据字段,支持字符串和JSON值模式
|
||||
- **智能缓存**:EXIF数据现在会被缓存到数据库中,以便更快地后续检索
|
||||
- **编辑标记**:手动编辑的提示词会被标记,并与原始文件元数据分开保存
|
||||
- **数据验证**:对必填字段和唯一键约束进行内置验证
|
||||
|
||||
**使用方法:**
|
||||
1. 点击任意图片生成信息面板上的"编辑"按钮
|
||||
2. 在模态框中修改正负向提示词和其他参数
|
||||
3. 如需要,使用键值对编辑器添加自定义元数据
|
||||
4. 点击"保存提示词"更新数据库
|
||||
5. 编辑过的提示词会被标记,并将覆盖原始文件元数据
|
||||
|
||||
**注意:**编辑过的提示词存储在数据库中,不会修改原始图片文件。
|
||||
|
||||
<img width="400" alt="Prompt editing" src="docs/imgs/prompt-edit.png" />
|
||||
|
||||
## 2026-02-23
|
||||
### 🎬 视频原地播放功能
|
||||
为宽度超过 400px 的视频 item 添加了原地播放功能。
|
||||
|
|
@ -735,7 +883,6 @@ Triggered under the same circumstances as above, there will be a button to updat
|
|||
- **"在此播放"按钮**:鼠标悬停在视频上时,左下角会显示播放按钮
|
||||
- **原地播放**:点击按钮直接在网格 item 内播放视频,无需打开弹窗
|
||||
- **自动停止其他视频**:播放新视频时,会自动停止其他正在播放的视频
|
||||
- **多语言支持**:按钮文字支持多语言(英文、简体中文、繁体中文、德语)
|
||||
- **智能重置**:列表重新排序或刷新时,自动停止播放
|
||||
|
||||
**使用方法:**
|
||||
Binary file not shown.
|
After Width: | Height: | Size: 210 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 477 KiB |
|
|
@ -13,8 +13,8 @@ Promise.resolve().then(async () => {
|
|||
<link rel="icon" href="/favicon.ico" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||
<title>Infinite Image Browsing</title>
|
||||
<script type="module" crossorigin src="/infinite_image_browsing/fe-static/assets/index-f2db319b.js"></script>
|
||||
<link rel="stylesheet" href="/infinite_image_browsing/fe-static/assets/index-dd273d5b.css">
|
||||
<script type="module" crossorigin src="/infinite_image_browsing/fe-static/assets/index-b01f57e3.js"></script>
|
||||
<link rel="stylesheet" href="/infinite_image_browsing/fe-static/assets/index-882e7f3d.css">
|
||||
</head>
|
||||
|
||||
<body>
|
||||
|
|
|
|||
|
|
@ -0,0 +1,213 @@
|
|||
import argparse
|
||||
from contextlib import closing
|
||||
from datetime import datetime
|
||||
import os
|
||||
import random
|
||||
|
||||
from scripts.iib.db.datamodel import DataBase
|
||||
|
||||
|
||||
def get_creation_time_path(file_path: str) -> str:
|
||||
"""
|
||||
Get the creation time of a file and format it as YYYYMMDD_HHMMSS_<random>.
|
||||
Format example: 20260123_011706_8662450
|
||||
|
||||
On Windows, this uses creation time. On Unix, falls back to modification time.
|
||||
|
||||
Args:
|
||||
file_path (str): Path to the file.
|
||||
|
||||
Returns:
|
||||
str: Formatted creation time string (YYYYMMDD_HHMMSS_<random>).
|
||||
"""
|
||||
# On Windows, st_ctime is creation time
|
||||
# On Unix, st_ctime is metadata change time, so we use st_mtime
|
||||
if os.name == 'nt':
|
||||
timestamp = os.path.getctime(file_path)
|
||||
else:
|
||||
timestamp = os.path.getmtime(file_path)
|
||||
|
||||
dt = datetime.fromtimestamp(timestamp)
|
||||
time_str = dt.strftime("%Y%m%d_%H%M%S")
|
||||
random_fragment = random.randint(1000000, 9999999)
|
||||
return f"{time_str}_{random_fragment}"
|
||||
|
||||
|
||||
def check_database_exists(conn, file_path: str) -> bool:
|
||||
"""
|
||||
Check if a file path exists in the database.
|
||||
|
||||
Args:
|
||||
conn: Database connection object.
|
||||
file_path (str): Path to check.
|
||||
|
||||
Returns:
|
||||
bool: True if the file path exists in the database.
|
||||
"""
|
||||
normalized_path = os.path.normpath(file_path)
|
||||
|
||||
with closing(conn.cursor()) as cur:
|
||||
cur.execute(
|
||||
"SELECT 1 FROM image WHERE path = ? LIMIT 1",
|
||||
(normalized_path,)
|
||||
)
|
||||
return cur.fetchone() is not None
|
||||
|
||||
|
||||
def update_database_paths(conn, old_path: str, new_path: str):
|
||||
"""
|
||||
Update database paths when a file is renamed.
|
||||
|
||||
Args:
|
||||
conn: Database connection object.
|
||||
old_path (str): Original file path.
|
||||
new_path (str): New file path after rename.
|
||||
"""
|
||||
normalized_old = os.path.normpath(old_path)
|
||||
normalized_new = os.path.normpath(new_path)
|
||||
|
||||
with closing(conn.cursor()) as cur:
|
||||
# Update image table
|
||||
cur.execute(
|
||||
"UPDATE image SET path = ? WHERE path = ?",
|
||||
(normalized_new, normalized_old)
|
||||
)
|
||||
|
||||
|
||||
def normalize_single_file(file_path: str, dry_run: bool, conn):
|
||||
"""
|
||||
Normalize a single file's name based on its creation time (YYYYMMDD_HHMMSS_<random>).
|
||||
|
||||
Args:
|
||||
file_path (str): Path to the file to normalize.
|
||||
dry_run (bool): If True, only print without renaming.
|
||||
conn: Database connection object. If provided, will update database paths.
|
||||
|
||||
Returns:
|
||||
tuple: (status, message) where status is 'normalized', 'skipped', or 'error'
|
||||
"""
|
||||
try:
|
||||
# Get the directory and file extension
|
||||
dir_path = os.path.dirname(file_path)
|
||||
filename = os.path.basename(file_path)
|
||||
name, ext = os.path.splitext(filename)
|
||||
|
||||
# Skip if already in the target format (YYYYMMDD_HHMMSS_<random>)
|
||||
# Format: 8 digits + _ + 6 digits + _ + 7 digits = 23 characters
|
||||
parts = name.split('_')
|
||||
if len(parts) == 3 and len(parts[0]) == 8 and parts[0].isdigit() and len(parts[1]) == 6 and parts[1].isdigit() and len(parts[2]) == 7 and parts[2].isdigit():
|
||||
return 'skipped', f"Skip (already normalized): {filename}"
|
||||
|
||||
# Get creation time formatted string
|
||||
time_str = get_creation_time_path(file_path)
|
||||
new_filename = f"{time_str}{ext}"
|
||||
new_path = os.path.join(dir_path, new_filename)
|
||||
|
||||
# Handle duplicate filenames by appending a counter
|
||||
counter = 1
|
||||
while os.path.exists(new_path) and new_path != file_path:
|
||||
new_filename = f"{time_str}_{counter}{ext}"
|
||||
new_path = os.path.join(dir_path, new_filename)
|
||||
counter += 1
|
||||
|
||||
if new_path == file_path:
|
||||
return 'skipped', f"Skip (same name): {filename}"
|
||||
|
||||
if dry_run:
|
||||
# Check if file exists in database and include in message
|
||||
db_info = ""
|
||||
if conn:
|
||||
if check_database_exists(conn, file_path):
|
||||
db_info = " [in database]"
|
||||
else:
|
||||
db_info = " [not in database]"
|
||||
return 'normalized', f"Would normalize: {filename} -> {new_filename}{db_info}"
|
||||
else:
|
||||
os.rename(file_path, new_path)
|
||||
# Update database if connection is provided
|
||||
if conn:
|
||||
update_database_paths(conn, file_path, new_path)
|
||||
return 'normalized', f"Normalized: {filename} -> {new_filename}"
|
||||
|
||||
except Exception as e:
|
||||
return 'error', f"Error normalizing {file_path}: {e}"
|
||||
|
||||
|
||||
def normalize_filenames(dir_path: str, recursive: bool = False, dry_run: bool = False, db_path: str = None):
|
||||
"""
|
||||
Normalize all filenames in the specified directory to creation time format (YYYYMMDD_HHMMSS_<random>).
|
||||
|
||||
Args:
|
||||
dir_path (str): Path to the directory containing files to normalize.
|
||||
recursive (bool): Whether to recursively process subdirectories. Default is False.
|
||||
dry_run (bool): If True, only print what would be normalized without actually renaming. Default is False.
|
||||
db_path (str): Path to the IIB database file to update with new paths. Default is None.
|
||||
"""
|
||||
normalized_count = 0
|
||||
skipped_count = 0
|
||||
error_count = 0
|
||||
|
||||
# Setup database connection if db_path is provided
|
||||
conn = None
|
||||
if db_path:
|
||||
DataBase.path = os.path.normpath(os.path.join(os.getcwd(), db_path))
|
||||
conn = DataBase.get_conn()
|
||||
|
||||
files_to_process = []
|
||||
|
||||
if recursive:
|
||||
# Walk through all files in directory and subdirectories
|
||||
for root, _, files in os.walk(dir_path):
|
||||
for filename in files:
|
||||
files_to_process.append(os.path.join(root, filename))
|
||||
else:
|
||||
# Only process files in the specified directory (non-recursive)
|
||||
for entry in os.listdir(dir_path):
|
||||
full_path = os.path.join(dir_path, entry)
|
||||
if os.path.isfile(full_path):
|
||||
files_to_process.append(full_path)
|
||||
|
||||
# Process all files
|
||||
for file_path in files_to_process:
|
||||
status, message = normalize_single_file(file_path, dry_run, conn)
|
||||
print(message)
|
||||
if status == 'normalized':
|
||||
normalized_count += 1
|
||||
elif status == 'skipped':
|
||||
skipped_count += 1
|
||||
elif status == 'error':
|
||||
error_count += 1
|
||||
|
||||
print(f"\nSummary: {'Dry run - ' if dry_run else ''}Normalized: {normalized_count}, Skipped: {skipped_count}, Errors: {error_count}")
|
||||
|
||||
if conn and not dry_run:
|
||||
conn.commit()
|
||||
print(f"Database updated at: {db_path}")
|
||||
|
||||
|
||||
def setup_parser() -> argparse.ArgumentParser:
|
||||
parser = argparse.ArgumentParser(
|
||||
description="Normalize filenames to creation time format (YYYYMMDD_HHMMSS_<random>) and optionally update IIB database paths."
|
||||
)
|
||||
parser.add_argument(
|
||||
"dir_path", type=str, help="Path to the directory containing files to normalize."
|
||||
)
|
||||
parser.add_argument(
|
||||
"-r", "--recursive", action="store_true", help="Recursively process subdirectories."
|
||||
)
|
||||
parser.add_argument(
|
||||
"--force", action="store_true", help="Actually perform the normalization. Without this flag, runs in dry-run mode."
|
||||
)
|
||||
parser.add_argument(
|
||||
"--db_path", type=str, help="Path to the IIB database file to update with new paths. Default value is 'iib.db'.", default="iib.db"
|
||||
)
|
||||
return parser
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
parser = setup_parser()
|
||||
args = parser.parse_args()
|
||||
|
||||
# Default to dry-run mode unless --force is specified
|
||||
dry_run = not args.force
|
||||
normalize_filenames(args.dir_path, args.recursive, dry_run, args.db_path)
|
||||
|
|
@ -15,6 +15,7 @@ from scripts.iib.tool import (
|
|||
is_media_file,
|
||||
get_cache_dir,
|
||||
get_formatted_date,
|
||||
get_modified_date,
|
||||
is_win,
|
||||
cwd,
|
||||
locale,
|
||||
|
|
@ -655,6 +656,47 @@ def infinite_image_browsing_api(app: FastAPI, **kwargs):
|
|||
headers={"Cache-Control": "max-age=31536000", "ETag": hash},
|
||||
)
|
||||
|
||||
@app.get(api_base + "/img/{filename}", dependencies=[Depends(verify_secret)])
|
||||
async def get_image(filename: str, path: str, t: str):
|
||||
import mimetypes
|
||||
import urllib.parse
|
||||
|
||||
check_path_trust(path)
|
||||
|
||||
# 验证文件名是否匹配
|
||||
actual_filename = os.path.basename(path)
|
||||
decoded_filename = urllib.parse.unquote(filename)
|
||||
|
||||
if actual_filename != decoded_filename:
|
||||
raise HTTPException(status_code=400, detail="Filename mismatch")
|
||||
|
||||
if not os.path.exists(path):
|
||||
raise HTTPException(status_code=404)
|
||||
if not os.path.isfile(path):
|
||||
raise HTTPException(status_code=400, detail=f"{path} is not a file")
|
||||
|
||||
# 验证是否为图片文件
|
||||
media_type, _ = mimetypes.guess_type(path)
|
||||
if media_type and not media_type.startswith('image/'):
|
||||
raise HTTPException(status_code=400, detail="Not an image file")
|
||||
|
||||
# 设置 Content-Disposition 为 inline,带文件名
|
||||
headers = {}
|
||||
encoded_filename = urllib.parse.quote(filename.encode('utf-8'))
|
||||
headers['Content-Disposition'] = f"inline; filename*=UTF-8''{encoded_filename}"
|
||||
|
||||
if is_path_under_parents(path) and is_valid_media_path(path):
|
||||
headers["Cache-Control"] = "public, max-age=31536000"
|
||||
headers["Expires"] = (datetime.now() + timedelta(days=365)).strftime(
|
||||
"%a, %d %b %Y %H:%M:%S GMT"
|
||||
)
|
||||
|
||||
return FileResponse(
|
||||
path,
|
||||
media_type=media_type,
|
||||
headers=headers,
|
||||
)
|
||||
|
||||
@app.get(api_base + "/file", dependencies=[Depends(verify_secret)])
|
||||
async def get_file(path: str, t: str, disposition: Optional[str] = None):
|
||||
filename = path
|
||||
|
|
@ -807,11 +849,24 @@ def infinite_image_browsing_api(app: FastAPI, **kwargs):
|
|||
|
||||
@app.get(api_base + "/image_geninfo", dependencies=[Depends(verify_secret)])
|
||||
async def image_geninfo(path: str):
|
||||
# 使用 get_exif_data 函数,它已经支持视频文件
|
||||
from scripts.iib.db.update_image_data import get_exif_data
|
||||
conn = DataBase.get_conn()
|
||||
try:
|
||||
# 优先从数据库查询
|
||||
img = DbImg.get(conn, path)
|
||||
if img and img.exif:
|
||||
return img.exif
|
||||
|
||||
# 数据库中没有,从文件读取
|
||||
result = get_exif_data(path)
|
||||
return result.raw_info or ""
|
||||
raw_info = result.raw_info or ""
|
||||
|
||||
# 如果 DbImg 存在,将读取到的数据缓存到数据库
|
||||
if img and raw_info:
|
||||
img.exif = raw_info
|
||||
img.update(conn)
|
||||
|
||||
return raw_info
|
||||
except Exception as e:
|
||||
logger.error(f"Failed to get geninfo for {path}: {e}")
|
||||
return ""
|
||||
|
|
@ -850,15 +905,44 @@ def infinite_image_browsing_api(app: FastAPI, **kwargs):
|
|||
exif_data = {str(k): str(v) for k, v in exif_dict.items()}
|
||||
except AttributeError:
|
||||
pass
|
||||
|
||||
|
||||
info_data = {k: str(v) for k, v in img.info.items() if not k.startswith('exif')}
|
||||
exif_data.update(info_data)
|
||||
|
||||
|
||||
return exif_data
|
||||
except Exception as e:
|
||||
logger.error(f"Failed to get exif for {path}: {e}")
|
||||
return {}
|
||||
|
||||
class UpdateExifReq(BaseModel):
|
||||
path: str
|
||||
exif: str
|
||||
|
||||
@app.post(api_base + "/update_exif", dependencies=[Depends(verify_secret), Depends(write_permission_required)])
|
||||
async def update_exif(req: UpdateExifReq):
|
||||
"""更新图片/视频的 exif 信息"""
|
||||
conn = DataBase.get_conn()
|
||||
try:
|
||||
img = DbImg.get(conn, req.path)
|
||||
if img:
|
||||
img.update_exif(conn, req.exif)
|
||||
conn.commit()
|
||||
return {"success": True, "message": "Exif updated successfully"}
|
||||
else:
|
||||
# 如果数据库中没有记录,创建新记录
|
||||
img = DbImg(path=req.path, exif=req.exif, exif_edited=True)
|
||||
# 获取文件信息
|
||||
if os.path.exists(req.path):
|
||||
stat = os.stat(req.path)
|
||||
img.size = stat.st_size
|
||||
img.date = get_modified_date(req.path)
|
||||
img.save(conn)
|
||||
conn.commit()
|
||||
return {"success": True, "message": "Exif created successfully"}
|
||||
except Exception as e:
|
||||
logger.error(f"Failed to update exif for {req.path}: {e}", stack_info=True)
|
||||
raise HTTPException(status_code=500, detail=str(e))
|
||||
|
||||
|
||||
class CheckPathExistsReq(BaseModel):
|
||||
paths: List[str]
|
||||
|
|
|
|||
|
|
@ -97,9 +97,10 @@ class DataBase:
|
|||
|
||||
|
||||
class Image:
|
||||
def __init__(self, path, exif=None, size=0, date="", id=None):
|
||||
def __init__(self, path, exif=None, size=0, date="", exif_edited=False, id=None):
|
||||
self.path = path
|
||||
self.exif = exif
|
||||
self.exif_edited = exif_edited
|
||||
self.id = id
|
||||
self.size = size
|
||||
self.date = date
|
||||
|
|
@ -120,11 +121,21 @@ class Image:
|
|||
def save(self, conn):
|
||||
with closing(conn.cursor()) as cur:
|
||||
cur.execute(
|
||||
"INSERT OR REPLACE INTO image (path, exif, size, date) VALUES (?, ?, ?, ?)",
|
||||
(self.path, self.exif, self.size, self.date),
|
||||
"INSERT OR REPLACE INTO image (path, exif, exif_edited, size, date) VALUES (?, ?, ?, ?, ?)",
|
||||
(self.path, self.exif, int(self.exif_edited), self.size, self.date),
|
||||
)
|
||||
self.id = cur.lastrowid
|
||||
|
||||
def update_exif(self, conn: Connection, exif: str, mark_edited: bool = True):
|
||||
"""更新图片的 exif 信息并标记为已编辑"""
|
||||
with closing(conn.cursor()) as cur:
|
||||
cur.execute(
|
||||
"UPDATE image SET exif = ?, exif_edited = ? WHERE id = ?",
|
||||
(exif, mark_edited, self.id),
|
||||
)
|
||||
self.exif = exif
|
||||
self.exif_edited = mark_edited
|
||||
|
||||
def update_path(self, conn: Connection, new_path: str, force=False):
|
||||
self.path = os.path.normpath(new_path)
|
||||
with closing(conn.cursor()) as cur:
|
||||
|
|
@ -174,11 +185,22 @@ class Image:
|
|||
path TEXT UNIQUE,
|
||||
exif TEXT,
|
||||
size INTEGER,
|
||||
date TEXT
|
||||
date TEXT,
|
||||
exif_edited INTEGER DEFAULT 0
|
||||
)"""
|
||||
)
|
||||
cur.execute("CREATE INDEX IF NOT EXISTS image_idx_path ON image(path)")
|
||||
|
||||
# 数据库迁移:为旧表添加 exif_edited 列
|
||||
try:
|
||||
cur.execute(
|
||||
"""ALTER TABLE image
|
||||
ADD COLUMN exif_edited INTEGER DEFAULT 0"""
|
||||
)
|
||||
except sqlite3.OperationalError:
|
||||
# 列已存在,忽略
|
||||
pass
|
||||
|
||||
@classmethod
|
||||
def count(cls, conn):
|
||||
with closing(conn.cursor()) as cur:
|
||||
|
|
@ -188,7 +210,11 @@ class Image:
|
|||
|
||||
@classmethod
|
||||
def from_row(cls, row: tuple):
|
||||
image = cls(path=row[1], exif=row[2], size=row[3], date=row[4])
|
||||
"""从数据库行创建 Image 对象
|
||||
|
||||
字段顺序:id=0, path=1, exif=2, size=3, date=4, exif_edited=5
|
||||
"""
|
||||
image = cls(path=row[1], exif=row[2], size=row[3], date=row[4], exif_edited=bool(row[5]) )
|
||||
image.id = row[0]
|
||||
return image
|
||||
|
||||
|
|
|
|||
|
|
@ -146,6 +146,11 @@ def get_extra_meta_keys_from_plugins(source_identifier: str):
|
|||
|
||||
def build_single_img_idx(conn, file_path, is_rebuild, safe_save_img_tag):
|
||||
img = DbImg.get(conn, file_path)
|
||||
|
||||
if img and is_rebuild and img.exif_edited:
|
||||
logger.info(f"Image {file_path} has been manually edited, skipping rebuild.")
|
||||
return
|
||||
|
||||
parsed_params = None
|
||||
if is_rebuild:
|
||||
info = get_exif_data(file_path)
|
||||
|
|
|
|||
|
|
@ -53,12 +53,15 @@ declare module '@vue/runtime-core' {
|
|||
BaseFileListInfo: typeof import('./src/components/BaseFileListInfo.vue')['default']
|
||||
ChangeIndicator: typeof import('./src/components/ChangeIndicator.vue')['default']
|
||||
ContextMenu: typeof import('./src/components/ContextMenu.vue')['default']
|
||||
DraggableImage: typeof import('./src/components/DraggableImage.vue')['default']
|
||||
ExifBrowser: typeof import('./src/components/ExifBrowser.vue')['default']
|
||||
FileItem: typeof import('./src/components/FileItem.vue')['default']
|
||||
HistoryRecord: typeof import('./src/components/HistoryRecord.vue')['default']
|
||||
KvPairEditor: typeof import('./src/components/KvPairEditor.vue')['default']
|
||||
MultiSelectKeep: typeof import('./src/components/MultiSelectKeep.vue')['default']
|
||||
NumInput: typeof import('./src/components/numInput.vue')['default']
|
||||
OrganizeJobsPanel: typeof import('./src/components/OrganizeJobsPanel.vue')['default']
|
||||
PromptEditorModal: typeof import('./src/components/PromptEditorModal.vue')['default']
|
||||
RouterLink: typeof import('vue-router')['RouterLink']
|
||||
RouterView: typeof import('vue-router')['RouterView']
|
||||
SmartOrganizeConfigModal: typeof import('./src/components/SmartOrganizeConfigModal.vue')['default']
|
||||
|
|
|
|||
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
|
|
@ -1 +1 @@
|
|||
import{d as a,o as t,j as n,c as s,c1 as _,n as o}from"./index-f2db319b.js";const c={class:"img-sli-container"},i=a({__name:"ImgSliPagePane",props:{paneIdx:{},tabIdx:{},left:{},right:{}},setup(l){return(e,r)=>(t(),n("div",c,[s(_,{left:e.left,right:e.right},null,8,["left","right"])]))}});const d=o(i,[["__scopeId","data-v-ae3fb9a8"]]);export{d as default};
|
||||
import{d as a,o as t,j as n,c as s,c1 as _,n as o}from"./index-b01f57e3.js";const c={class:"img-sli-container"},i=a({__name:"ImgSliPagePane",props:{paneIdx:{},tabIdx:{},left:{},right:{}},setup(l){return(e,r)=>(t(),n("div",c,[s(_,{left:e.left,right:e.right},null,8,["left","right"])]))}});const d=o(i,[["__scopeId","data-v-ae3fb9a8"]]);export{d as default};
|
||||
|
|
@ -0,0 +1 @@
|
|||
.container[data-v-ed0cd2f6]{background:var(--zp-secondary-background);position:relative}.container .action-bar[data-v-ed0cd2f6]{display:flex;align-items:center;user-select:none;gap:4px;padding:4px}.container .action-bar>*[data-v-ed0cd2f6]{flex-wrap:wrap}.container .file-list[data-v-ed0cd2f6]{list-style:none;padding:8px;overflow:auto;height:calc(var(--pane-max-height) - 40px);width:100%}.container .no-res-hint[data-v-ed0cd2f6]{height:var(--pane-max-height);display:flex;align-items:center;flex-direction:column;justify-content:center}.container .no-res-hint .hint[data-v-ed0cd2f6]{font-size:1.6em;margin-bottom:2em;text-align:center}
|
||||
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
|
|
@ -0,0 +1 @@
|
|||
.container[data-v-59722f79]{background:var(--zp-secondary-background);position:relative;height:var(--pane-max-height)}.action-bar[data-v-59722f79]{display:flex;align-items:center;user-select:none;gap:6px;padding:6px 8px}.title[data-v-59722f79]{font-weight:700;max-width:40vw}.file-list[data-v-59722f79]{list-style:none;padding:8px;overflow:auto;height:calc(var(--pane-max-height) - 44px);width:100%}.no-res-hint[data-v-59722f79]{height:calc(var(--pane-max-height) - 44px);display:flex;align-items:center;flex-direction:column;justify-content:center}.no-res-hint .hint[data-v-59722f79]{font-size:1.2em;opacity:.7}.preview-switch[data-v-59722f79]{position:fixed;bottom:24px;right:24px;display:flex;gap:8px;font-size:36px;user-select:none}.disable[data-v-59722f79]{opacity:.3;pointer-events:none}
|
||||
|
|
@ -1 +0,0 @@
|
|||
.container[data-v-4815fec6]{background:var(--zp-secondary-background);position:relative}.container .action-bar[data-v-4815fec6]{display:flex;align-items:center;user-select:none;gap:4px;padding:4px}.container .action-bar>*[data-v-4815fec6]{flex-wrap:wrap}.container .file-list[data-v-4815fec6]{list-style:none;padding:8px;overflow:auto;height:calc(var(--pane-max-height) - 40px);width:100%}.container .no-res-hint[data-v-4815fec6]{height:var(--pane-max-height);display:flex;align-items:center;flex-direction:column;justify-content:center}.container .no-res-hint .hint[data-v-4815fec6]{font-size:1.6em;margin-bottom:2em;text-align:center}
|
||||
File diff suppressed because one or more lines are too long
|
|
@ -1 +0,0 @@
|
|||
.container[data-v-aea581a5]{background:var(--zp-secondary-background);position:relative;height:var(--pane-max-height)}.action-bar[data-v-aea581a5]{display:flex;align-items:center;user-select:none;gap:6px;padding:6px 8px}.title[data-v-aea581a5]{font-weight:700;max-width:40vw}.file-list[data-v-aea581a5]{list-style:none;padding:8px;overflow:auto;height:calc(var(--pane-max-height) - 44px);width:100%}.no-res-hint[data-v-aea581a5]{height:calc(var(--pane-max-height) - 44px);display:flex;align-items:center;flex-direction:column;justify-content:center}.no-res-hint .hint[data-v-aea581a5]{font-size:1.2em;opacity:.7}.preview-switch[data-v-aea581a5]{position:fixed;bottom:24px;right:24px;display:flex;gap:8px;font-size:36px;user-select:none}.disable[data-v-aea581a5]{opacity:.3;pointer-events:none}
|
||||
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
|
|
@ -1 +0,0 @@
|
|||
[data-v-4584136c] .float-panel{position:fixed}.regex-icon[data-v-4584136c]{user-select:none;padding:4px;margin:0 4px;cursor:pointer;border:1px solid var(--zp-border);border-radius:4px}.regex-icon img[data-v-4584136c]{height:1.5em}.regex-icon[data-v-4584136c]:hover{background:var(--zp-border)}.regex-icon.selected[data-v-4584136c]{background:var(--primary-color-1);border:1px solid var(--primary-color)}.search-bar[data-v-4584136c]{padding:8px 8px 0;display:flex}.search-bar.last[data-v-4584136c]{padding-bottom:8px}.search-bar .form-name[data-v-4584136c]{flex-shrink:0;padding:4px 8px}.search-bar .actions>*[data-v-4584136c]{margin-right:4px}.tips-wrapper[data-v-4584136c]{padding:0 8px}.container[data-v-4584136c]{background:var(--zp-secondary-background);position:relative}.container .file-list[data-v-4584136c]{list-style:none;padding:8px;height:100%;overflow:auto;height:var(--pane-max-height);width:100%}
|
||||
|
|
@ -0,0 +1 @@
|
|||
[data-v-4fecbf3c] .float-panel{position:fixed}.regex-icon[data-v-4fecbf3c]{user-select:none;padding:4px;margin:0 4px;cursor:pointer;border:1px solid var(--zp-border);border-radius:4px}.regex-icon img[data-v-4fecbf3c]{height:1.5em}.regex-icon[data-v-4fecbf3c]:hover{background:var(--zp-border)}.regex-icon.selected[data-v-4fecbf3c]{background:var(--primary-color-1);border:1px solid var(--primary-color)}.search-bar[data-v-4fecbf3c]{padding:8px 8px 0;display:flex}.search-bar.last[data-v-4fecbf3c]{padding-bottom:8px}.search-bar .form-name[data-v-4fecbf3c]{flex-shrink:0;padding:4px 8px}.search-bar .actions>*[data-v-4fecbf3c]{margin-right:4px}.tips-wrapper[data-v-4fecbf3c]{padding:0 8px}.container[data-v-4fecbf3c]{background:var(--zp-secondary-background);position:relative}.container .file-list[data-v-4fecbf3c]{list-style:none;padding:8px;height:100%;overflow:auto;height:var(--pane-max-height);width:100%}
|
||||
File diff suppressed because one or more lines are too long
|
|
@ -1,3 +1,3 @@
|
|||
import{bJ as N,bK as E,bL as q,c as u,A as U,d as R,o as d,j as p,k as _,F as X,K as G,ah as K,C as f,l as g,t as h,E as L,a3 as H,n as z,aq as Q,aC as C,aD as F,r as W,aj as I,X as V,v as Y,bM as Z,m as T,bN as ee,bO as te}from"./index-f2db319b.js";const ge=N(E),me=N(q);var ne={icon:{tag:"svg",attrs:{viewBox:"64 64 896 896",focusable:"false"},children:[{tag:"path",attrs:{d:"M878.3 392.1L631.9 145.7c-6.5-6.5-15-9.7-23.5-9.7s-17 3.2-23.5 9.7L423.8 306.9c-12.2-1.4-24.5-2-36.8-2-73.2 0-146.4 24.1-206.5 72.3-15.4 12.3-16.6 35.4-2.7 49.4l181.7 181.7-215.4 215.2a15.8 15.8 0 00-4.6 9.8l-3.4 37.2c-.9 9.4 6.6 17.4 15.9 17.4.5 0 1 0 1.5-.1l37.2-3.4c3.7-.3 7.2-2 9.8-4.6l215.4-215.4 181.7 181.7c6.5 6.5 15 9.7 23.5 9.7 9.7 0 19.3-4.2 25.9-12.4 56.3-70.3 79.7-158.3 70.2-243.4l161.1-161.1c12.9-12.8 12.9-33.8 0-46.8z"}}]},name:"pushpin",theme:"filled"};const se=ne;function k(i){for(var e=1;e<arguments.length;e++){var t=arguments[e]!=null?Object(arguments[e]):{},n=Object.keys(t);typeof Object.getOwnPropertySymbols=="function"&&(n=n.concat(Object.getOwnPropertySymbols(t).filter(function(s){return Object.getOwnPropertyDescriptor(t,s).enumerable}))),n.forEach(function(s){ie(i,s,t[s])})}return i}function ie(i,e,t){return e in i?Object.defineProperty(i,e,{value:t,enumerable:!0,configurable:!0,writable:!0}):i[e]=t,i}var P=function(e,t){var n=k({},e,t.attrs);return u(U,k({},n,{icon:se}),null)};P.displayName="PushpinFilled";P.inheritAttrs=!1;const re=P,ae={class:"record-container"},oe={style:{flex:"1"}},ce={class:"rec-actions"},le=["onClick"],ue=R({__name:"HistoryRecord",props:{records:{}},emits:["reuseRecord"],setup(i){return(e,t)=>{const n=H;return d(),p("div",null,[_("ul",ae,[(d(!0),p(X,null,G(e.records.getRecords(),s=>(d(),p("li",{key:s.id,class:"record"},[_("div",oe,[K(e.$slots,"default",{record:s},void 0,!0)]),_("div",ce,[u(n,{onClick:o=>e.$emit("reuseRecord",s),type:"primary"},{default:f(()=>[g(h(e.$t("restore")),1)]),_:2},1032,["onClick"]),_("div",{class:"pin",onClick:o=>e.records.switchPin(s)},[u(L(re)),g(" "+h(e.records.isPinned(s)?e.$t("unpin"):e.$t("pin")),1)],8,le)])]))),128))])])}}});const ve=z(ue,[["__scopeId","data-v-834a248f"]]);class m{constructor(e=128,t=[],n=[]){this.maxLength=e,this.records=t,this.pinnedValues=n}isPinned(e){return this.pinnedValues.some(t=>t.id===e.id)}add(e){this.records.length>=this.maxLength&&this.records.pop(),this.records.unshift({...e,id:Q()+Date.now(),time:new Date().toLocaleString()})}pin(e){const t=this.records.findIndex(n=>n.id===e.id);t!==-1&&this.records.splice(t,1),this.pinnedValues.push(e)}unpin(e){const t=this.pinnedValues.findIndex(n=>n.id===e.id);t!==-1&&this.pinnedValues.splice(t,1),this.records.unshift(e)}switchPin(e){this.isPinned(e)?this.unpin(e):this.pin(e)}getRecords(){return[...this.pinnedValues,...this.records]}getPinnedValues(){return this.pinnedValues}}const ye=C(`${F}fuzzy-search-HistoryRecord`,new m,{serializer:{read:i=>{const e=JSON.parse(i);return new m(e.maxLength,e.records,e.pinnedValues)},write:JSON.stringify}}),be=C(`${F}tag-search-HistoryRecord`,new m,{serializer:{read:i=>{const e=JSON.parse(i);return new m(e.maxLength,e.records,e.pinnedValues)},write:JSON.stringify}}),de={key:0,class:"tips-carousel"},pe={class:"tip-text"},_e={key:0,class:"tip-actions"},he=R({__name:"TipsCarousel",props:{interval:{type:Number,default:1e4}},setup(i){const e=i,t=C("iib-dismissed-tips-v2",{}),n=W(0);let s=null;const o=I(()=>{var v;const r=[];for(let a=1;a<=10;a++){const l=`loadingTip${a}`,y=V(l);if(!y||typeof y!="string")continue;const $=y.split("|"),b=$[0],M=((v=$[1])==null?void 0:v.trim())||"info",S=`
|
||||
import{bJ as N,bK as E,bL as q,c as u,A as U,d as R,o as d,j as p,k as _,F as X,K as G,ah as K,C as f,l as g,t as h,E as L,a3 as H,n as z,aq as Q,aC as C,aD as F,r as W,aj as I,X as V,v as Y,bM as Z,m as T,bN as ee,bO as te}from"./index-b01f57e3.js";const ge=N(E),me=N(q);var ne={icon:{tag:"svg",attrs:{viewBox:"64 64 896 896",focusable:"false"},children:[{tag:"path",attrs:{d:"M878.3 392.1L631.9 145.7c-6.5-6.5-15-9.7-23.5-9.7s-17 3.2-23.5 9.7L423.8 306.9c-12.2-1.4-24.5-2-36.8-2-73.2 0-146.4 24.1-206.5 72.3-15.4 12.3-16.6 35.4-2.7 49.4l181.7 181.7-215.4 215.2a15.8 15.8 0 00-4.6 9.8l-3.4 37.2c-.9 9.4 6.6 17.4 15.9 17.4.5 0 1 0 1.5-.1l37.2-3.4c3.7-.3 7.2-2 9.8-4.6l215.4-215.4 181.7 181.7c6.5 6.5 15 9.7 23.5 9.7 9.7 0 19.3-4.2 25.9-12.4 56.3-70.3 79.7-158.3 70.2-243.4l161.1-161.1c12.9-12.8 12.9-33.8 0-46.8z"}}]},name:"pushpin",theme:"filled"};const se=ne;function k(i){for(var e=1;e<arguments.length;e++){var t=arguments[e]!=null?Object(arguments[e]):{},n=Object.keys(t);typeof Object.getOwnPropertySymbols=="function"&&(n=n.concat(Object.getOwnPropertySymbols(t).filter(function(s){return Object.getOwnPropertyDescriptor(t,s).enumerable}))),n.forEach(function(s){ie(i,s,t[s])})}return i}function ie(i,e,t){return e in i?Object.defineProperty(i,e,{value:t,enumerable:!0,configurable:!0,writable:!0}):i[e]=t,i}var P=function(e,t){var n=k({},e,t.attrs);return u(U,k({},n,{icon:se}),null)};P.displayName="PushpinFilled";P.inheritAttrs=!1;const re=P,ae={class:"record-container"},oe={style:{flex:"1"}},ce={class:"rec-actions"},le=["onClick"],ue=R({__name:"HistoryRecord",props:{records:{}},emits:["reuseRecord"],setup(i){return(e,t)=>{const n=H;return d(),p("div",null,[_("ul",ae,[(d(!0),p(X,null,G(e.records.getRecords(),s=>(d(),p("li",{key:s.id,class:"record"},[_("div",oe,[K(e.$slots,"default",{record:s},void 0,!0)]),_("div",ce,[u(n,{onClick:o=>e.$emit("reuseRecord",s),type:"primary"},{default:f(()=>[g(h(e.$t("restore")),1)]),_:2},1032,["onClick"]),_("div",{class:"pin",onClick:o=>e.records.switchPin(s)},[u(L(re)),g(" "+h(e.records.isPinned(s)?e.$t("unpin"):e.$t("pin")),1)],8,le)])]))),128))])])}}});const ve=z(ue,[["__scopeId","data-v-834a248f"]]);class m{constructor(e=128,t=[],n=[]){this.maxLength=e,this.records=t,this.pinnedValues=n}isPinned(e){return this.pinnedValues.some(t=>t.id===e.id)}add(e){this.records.length>=this.maxLength&&this.records.pop(),this.records.unshift({...e,id:Q()+Date.now(),time:new Date().toLocaleString()})}pin(e){const t=this.records.findIndex(n=>n.id===e.id);t!==-1&&this.records.splice(t,1),this.pinnedValues.push(e)}unpin(e){const t=this.pinnedValues.findIndex(n=>n.id===e.id);t!==-1&&this.pinnedValues.splice(t,1),this.records.unshift(e)}switchPin(e){this.isPinned(e)?this.unpin(e):this.pin(e)}getRecords(){return[...this.pinnedValues,...this.records]}getPinnedValues(){return this.pinnedValues}}const ye=C(`${F}fuzzy-search-HistoryRecord`,new m,{serializer:{read:i=>{const e=JSON.parse(i);return new m(e.maxLength,e.records,e.pinnedValues)},write:JSON.stringify}}),be=C(`${F}tag-search-HistoryRecord`,new m,{serializer:{read:i=>{const e=JSON.parse(i);return new m(e.maxLength,e.records,e.pinnedValues)},write:JSON.stringify}}),de={key:0,class:"tips-carousel"},pe={class:"tip-text"},_e={key:0,class:"tip-actions"},he=R({__name:"TipsCarousel",props:{interval:{type:Number,default:1e4}},setup(i){const e=i,t=C("iib-dismissed-tips-v2",{}),n=W(0);let s=null;const o=I(()=>{var v;const r=[];for(let a=1;a<=10;a++){const l=`loadingTip${a}`,y=V(l);if(!y||typeof y!="string")continue;const $=y.split("|"),b=$[0],M=((v=$[1])==null?void 0:v.trim())||"info",S=`
|
||||
|
||||
`,w=b.indexOf(S);if(w===-1)continue;const x=b.substring(0,w).trim(),A=b.substring(w+S.length).trim(),O={title:x,content:A,type:M};O.type==="warning"&&t.value[x]||r.push(O)}for(let a=r.length-1;a>0;a--){const l=Math.floor(Math.random()*(a+1));[r[a],r[l]]=[r[l],r[a]]}return r}),c=I(()=>{const r=o.value;return r.length===0?{title:"",content:"",type:"info"}:r[n.value%r.length]}),j=r=>{switch(r){case"warning":return"warning";case"info":return"blue";case"tip":return"green";default:return"default"}},B=()=>{c.value.type==="warning"&&(t.value={...t.value,[c.value.title]:!0})},D=()=>{s&&clearInterval(s),s=setInterval(()=>{o.value.length>1&&(n.value=(n.value+1)%o.value.length)},e.interval)},J=()=>{s&&(clearInterval(s),s=null)};return Y(()=>{D()}),Z(()=>{J()}),(r,v)=>{const a=te,l=H;return o.value.length>0?(d(),p("div",de,[u(ee,{name:"tip-fade",mode:"out-in"},{default:f(()=>[(d(),p("div",{key:n.value,class:"tip-content"},[u(a,{color:j(c.value.type),class:"tip-tag"},{default:f(()=>[g(h(c.value.title),1)]),_:1},8,["color"]),_("span",pe,h(c.value.content),1),c.value.type==="warning"?(d(),p("div",_e,[u(l,{size:"small",type:"link",onClick:B},{default:f(()=>[g(h(L(V)("dontShowAgain")),1)]),_:1})])):T("",!0)]))]),_:1})])):T("",!0)}}});const we=z(he,[["__scopeId","data-v-3b5692ee"]]);export{ve as H,we as T,me as _,ge as a,ye as f,be as t};
|
||||
File diff suppressed because one or more lines are too long
|
|
@ -1 +1 @@
|
|||
import{b2 as i,aO as t,dK as f,aS as n}from"./index-f2db319b.js";function u(e,a,r){if(!i(r))return!1;var s=typeof a;return(s=="number"?t(r)&&f(a,r.length):s=="string"&&a in r)?n(r[a],e):!1}export{u as i};
|
||||
import{b2 as i,aO as t,dN as f,aS as n}from"./index-b01f57e3.js";function u(e,a,r){if(!i(r))return!1;var s=typeof a;return(s=="number"?t(r)&&f(a,r.length):s=="string"&&a in r)?n(r[a],e):!1}export{u as i};
|
||||
|
|
@ -0,0 +1 @@
|
|||
.container[data-v-59d2d572]{background:var(--zp-secondary-background);height:100%;overflow:auto;display:flex;flex-direction:column}.container .actions-panel[data-v-59d2d572]{padding:8px;background-color:var(--zp-primary-background)}.container .actions-panel.actions[data-v-59d2d572]{display:flex;align-items:center;gap:16px;z-index:333}.container .file-list[data-v-59d2d572]{flex:1;z-index:222;list-style:none;padding:8px;height:var(--pane-max-height);width:100%}.container .file-list .hint[data-v-59d2d572]{text-align:center;font-size:2em;padding:30vh 128px 0}
|
||||
|
|
@ -1 +0,0 @@
|
|||
.container[data-v-a2642a17]{background:var(--zp-secondary-background);height:100%;overflow:auto;display:flex;flex-direction:column}.container .actions-panel[data-v-a2642a17]{padding:8px;background-color:var(--zp-primary-background)}.container .actions-panel.actions[data-v-a2642a17]{display:flex;align-items:center;gap:16px;z-index:333}.container .file-list[data-v-a2642a17]{flex:1;z-index:222;list-style:none;padding:8px;height:var(--pane-max-height);width:100%}.container .file-list .hint[data-v-a2642a17]{text-align:center;font-size:2em;padding:30vh 128px 0}
|
||||
|
|
@ -1 +1 @@
|
|||
import{d as z,p as B,c2 as $,bp as S,o as _,j as w,k as f,c as l,C as d,l as p,t as c,E as s,B as A,U as E,c3 as R,c4 as y,W as x,X as T,a3 as U,a6 as V,n as N}from"./index-f2db319b.js";import{F as j,s as L}from"./FileItem-72718f68.js";import{u as H,b as O,j as W}from"./index-0d856f16.js";import"./index-29e38a15.js";import"./shortcut-869fab50.js";import"./_isIterateeCall-dd643bcf.js";const q={class:"actions-panel actions"},G={class:"item"},P={key:0,class:"file-list"},Q={class:"hint"},X=z({__name:"batchDownload",props:{tabIdx:{},paneIdx:{},id:{}},setup(J){const{stackViewEl:b}=H().toRefs(),{itemSize:h,gridItems:D,cellWidth:g}=O(),i=B(),m=W(),{selectdFiles:a}=$(m),r=S(),v=async e=>{const t=R(e);t&&m.addFiles(t.nodes)},C=async()=>{r.pushAction(async()=>{const e=await y.value.post("/zip",{paths:a.value.map(u=>u.fullpath),compress:i.batchDownloadCompress,pack_only:!1},{responseType:"blob"}),t=window.URL.createObjectURL(new Blob([e.data])),o=document.createElement("a");o.href=t,o.setAttribute("download",`iib_${new Date().toLocaleString()}.zip`),document.body.appendChild(o),o.click()})},I=async()=>{r.pushAction(async()=>{await y.value.post("/zip",{paths:a.value.map(e=>e.fullpath),compress:i.batchDownloadCompress,pack_only:!0},{responseType:"blob"}),x.success(T("success"))})},F=e=>{a.value.splice(e,1)};return(e,t)=>{const o=U,u=V;return _(),w("div",{class:"container",ref_key:"stackViewEl",ref:b,onDrop:v},[f("div",q,[l(o,{onClick:t[0]||(t[0]=n=>s(m).selectdFiles=[])},{default:d(()=>[p(c(e.$t("clear")),1)]),_:1}),f("div",G,[p(c(e.$t("compressFile"))+": ",1),l(u,{checked:s(i).batchDownloadCompress,"onUpdate:checked":t[1]||(t[1]=n=>s(i).batchDownloadCompress=n)},null,8,["checked"])]),l(o,{onClick:I,type:"primary",loading:!s(r).isIdle},{default:d(()=>[p(c(e.$t("packOnlyNotDownload")),1)]),_:1},8,["loading"]),l(o,{onClick:C,type:"primary",loading:!s(r).isIdle},{default:d(()=>[p(c(e.$t("zipDownload")),1)]),_:1},8,["loading"])]),s(a).length?(_(),A(s(L),{key:1,ref:"scroller",class:"file-list",items:s(a).slice(),"item-size":s(h).first,"key-field":"fullpath","item-secondary-size":s(h).second,gridItems:s(D)},{default:d(({item:n,index:k})=>[l(j,{idx:k,file:n,"cell-width":s(g),"enable-close-icon":"",onCloseIconClick:K=>F(k),"full-screen-preview-image-url":s(E)(n),"enable-right-click-menu":!1},null,8,["idx","file","cell-width","onCloseIconClick","full-screen-preview-image-url"])]),_:1},8,["items","item-size","item-secondary-size","gridItems"])):(_(),w("div",P,[f("p",Q,c(e.$t("batchDownloaDDragAndDropHint")),1)]))],544)}}});const oe=N(X,[["__scopeId","data-v-a2642a17"]]);export{oe as default};
|
||||
import{d as B,p as F,c2 as $,bp as S,o as _,j as w,k as f,c as l,C as d,l as p,t as c,E as s,B as A,U as E,c3 as x,c4 as y,W as R,X as T,a3 as U,a6 as V,n as N}from"./index-b01f57e3.js";import{F as j,s as L}from"./FileItem-56a257c5.js";import{u as H,b as O,j as W}from"./index-59dc4640.js";import"./index-482cb1cf.js";import"./shortcut-bfd52548.js";import"./_isIterateeCall-f668c4e1.js";const q={class:"actions-panel actions"},G={class:"item"},P={key:0,class:"file-list"},Q={class:"hint"},X=B({__name:"batchDownload",props:{tabIdx:{},paneIdx:{},id:{}},setup(J){const{stackViewEl:b}=H().toRefs(),{itemSize:h,gridItems:D,cellWidth:g}=O(),i=F(),m=W(),{selectdFiles:a}=$(m),r=S(),v=async e=>{const t=x(e);t&&m.addFiles(t.nodes)},C=async()=>{r.pushAction(async()=>{const e=await y.value.post("/zip",{paths:a.value.map(u=>u.fullpath),compress:i.batchDownloadCompress,pack_only:!1},{responseType:"blob"}),t=window.URL.createObjectURL(new Blob([e.data])),o=document.createElement("a");o.href=t,o.setAttribute("download",`iib_${new Date().toLocaleString()}.zip`),document.body.appendChild(o),o.click()})},I=async()=>{r.pushAction(async()=>{await y.value.post("/zip",{paths:a.value.map(e=>e.fullpath),compress:i.batchDownloadCompress,pack_only:!0},{responseType:"blob"}),R.success(T("success"))})},z=e=>{a.value.splice(e,1)};return(e,t)=>{const o=U,u=V;return _(),w("div",{class:"container",ref_key:"stackViewEl",ref:b,onDrop:v},[f("div",q,[l(o,{onClick:t[0]||(t[0]=n=>s(m).selectdFiles=[])},{default:d(()=>[p(c(e.$t("clear")),1)]),_:1}),f("div",G,[p(c(e.$t("compressFile"))+": ",1),l(u,{checked:s(i).batchDownloadCompress,"onUpdate:checked":t[1]||(t[1]=n=>s(i).batchDownloadCompress=n)},null,8,["checked"])]),l(o,{onClick:I,type:"primary",loading:!s(r).isIdle},{default:d(()=>[p(c(e.$t("packOnlyNotDownload")),1)]),_:1},8,["loading"]),l(o,{onClick:C,type:"primary",loading:!s(r).isIdle},{default:d(()=>[p(c(e.$t("zipDownload")),1)]),_:1},8,["loading"])]),s(a).length?(_(),A(s(L),{key:1,ref:"scroller",class:"file-list",items:s(a).slice(),"item-size":s(h).first,"key-field":"fullpath","item-secondary-size":s(h).second,gridItems:s(D)},{default:d(({item:n,index:k})=>[l(j,{idx:k,file:n,"cell-width":s(g),"enable-close-icon":"",onCloseIconClick:K=>z(k),"full-screen-preview-image-url":s(E)(n),"enable-right-click-menu":!1},null,8,["idx","file","cell-width","onCloseIconClick","full-screen-preview-image-url"])]),_:1},8,["items","item-size","item-secondary-size","gridItems"])):(_(),w("div",P,[f("p",Q,c(e.$t("batchDownloaDDragAndDropHint")),1)]))],544)}}});const oe=N(X,[["__scopeId","data-v-59d2d572"]]);export{oe as default};
|
||||
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
|
|
@ -0,0 +1 @@
|
|||
.container[data-v-4c5d9776]{background:var(--zp-secondary-background);height:100%;overflow:auto;display:flex;flex-direction:column}.container .actions-panel[data-v-4c5d9776]{padding:8px;background-color:var(--zp-primary-background)}.container .file-list[data-v-4c5d9776]{flex:1;list-style:none;padding:8px;height:var(--pane-max-height);width:100%}.container .file-list .hint[data-v-4c5d9776]{text-align:center;font-size:2em;padding:30vh 128px 0}
|
||||
|
|
@ -0,0 +1 @@
|
|||
import{F as w,s as y}from"./FileItem-56a257c5.js";import{u as k,b as x}from"./index-59dc4640.js";import{d as I,p as b,bm as h,r as C,c5 as D,c6 as F,o as E,j as V,c as n,C as z,E as e,U as S,c3 as B,c7 as A,n as K}from"./index-b01f57e3.js";import"./index-482cb1cf.js";import"./shortcut-bfd52548.js";import"./_isIterateeCall-f668c4e1.js";const R=I({__name:"gridView",props:{tabIdx:{},paneIdx:{},id:{},removable:{type:Boolean},allowDragAndDrop:{type:Boolean},files:{},paneKey:{}},setup(p){const o=p,m=b(),{stackViewEl:d}=k().toRefs(),{itemSize:i,gridItems:u,cellWidth:g}=x(),f=h(),s=C(o.files??[]),_=async t=>{const l=B(t);o.allowDragAndDrop&&l&&(s.value=A([...s.value,...l.nodes]))},v=t=>{s.value.splice(t,1)};return D(()=>{m.pageFuncExportMap.set(o.paneKey,{getFiles:()=>F(s.value),setFiles:t=>s.value=t})}),(t,l)=>(E(),V("div",{class:"container",ref_key:"stackViewEl",ref:d,onDrop:_},[n(e(y),{ref:"scroller",class:"file-list",items:s.value.slice(),"item-size":e(i).first,"key-field":"fullpath","item-secondary-size":e(i).second,gridItems:e(u)},{default:z(({item:a,index:r})=>{var c;return[n(w,{idx:r,file:a,"cell-width":e(g),"enable-close-icon":o.removable,onCloseIconClick:T=>v(r),"full-screen-preview-image-url":e(S)(a),"extra-tags":(c=a==null?void 0:a.tags)==null?void 0:c.map(e(f).tagConvert),"enable-right-click-menu":!1},null,8,["idx","file","cell-width","enable-close-icon","onCloseIconClick","full-screen-preview-image-url","extra-tags"])]}),_:1},8,["items","item-size","item-secondary-size","gridItems"])],544))}});const M=K(R,[["__scopeId","data-v-4c5d9776"]]);export{M as default};
|
||||
|
|
@ -1 +0,0 @@
|
|||
import{F as w,s as y}from"./FileItem-72718f68.js";import{u as k,b as x}from"./index-0d856f16.js";import{d as F,p as b,bm as h,r as C,c5 as D,c6 as I,o as E,j as V,c,C as z,E as e,U as S,c3 as B,c7 as R,n as A}from"./index-f2db319b.js";import"./index-29e38a15.js";import"./shortcut-869fab50.js";import"./_isIterateeCall-dd643bcf.js";const K=F({__name:"gridView",props:{tabIdx:{},paneIdx:{},id:{},removable:{type:Boolean},allowDragAndDrop:{type:Boolean},files:{},paneKey:{}},setup(p){const o=p,m=b(),{stackViewEl:d}=k().toRefs(),{itemSize:i,gridItems:u,cellWidth:f}=x(),g=h(),s=C(o.files??[]),_=async t=>{const l=B(t);o.allowDragAndDrop&&l&&(s.value=R([...s.value,...l.nodes]))},v=t=>{s.value.splice(t,1)};return D(()=>{m.pageFuncExportMap.set(o.paneKey,{getFiles:()=>I(s.value),setFiles:t=>s.value=t})}),(t,l)=>(E(),V("div",{class:"container",ref_key:"stackViewEl",ref:d,onDrop:_},[c(e(y),{ref:"scroller",class:"file-list",items:s.value.slice(),"item-size":e(i).first,"key-field":"fullpath","item-secondary-size":e(i).second,gridItems:e(u)},{default:z(({item:a,index:r})=>{var n;return[c(w,{idx:r,file:a,"cell-width":e(f),"enable-close-icon":o.removable,onCloseIconClick:T=>v(r),"full-screen-preview-image-url":e(S)(a),"extra-tags":(n=a==null?void 0:a.tags)==null?void 0:n.map(e(g).tagConvert),"enable-right-click-menu":!1},null,8,["idx","file","cell-width","enable-close-icon","onCloseIconClick","full-screen-preview-image-url","extra-tags"])]}),_:1},8,["items","item-size","item-secondary-size","gridItems"])],544))}});const M=A(K,[["__scopeId","data-v-f35f4802"]]);export{M as default};
|
||||
|
|
@ -1 +0,0 @@
|
|||
.container[data-v-f35f4802]{background:var(--zp-secondary-background);height:100%;overflow:auto;display:flex;flex-direction:column}.container .actions-panel[data-v-f35f4802]{padding:8px;background-color:var(--zp-primary-background)}.container .file-list[data-v-f35f4802]{flex:1;list-style:none;padding:8px;height:var(--pane-max-height);width:100%}.container .file-list .hint[data-v-f35f4802]{text-align:center;font-size:2em;padding:30vh 128px 0}
|
||||
|
|
@ -1 +1 @@
|
|||
import{aL as F,r as g,bF as P,bG as S,ar as A,aj as R,bp as q,bH as L,bI as j}from"./index-f2db319b.js";import{u as z,b as H,f as O,c as Q,d as T,e as U,i as W,h as B}from"./index-0d856f16.js";let K=0;const V=()=>++K,X=(n,i,{dataUpdateStrategy:l="replace"}={})=>{const a=F([""]),c=g(!1),t=g(),o=g(!1);let f=g(-1);const v=new Set,w=e=>{var s;l==="replace"?t.value=e:l==="merge"&&(A((Array.isArray(t.value)||typeof t.value>"u")&&Array.isArray(e),"数据更新策略为合并时仅可用于值为数组的情况"),t.value=[...(s=t==null?void 0:t.value)!==null&&s!==void 0?s:[],...e])},d=e=>S(void 0,void 0,void 0,function*(){if(o.value||c.value&&typeof e>"u")return!1;o.value=!0;const s=V();f.value=s;try{let r;if(typeof e=="number"){if(r=a[e],typeof r!="string")return!1}else r=a[a.length-1];const m=yield n(r);if(v.has(s))return v.delete(s),!1;w(i(m));const u=m.cursor;if((e===a.length-1||typeof e!="number")&&(c.value=!u.has_next,u.has_next)){const y=u.next_cursor||u.next;A(typeof y=="string"),a.push(y)}}finally{f.value===s&&(o.value=!1)}return!0}),I=()=>{v.add(f.value),o.value=!1},x=(e=!1)=>S(void 0,void 0,void 0,function*(){const{refetch:s,force:r}=typeof e=="object"?e:{refetch:e};r&&I(),A(!o.value),a.splice(0,a.length,""),o.value=!1,t.value=void 0,c.value=!1,s&&(yield d())}),h=()=>({next:()=>S(void 0,void 0,void 0,function*(){if(o.value)throw new Error("不允许同时迭代");return{done:!(yield d()),value:t.value}})});return P({abort:I,load:c,next:d,res:t,loading:o,cursorStack:a,reset:x,[Symbol.asyncIterator]:h,iter:{[Symbol.asyncIterator]:h}})},ee=n=>F(X(n,i=>i.files,{dataUpdateStrategy:"merge"})),te=n=>{const i=F(new Set),l=R(()=>(n.res??[]).filter(p=>!i.has(p.fullpath))),a=q(),{stackViewEl:c,multiSelectedIdxs:t,stack:o,scroller:f,props:v}=z({images:l}).toRefs(),{itemSize:w,gridItems:d,cellWidth:I,onScroll:x}=H({fetchNext:()=>n.next()}),{showMenuIdx:h}=O(),{onFileDragStart:e,onFileDragEnd:s}=Q(),{showGenInfo:r,imageGenInfo:m,q:u,onContextMenuClick:y,onFileItemClick:C}=T({openNext:L}),{previewIdx:_,previewing:E,onPreviewVisibleChange:M,previewImgMove:D,canPreview:G}=U({loadNext:()=>n.next()}),J=async(p,b,N)=>{o.value=[{curr:"",files:l.value}],await y(p,b,N)};W("removeFiles",async({paths:p})=>{p.forEach(b=>i.add(b))});const k=()=>{j(l.value)};return{images:l,scroller:f,queue:a,iter:n,onContextMenuClickU:J,stackViewEl:c,previewIdx:_,previewing:E,onPreviewVisibleChange:M,previewImgMove:D,canPreview:G,itemSize:w,gridItems:d,showGenInfo:r,imageGenInfo:m,q:u,onContextMenuClick:y,onFileItemClick:C,showMenuIdx:h,multiSelectedIdxs:t,onFileDragStart:e,onFileDragEnd:s,cellWidth:I,onScroll:x,saveLoadedFileAsJson:k,saveAllFileAsJson:async()=>{for(;!n.load;)await n.next();k()},props:v,...B()}};export{ee as c,te as u};
|
||||
import{aL as F,r as g,bF as P,bG as S,ar as A,aj as R,bp as q,bH as L,bI as j}from"./index-b01f57e3.js";import{u as z,b as H,f as O,c as Q,d as T,e as U,i as W,h as B}from"./index-59dc4640.js";let K=0;const V=()=>++K,X=(n,i,{dataUpdateStrategy:l="replace"}={})=>{const a=F([""]),c=g(!1),t=g(),o=g(!1);let f=g(-1);const v=new Set,w=e=>{var s;l==="replace"?t.value=e:l==="merge"&&(A((Array.isArray(t.value)||typeof t.value>"u")&&Array.isArray(e),"数据更新策略为合并时仅可用于值为数组的情况"),t.value=[...(s=t==null?void 0:t.value)!==null&&s!==void 0?s:[],...e])},d=e=>S(void 0,void 0,void 0,function*(){if(o.value||c.value&&typeof e>"u")return!1;o.value=!0;const s=V();f.value=s;try{let r;if(typeof e=="number"){if(r=a[e],typeof r!="string")return!1}else r=a[a.length-1];const m=yield n(r);if(v.has(s))return v.delete(s),!1;w(i(m));const u=m.cursor;if((e===a.length-1||typeof e!="number")&&(c.value=!u.has_next,u.has_next)){const y=u.next_cursor||u.next;A(typeof y=="string"),a.push(y)}}finally{f.value===s&&(o.value=!1)}return!0}),I=()=>{v.add(f.value),o.value=!1},x=(e=!1)=>S(void 0,void 0,void 0,function*(){const{refetch:s,force:r}=typeof e=="object"?e:{refetch:e};r&&I(),A(!o.value),a.splice(0,a.length,""),o.value=!1,t.value=void 0,c.value=!1,s&&(yield d())}),h=()=>({next:()=>S(void 0,void 0,void 0,function*(){if(o.value)throw new Error("不允许同时迭代");return{done:!(yield d()),value:t.value}})});return P({abort:I,load:c,next:d,res:t,loading:o,cursorStack:a,reset:x,[Symbol.asyncIterator]:h,iter:{[Symbol.asyncIterator]:h}})},ee=n=>F(X(n,i=>i.files,{dataUpdateStrategy:"merge"})),te=n=>{const i=F(new Set),l=R(()=>(n.res??[]).filter(p=>!i.has(p.fullpath))),a=q(),{stackViewEl:c,multiSelectedIdxs:t,stack:o,scroller:f,props:v}=z({images:l}).toRefs(),{itemSize:w,gridItems:d,cellWidth:I,onScroll:x}=H({fetchNext:()=>n.next()}),{showMenuIdx:h}=O(),{onFileDragStart:e,onFileDragEnd:s}=Q(),{showGenInfo:r,imageGenInfo:m,q:u,onContextMenuClick:y,onFileItemClick:C}=T({openNext:L}),{previewIdx:_,previewing:E,onPreviewVisibleChange:M,previewImgMove:D,canPreview:G}=U({loadNext:()=>n.next()}),J=async(p,b,N)=>{o.value=[{curr:"",files:l.value}],await y(p,b,N)};W("removeFiles",async({paths:p})=>{p.forEach(b=>i.add(b))});const k=()=>{j(l.value)};return{images:l,scroller:f,queue:a,iter:n,onContextMenuClickU:J,stackViewEl:c,previewIdx:_,previewing:E,onPreviewVisibleChange:M,previewImgMove:D,canPreview:G,itemSize:w,gridItems:d,showGenInfo:r,imageGenInfo:m,q:u,onContextMenuClick:y,onFileItemClick:C,showMenuIdx:h,multiSelectedIdxs:t,onFileDragStart:e,onFileDragEnd:s,cellWidth:I,onScroll:x,saveLoadedFileAsJson:k,saveAllFileAsJson:async()=>{for(;!n.load;)await n.next();k()},props:v,...B()}};export{ee as c,te as u};
|
||||
File diff suppressed because one or more lines are too long
|
|
@ -1 +0,0 @@
|
|||
.ant-empty{margin:0 8px;font-size:14px;line-height:1.5715;text-align:center}.ant-empty-image{height:100px;margin-bottom:8px}.ant-empty-image img{height:100%}.ant-empty-image svg{height:100%;margin:auto}.ant-empty-footer{margin-top:16px}.ant-empty-normal{margin:32px 0;color:#00000040}.ant-empty-normal .ant-empty-image{height:40px}.ant-empty-small{margin:8px 0;color:#00000040}.ant-empty-small .ant-empty-image{height:35px}.ant-empty-img-default-ellipse{fill:#f5f5f5;fill-opacity:.8}.ant-empty-img-default-path-1{fill:#aeb8c2}.ant-empty-img-default-path-2{fill:url(#linearGradient-1)}.ant-empty-img-default-path-3{fill:#f5f5f7}.ant-empty-img-default-path-4,.ant-empty-img-default-path-5{fill:#dce0e6}.ant-empty-img-default-g{fill:#fff}.ant-empty-img-simple-ellipse{fill:#f5f5f5}.ant-empty-img-simple-g{stroke:#d9d9d9}.ant-empty-img-simple-path{fill:#fafafa}.ant-empty-rtl{direction:rtl}
|
||||
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
|
|
@ -1 +1 @@
|
|||
import{d as x,p as $,an as g,c8 as b,r as w,o as p,j as d,k as a,c as l,C as i,l as u,t as n,F as B,K as I,E as m,as as W,W as _,X as v,a2 as D,a3 as F,c9 as N,n as R}from"./index-f2db319b.js";const V={class:"container"},E={class:"actions"},K={class:"uni-desc"},L={class:"snapshot"},j=x({__name:"index",props:{tabIdx:{},paneIdx:{},id:{},paneKey:{}},setup(q){const h=$(),t=g(),f=e=>{h.tabList=W(e.tabs)},k=b(async e=>{await N(`workspace_snapshot_${e.id}`),t.snapshots=t.snapshots.filter(c=>c.id!==e.id),_.success(v("deleteSuccess"))}),o=w(""),C=async()=>{if(!o.value){_.error(v("nameRequired"));return}const e=t.createSnapshot(o.value);await t.addSnapshot(e),_.success(v("saveCompleted"))};return(e,c)=>{const y=D,r=F;return p(),d("div",V,[a("div",E,[l(y,{value:o.value,"onUpdate:value":c[0]||(c[0]=s=>o.value=s),placeholder:e.$t("name"),style:{"max-width":"300px"}},null,8,["value","placeholder"]),l(r,{type:"primary",onClick:C},{default:i(()=>[u(n(e.$t("saveWorkspaceSnapshot")),1)]),_:1})]),a("p",K,n(e.$t("WorkspaceSnapshotDesc")),1),a("ul",L,[(p(!0),d(B,null,I(m(t).snapshots,s=>(p(),d("li",{key:s.id},[a("div",null,[a("span",null,n(s.name),1)]),a("div",null,[l(r,{onClick:S=>f(s)},{default:i(()=>[u(n(e.$t("restore")),1)]),_:2},1032,["onClick"]),l(r,{onClick:S=>m(k)(s)},{default:i(()=>[u(n(e.$t("remove")),1)]),_:2},1032,["onClick"])])]))),128))])])}}});const G=R(j,[["__scopeId","data-v-2c44013c"]]);export{G as default};
|
||||
import{d as x,p as $,an as g,c8 as b,r as w,o as p,j as d,k as a,c as l,C as i,l as u,t as n,F as B,K as I,E as m,as as W,W as _,X as v,a2 as D,a3 as F,c9 as N,n as R}from"./index-b01f57e3.js";const V={class:"container"},E={class:"actions"},K={class:"uni-desc"},L={class:"snapshot"},j=x({__name:"index",props:{tabIdx:{},paneIdx:{},id:{},paneKey:{}},setup(q){const h=$(),t=g(),f=e=>{h.tabList=W(e.tabs)},k=b(async e=>{await N(`workspace_snapshot_${e.id}`),t.snapshots=t.snapshots.filter(c=>c.id!==e.id),_.success(v("deleteSuccess"))}),o=w(""),C=async()=>{if(!o.value){_.error(v("nameRequired"));return}const e=t.createSnapshot(o.value);await t.addSnapshot(e),_.success(v("saveCompleted"))};return(e,c)=>{const y=D,r=F;return p(),d("div",V,[a("div",E,[l(y,{value:o.value,"onUpdate:value":c[0]||(c[0]=s=>o.value=s),placeholder:e.$t("name"),style:{"max-width":"300px"}},null,8,["value","placeholder"]),l(r,{type:"primary",onClick:C},{default:i(()=>[u(n(e.$t("saveWorkspaceSnapshot")),1)]),_:1})]),a("p",K,n(e.$t("WorkspaceSnapshotDesc")),1),a("ul",L,[(p(!0),d(B,null,I(m(t).snapshots,s=>(p(),d("li",{key:s.id},[a("div",null,[a("span",null,n(s.name),1)]),a("div",null,[l(r,{onClick:S=>f(s)},{default:i(()=>[u(n(e.$t("restore")),1)]),_:2},1032,["onClick"]),l(r,{onClick:S=>m(k)(s)},{default:i(()=>[u(n(e.$t("remove")),1)]),_:2},1032,["onClick"])])]))),128))])])}}});const G=R(j,[["__scopeId","data-v-2c44013c"]]);export{G as default};
|
||||
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
|
|
@ -0,0 +1 @@
|
|||
import{d as Y,p as ee,r as N,aC as te,aD as le,v as se,ca as ie,o as v,j as A,c as s,E as e,k as g,C as n,l as k,t as u,G as F,I as oe,H as ae,U as ne,V as R,m as $,B as re,W as w,X as ce,cb as de,a3 as ue,a1 as me,Z as fe,n as pe}from"./index-b01f57e3.js";import{F as ve,s as ge}from"./FileItem-56a257c5.js";import{u as ke,g as we,c as he,b as Ce,d as Se,e as Ie,o as z}from"./index-59dc4640.js";import{M as _e,L as be,R as ye,f as xe}from"./MultiSelectKeep-3861be7f.js";import"./index-482cb1cf.js";import"./shortcut-bfd52548.js";import"./_isIterateeCall-f668c4e1.js";const Ve={class:"refresh-button"},Me={class:"hint"},Te={key:0,class:"preview-switch"},Ne=Y({__name:"randomImage",props:{tabIdx:{},paneIdx:{},id:{},paneKey:{}},setup(Ae){const B=ee(),m=N(!1),l=N([]),r=l,h=te(`${le}randomImageSettingNotificationShown`,!1),P=()=>{h.value||(w.info({content:ce("randomImageSettingNotification"),duration:6,key:"randomImageSetting"}),h.value=!0)},f=async()=>{try{m.value=!0;const i=await de();i.length===0&&w.warn("No data, please generate index in image search page first"),l.value=i}finally{m.value=!1,I()}},C=()=>{if(l.value.length===0){w.warn("没有图片可以浏览");return}z(l.value,a.value||0)};se(()=>{f(),setTimeout(()=>{P()},2e3)});const{stackViewEl:D,multiSelectedIdxs:p,stack:E,scroller:G}=ke({images:l}).toRefs(),{onClearAllSelected:U,onSelectAll:K,onReverseSelect:L}=we();he();const{itemSize:S,gridItems:O,cellWidth:W,onScroll:I}=Ce(),{showGenInfo:c,imageGenInfo:_,q:H,onContextMenuClick:j,onFileItemClick:q}=Se({openNext:ie}),{previewIdx:a,previewing:b,onPreviewVisibleChange:Q,previewImgMove:y,canPreview:x}=Ie(),V=async(i,t,d)=>{E.value=[{curr:"",files:l.value}],await j(i,t,d)};return(i,t)=>{var M;const d=ue,X=me,Z=fe;return v(),A("div",{class:"container",ref_key:"stackViewEl",ref:D},[s(_e,{show:!!e(p).length||e(B).keepMultiSelect,onClearAllSelected:e(U),onSelectAll:e(K),onReverseSelect:e(L)},null,8,["show","onClearAllSelected","onSelectAll","onReverseSelect"]),g("div",Ve,[s(d,{onClick:f,onTouchstart:F(f,["prevent"]),type:"primary",loading:m.value,shape:"round"},{default:n(()=>[k(u(i.$t("shuffle")),1)]),_:1},8,["onTouchstart","loading"]),s(d,{onClick:C,onTouchstart:F(C,["prevent"]),type:"default",disabled:!((M=l.value)!=null&&M.length),shape:"round"},{default:n(()=>[k(u(i.$t("tiktokView")),1)]),_:1},8,["onTouchstart","disabled"])]),s(Z,{visible:e(c),"onUpdate:visible":t[1]||(t[1]=o=>ae(c)?c.value=o:null),width:"70vw","mask-closable":"",onOk:t[2]||(t[2]=o=>c.value=!1)},{cancelText:n(()=>[]),default:n(()=>[s(X,{active:"",loading:!e(H).isIdle},{default:n(()=>[g("div",{style:{width:"100%","word-break":"break-all","white-space":"pre-line","max-height":"70vh",overflow:"auto"},onDblclick:t[0]||(t[0]=o=>e(oe)(e(_)))},[g("div",Me,u(i.$t("doubleClickToCopy")),1),k(" "+u(e(_)),1)],32)]),_:1},8,["loading"])]),_:1},8,["visible"]),s(e(ge),{ref_key:"scroller",ref:G,class:"file-list",items:l.value.slice(),"item-size":e(S).first,"key-field":"fullpath","item-secondary-size":e(S).second,gridItems:e(O),onScroll:e(I)},{default:n(({item:o,index:T})=>[s(ve,{idx:T,file:o,"cell-width":e(W),"full-screen-preview-image-url":e(r)[e(a)]?e(ne)(e(r)[e(a)]):"",onContextMenuClick:V,onPreviewVisibleChange:e(Q),"is-selected-mutil-files":e(p).length>1,selected:e(p).includes(T),onFileItemClick:e(q),onTiktokView:(Fe,J)=>e(z)(l.value,J)},null,8,["idx","file","cell-width","full-screen-preview-image-url","onPreviewVisibleChange","is-selected-mutil-files","selected","onFileItemClick","onTiktokView"])]),_:1},8,["items","item-size","item-secondary-size","gridItems","onScroll"]),e(b)?(v(),A("div",Te,[s(e(be),{onClick:t[3]||(t[3]=o=>e(y)("prev")),class:R({disable:!e(x)("prev")})},null,8,["class"]),s(e(ye),{onClick:t[4]||(t[4]=o=>e(y)("next")),class:R({disable:!e(x)("next")})},null,8,["class"])])):$("",!0),e(b)&&e(r)&&e(r)[e(a)]?(v(),re(xe,{key:1,file:e(r)[e(a)],idx:e(a),onContextMenuClick:V},null,8,["file","idx"])):$("",!0)],512)}}});const Ge=pe(Ne,[["__scopeId","data-v-fc8b4c69"]]);export{Ge as default};
|
||||
|
|
@ -1 +0,0 @@
|
|||
import{d as Y,p as ee,r as F,aC as te,aD as le,v as se,ca as ie,o as v,j as N,c as s,E as e,k as g,C as n,l as k,t as u,G as R,I as oe,H as ae,U as ne,V as A,m as $,B as re,W as w,X as ce,cb as de,a3 as ue,a1 as me,Z as fe,n as pe}from"./index-f2db319b.js";import{F as ve,s as ge}from"./FileItem-72718f68.js";import{u as ke,g as we,c as he,b as Ce,d as Se,e as _e,o as z}from"./index-0d856f16.js";import{M as Ie,L as ye,R as be,f as xe}from"./MultiSelectKeep-a11efe88.js";import"./index-29e38a15.js";import"./shortcut-869fab50.js";import"./_isIterateeCall-dd643bcf.js";/* empty css */const Ve={class:"refresh-button"},Me={class:"hint"},Te={key:0,class:"preview-switch"},Fe=Y({__name:"randomImage",props:{tabIdx:{},paneIdx:{},id:{},paneKey:{}},setup(Ne){const B=ee(),m=F(!1),l=F([]),r=l,h=te(`${le}randomImageSettingNotificationShown`,!1),P=()=>{h.value||(w.info({content:ce("randomImageSettingNotification"),duration:6,key:"randomImageSetting"}),h.value=!0)},f=async()=>{try{m.value=!0;const i=await de();i.length===0&&w.warn("No data, please generate index in image search page first"),l.value=i}finally{m.value=!1,_()}},C=()=>{if(l.value.length===0){w.warn("没有图片可以浏览");return}z(l.value,a.value||0)};se(()=>{f(),setTimeout(()=>{P()},2e3)});const{stackViewEl:D,multiSelectedIdxs:p,stack:E,scroller:G}=ke({images:l}).toRefs(),{onClearAllSelected:U,onSelectAll:K,onReverseSelect:L}=we();he();const{itemSize:S,gridItems:O,cellWidth:W,onScroll:_}=Ce(),{showGenInfo:c,imageGenInfo:I,q:H,onContextMenuClick:j,onFileItemClick:q}=Se({openNext:ie}),{previewIdx:a,previewing:y,onPreviewVisibleChange:Q,previewImgMove:b,canPreview:x}=_e(),V=async(i,t,d)=>{E.value=[{curr:"",files:l.value}],await j(i,t,d)};return(i,t)=>{var M;const d=ue,X=me,Z=fe;return v(),N("div",{class:"container",ref_key:"stackViewEl",ref:D},[s(Ie,{show:!!e(p).length||e(B).keepMultiSelect,onClearAllSelected:e(U),onSelectAll:e(K),onReverseSelect:e(L)},null,8,["show","onClearAllSelected","onSelectAll","onReverseSelect"]),g("div",Ve,[s(d,{onClick:f,onTouchstart:R(f,["prevent"]),type:"primary",loading:m.value,shape:"round"},{default:n(()=>[k(u(i.$t("shuffle")),1)]),_:1},8,["onTouchstart","loading"]),s(d,{onClick:C,onTouchstart:R(C,["prevent"]),type:"default",disabled:!((M=l.value)!=null&&M.length),shape:"round"},{default:n(()=>[k(u(i.$t("tiktokView")),1)]),_:1},8,["onTouchstart","disabled"])]),s(Z,{visible:e(c),"onUpdate:visible":t[1]||(t[1]=o=>ae(c)?c.value=o:null),width:"70vw","mask-closable":"",onOk:t[2]||(t[2]=o=>c.value=!1)},{cancelText:n(()=>[]),default:n(()=>[s(X,{active:"",loading:!e(H).isIdle},{default:n(()=>[g("div",{style:{width:"100%","word-break":"break-all","white-space":"pre-line","max-height":"70vh",overflow:"auto"},onDblclick:t[0]||(t[0]=o=>e(oe)(e(I)))},[g("div",Me,u(i.$t("doubleClickToCopy")),1),k(" "+u(e(I)),1)],32)]),_:1},8,["loading"])]),_:1},8,["visible"]),s(e(ge),{ref_key:"scroller",ref:G,class:"file-list",items:l.value.slice(),"item-size":e(S).first,"key-field":"fullpath","item-secondary-size":e(S).second,gridItems:e(O),onScroll:e(_)},{default:n(({item:o,index:T})=>[s(ve,{idx:T,file:o,"cell-width":e(W),"full-screen-preview-image-url":e(r)[e(a)]?e(ne)(e(r)[e(a)]):"",onContextMenuClick:V,onPreviewVisibleChange:e(Q),"is-selected-mutil-files":e(p).length>1,selected:e(p).includes(T),onFileItemClick:e(q),onTiktokView:(Re,J)=>e(z)(l.value,J)},null,8,["idx","file","cell-width","full-screen-preview-image-url","onPreviewVisibleChange","is-selected-mutil-files","selected","onFileItemClick","onTiktokView"])]),_:1},8,["items","item-size","item-secondary-size","gridItems","onScroll"]),e(y)?(v(),N("div",Te,[s(e(ye),{onClick:t[3]||(t[3]=o=>e(b)("prev")),class:A({disable:!e(x)("prev")})},null,8,["class"]),s(e(be),{onClick:t[4]||(t[4]=o=>e(b)("next")),class:A({disable:!e(x)("next")})},null,8,["class"])])):$("",!0),e(y)&&e(r)&&e(r)[e(a)]?(v(),re(xe,{key:1,file:e(r)[e(a)],idx:e(a),onContextMenuClick:V},null,8,["file","idx"])):$("",!0)],512)}}});const Ue=pe(Fe,[["__scopeId","data-v-49082269"]]);export{Ue as default};
|
||||
|
|
@ -1 +1 @@
|
|||
.container[data-v-49082269]{background:var(--zp-secondary-background);height:100%;overflow:auto;display:flex;flex-direction:column}.container .actions-panel[data-v-49082269]{padding:8px;background-color:var(--zp-primary-background)}.container .refresh-button[data-v-49082269]{position:absolute;top:90%;left:50%;transform:translate(-50%,-50%);z-index:99;background:white;border-radius:9999px;box-shadow:0 0 20px var(--zp-secondary);padding:4px;display:flex;align-items:center;gap:8px}.container .file-list[data-v-49082269]{flex:1;list-style:none;padding:8px;height:var(--pane-max-height);width:100%}.container .file-list .hint[data-v-49082269]{text-align:center;font-size:2em;padding:30vh 128px 0}
|
||||
.container[data-v-fc8b4c69]{background:var(--zp-secondary-background);height:100%;overflow:auto;display:flex;flex-direction:column}.container .actions-panel[data-v-fc8b4c69]{padding:8px;background-color:var(--zp-primary-background)}.container .refresh-button[data-v-fc8b4c69]{position:absolute;top:90%;left:50%;transform:translate(-50%,-50%);z-index:99;background:white;border-radius:9999px;box-shadow:0 0 20px var(--zp-secondary);padding:4px;display:flex;align-items:center;gap:8px}.container .file-list[data-v-fc8b4c69]{flex:1;list-style:none;padding:8px;height:var(--pane-max-height);width:100%}.container .file-list .hint[data-v-fc8b4c69]{text-align:center;font-size:2em;padding:30vh 128px 0}
|
||||
File diff suppressed because one or more lines are too long
|
|
@ -1 +1 @@
|
|||
.ant-breadcrumb{box-sizing:border-box;margin:0;padding:0;color:#000000d9;font-variant:tabular-nums;line-height:1.5715;list-style:none;font-feature-settings:"tnum";color:#00000073;font-size:14px}.ant-breadcrumb .anticon{font-size:14px}.ant-breadcrumb a{color:#00000073;transition:color .3s}.ant-breadcrumb a:hover{color:#de632f}.ant-breadcrumb>span:last-child{color:#000000d9}.ant-breadcrumb>span:last-child a{color:#000000d9}.ant-breadcrumb>span:last-child .ant-breadcrumb-separator{display:none}.ant-breadcrumb-separator{margin:0 8px;color:#00000073}.ant-breadcrumb-link>.anticon+span,.ant-breadcrumb-link>.anticon+a{margin-left:4px}.ant-breadcrumb-overlay-link>.anticon{margin-left:4px}.ant-breadcrumb-rtl{direction:rtl}.ant-breadcrumb-rtl:before{display:table;content:""}.ant-breadcrumb-rtl:after{display:table;clear:both;content:""}.ant-breadcrumb-rtl>span{float:right}.ant-breadcrumb-rtl .ant-breadcrumb-link>.anticon+span,.ant-breadcrumb-rtl .ant-breadcrumb-link>.anticon+a{margin-right:4px;margin-left:0}.ant-breadcrumb-rtl .ant-breadcrumb-overlay-link>.anticon{margin-right:4px;margin-left:0}.nprogress{pointer-events:none}.nprogress .bar{background:#29d;position:fixed;z-index:1031;top:0;left:0;width:100%;height:2px}.nprogress .peg{display:block;position:absolute;right:0px;width:100px;height:100%;box-shadow:0 0 10px #29d,0 0 5px #29d;opacity:1;-webkit-transform:rotate(3deg) translate(0px,-4px);-ms-transform:rotate(3deg) translate(0px,-4px);transform:rotate(3deg) translateY(-4px)}.nprogress .spinner{display:block;position:fixed;z-index:1031;top:15px;right:15px}.nprogress .spinner-icon{width:18px;height:18px;box-sizing:border-box;border:solid 2px transparent;border-top-color:#29d;border-left-color:#29d;border-radius:50%;-webkit-animation:nprogress-spinner .4s linear infinite;animation:nprogress-spinner .4s linear infinite}.nprogress-custom-parent{overflow:hidden;position:relative}.nprogress-custom-parent .nprogress .spinner,.nprogress-custom-parent .nprogress .bar{position:absolute}@-webkit-keyframes nprogress-spinner{0%{-webkit-transform:rotate(0deg)}to{-webkit-transform:rotate(360deg)}}@keyframes nprogress-spinner{0%{transform:rotate(0)}to{transform:rotate(360deg)}}.base-info[data-v-afd25667]{position:absolute;padding:4px;font-size:.8em;background:var(--zp-primary-background);color:var(--zp-primary);left:0;bottom:0;border-top-right-radius:4px}.location-act[data-v-9516e28d]{margin-left:8px}.location-act .copy[data-v-9516e28d]{margin-right:4px}@media (max-width: 768px){.location-act[data-v-9516e28d]{display:flex;flex-direction:column}.location-act>*[data-v-9516e28d],.location-act .copy[data-v-9516e28d]{margin:2px}}.breadcrumb[data-v-9516e28d]{display:flex;align-items:center}.breadcrumb>*[data-v-9516e28d]{margin-right:4px}@media (max-width: 768px){.breadcrumb[data-v-9516e28d]{width:100%}.breadcrumb .ant-breadcrumb>*[data-v-9516e28d]{display:inline-block}}.container[data-v-9516e28d]{background:var(--zp-secondary-background);height:var(--pane-max-height)}.location-bar[data-v-9516e28d]{padding:4px 16px;background:var(--zp-primary-background);border-bottom:1px solid var(--zp-border);display:flex;align-items:center;justify-content:space-between}@media (max-width: 768px){.location-bar[data-v-9516e28d]{flex-direction:column}.location-bar[data-v-9516e28d] ::-webkit-scrollbar{height:2px;background-color:var(--zp-secondary-variant-background)}.location-bar .actions[data-v-9516e28d]{padding:4px 0;width:100%;overflow:auto;display:flex;align-items:center}.location-bar .actions>*[data-v-9516e28d]{flex-shrink:0}}.location-bar .actions[data-v-9516e28d]{display:flex;align-items:center;flex-shrink:0}.location-bar a.opt[data-v-9516e28d]{margin-left:8px}.view[data-v-9516e28d]{padding:8px;height:calc(100vh - 48px)}.view .file-list[data-v-9516e28d]{list-style:none;padding:8px;height:100%;overflow:auto}.hint[data-v-9516e28d]{padding:4px;border:4px;background:var(--zp-secondary-background);border:1px solid var(--zp-border)}
|
||||
.ant-breadcrumb{box-sizing:border-box;margin:0;padding:0;color:#000000d9;font-variant:tabular-nums;line-height:1.5715;list-style:none;font-feature-settings:"tnum";color:#00000073;font-size:14px}.ant-breadcrumb .anticon{font-size:14px}.ant-breadcrumb a{color:#00000073;transition:color .3s}.ant-breadcrumb a:hover{color:#de632f}.ant-breadcrumb>span:last-child{color:#000000d9}.ant-breadcrumb>span:last-child a{color:#000000d9}.ant-breadcrumb>span:last-child .ant-breadcrumb-separator{display:none}.ant-breadcrumb-separator{margin:0 8px;color:#00000073}.ant-breadcrumb-link>.anticon+span,.ant-breadcrumb-link>.anticon+a{margin-left:4px}.ant-breadcrumb-overlay-link>.anticon{margin-left:4px}.ant-breadcrumb-rtl{direction:rtl}.ant-breadcrumb-rtl:before{display:table;content:""}.ant-breadcrumb-rtl:after{display:table;clear:both;content:""}.ant-breadcrumb-rtl>span{float:right}.ant-breadcrumb-rtl .ant-breadcrumb-link>.anticon+span,.ant-breadcrumb-rtl .ant-breadcrumb-link>.anticon+a{margin-right:4px;margin-left:0}.ant-breadcrumb-rtl .ant-breadcrumb-overlay-link>.anticon{margin-right:4px;margin-left:0}.nprogress{pointer-events:none}.nprogress .bar{background:#29d;position:fixed;z-index:1031;top:0;left:0;width:100%;height:2px}.nprogress .peg{display:block;position:absolute;right:0px;width:100px;height:100%;box-shadow:0 0 10px #29d,0 0 5px #29d;opacity:1;-webkit-transform:rotate(3deg) translate(0px,-4px);-ms-transform:rotate(3deg) translate(0px,-4px);transform:rotate(3deg) translateY(-4px)}.nprogress .spinner{display:block;position:fixed;z-index:1031;top:15px;right:15px}.nprogress .spinner-icon{width:18px;height:18px;box-sizing:border-box;border:solid 2px transparent;border-top-color:#29d;border-left-color:#29d;border-radius:50%;-webkit-animation:nprogress-spinner .4s linear infinite;animation:nprogress-spinner .4s linear infinite}.nprogress-custom-parent{overflow:hidden;position:relative}.nprogress-custom-parent .nprogress .spinner,.nprogress-custom-parent .nprogress .bar{position:absolute}@-webkit-keyframes nprogress-spinner{0%{-webkit-transform:rotate(0deg)}to{-webkit-transform:rotate(360deg)}}@keyframes nprogress-spinner{0%{transform:rotate(0)}to{transform:rotate(360deg)}}.base-info[data-v-afd25667]{position:absolute;padding:4px;font-size:.8em;background:var(--zp-primary-background);color:var(--zp-primary);left:0;bottom:0;border-top-right-radius:4px}.location-act[data-v-29b8fad9]{margin-left:8px}.location-act .copy[data-v-29b8fad9]{margin-right:4px}@media (max-width: 768px){.location-act[data-v-29b8fad9]{display:flex;flex-direction:column}.location-act>*[data-v-29b8fad9],.location-act .copy[data-v-29b8fad9]{margin:2px}}.breadcrumb[data-v-29b8fad9]{display:flex;align-items:center}.breadcrumb>*[data-v-29b8fad9]{margin-right:4px}@media (max-width: 768px){.breadcrumb[data-v-29b8fad9]{width:100%}.breadcrumb .ant-breadcrumb>*[data-v-29b8fad9]{display:inline-block}}.container[data-v-29b8fad9]{background:var(--zp-secondary-background);height:var(--pane-max-height)}.location-bar[data-v-29b8fad9]{padding:4px 16px;background:var(--zp-primary-background);border-bottom:1px solid var(--zp-border);display:flex;align-items:center;justify-content:space-between}@media (max-width: 768px){.location-bar[data-v-29b8fad9]{flex-direction:column}.location-bar[data-v-29b8fad9] ::-webkit-scrollbar{height:2px;background-color:var(--zp-secondary-variant-background)}.location-bar .actions[data-v-29b8fad9]{padding:4px 0;width:100%;overflow:auto;display:flex;align-items:center}.location-bar .actions>*[data-v-29b8fad9]{flex-shrink:0}}.location-bar .actions[data-v-29b8fad9]{display:flex;align-items:center;flex-shrink:0}.location-bar a.opt[data-v-29b8fad9]{margin-left:8px}.view[data-v-29b8fad9]{padding:8px;height:calc(100vh - 48px)}.view .file-list[data-v-29b8fad9]{list-style:none;padding:8px;height:100%;overflow:auto}.hint[data-v-29b8fad9]{padding:4px;border:4px;background:var(--zp-secondary-background);border:1px solid var(--zp-border)}
|
||||
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
|
|
@ -7,8 +7,8 @@
|
|||
<link rel="icon" href="/favicon.ico" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||
<title>Infinite Image Browsing</title>
|
||||
<script type="module" crossorigin src="/infinite_image_browsing/fe-static/assets/index-f2db319b.js"></script>
|
||||
<link rel="stylesheet" href="/infinite_image_browsing/fe-static/assets/index-dd273d5b.css">
|
||||
<script type="module" crossorigin src="/infinite_image_browsing/fe-static/assets/index-b01f57e3.js"></script>
|
||||
<link rel="stylesheet" href="/infinite_image_browsing/fe-static/assets/index-882e7f3d.css">
|
||||
</head>
|
||||
|
||||
<body>
|
||||
|
|
|
|||
|
|
@ -8,6 +8,7 @@ import SplitViewTab from '@/page/SplitViewTab/SplitViewTab.vue'
|
|||
import OrganizeJobsPanel from '@/components/OrganizeJobsPanel.vue'
|
||||
import OrganizePreview from '@/page/OrganizeFiles/OrganizePreview.vue'
|
||||
import SmartOrganizeConfigModal from '@/components/SmartOrganizeConfigModal.vue'
|
||||
import PromptEditorModal from '@/components/PromptEditorModal.vue'
|
||||
import { Dict, createReactiveQueue, globalEvents, useGlobalEventListen } from './util'
|
||||
import { resolveQueryActions } from './queryActions'
|
||||
import { refreshTauriConf, tauriConf } from './util/tauriAppConf'
|
||||
|
|
@ -252,6 +253,9 @@ onMounted(async () => {
|
|||
<!-- Smart Organize Config Modal -->
|
||||
<SmartOrganizeConfigModal />
|
||||
|
||||
<!-- Prompt Editor Modal (自包含组件,通过全局事件控制) -->
|
||||
<PromptEditorModal />
|
||||
|
||||
<!-- Fullscreen Loading for Moving Files -->
|
||||
<div v-if="isMovingFiles" class="moving-files-overlay">
|
||||
<div class="moving-files-content">
|
||||
|
|
|
|||
|
|
@ -186,6 +186,11 @@ export const getImageGenerationInfo = async (path: string) => {
|
|||
.data as string
|
||||
}
|
||||
|
||||
export const updateExif = async (path: string, exif: string) => {
|
||||
const resp = await axiosInst.value.post('/update_exif', { path, exif })
|
||||
return resp.data as { success: boolean, message: string }
|
||||
}
|
||||
|
||||
export const getImageExif = async (path: string) => {
|
||||
return (await axiosInst.value.get(`/image_exif?path=${encodeURIComponent(path)}`))
|
||||
.data as Record<string, string>
|
||||
|
|
|
|||
|
|
@ -0,0 +1,254 @@
|
|||
<script setup lang="ts">
|
||||
import { computed, ref } from 'vue'
|
||||
import { toImageUrl } from '@/util/file'
|
||||
import type { FileNodeInfo } from '@/api/files'
|
||||
import { DragOutlined } from '@/icon'
|
||||
|
||||
interface Props {
|
||||
file: FileNodeInfo
|
||||
title?: string
|
||||
size?: string
|
||||
}
|
||||
|
||||
const props = withDefaults(defineProps<Props>(), {
|
||||
title: 'Drag to transfer image'
|
||||
})
|
||||
|
||||
const imageUrl = computed(() => {
|
||||
if (!props.file) return ''
|
||||
return toImageUrl(props.file)
|
||||
})
|
||||
|
||||
const showImage = ref(false)
|
||||
|
||||
function toggleImage() {
|
||||
showImage.value = !showImage.value
|
||||
}
|
||||
const closeImage = () => {
|
||||
showImage.value=false
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="draggable-image-wrapper"
|
||||
@mouseleave="closeImage">
|
||||
<div class="trigger-container" :title="title" @click="toggleImage" >
|
||||
<slot class="trigger-slot">
|
||||
<div class="default-trigger">
|
||||
<DragOutlined class="trigger-icon" />
|
||||
<span class="trigger-text">{{ $t('dragImageToTransfer') }}</span>
|
||||
</div>
|
||||
</slot>
|
||||
<img
|
||||
v-if="showImage"
|
||||
:src="imageUrl"
|
||||
:alt="file.name"
|
||||
draggable="true"
|
||||
class="hover-image"
|
||||
:style="{ width: size||'256px', height: size || '256px' }"
|
||||
@mouseleave="closeImage"
|
||||
@error="(e) => (e.target as HTMLImageElement).style.display = 'none'"
|
||||
@click.stop
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style scoped lang="scss">
|
||||
.draggable-image-wrapper {
|
||||
padding: 4px 0;
|
||||
margin-top: 8px;
|
||||
display: flex;
|
||||
cursor: pointer;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
|
||||
.trigger-container {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
position: relative;
|
||||
user-select: none;
|
||||
|
||||
:deep(.trigger-slot) {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
cursor: pointer;
|
||||
transition: all 0.3s ease;
|
||||
}
|
||||
|
||||
&:active :deep(.trigger-slot) {
|
||||
transform: scale(0.95);
|
||||
}
|
||||
|
||||
:deep(.custom-drag-trigger) {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: 10px;
|
||||
padding: 6px 24px;
|
||||
border-radius: 8px;
|
||||
background: var(--zp-primary-background);
|
||||
color: var(--zp-primary);
|
||||
transition: all 0.3s ease;
|
||||
position: relative;
|
||||
overflow: hidden;
|
||||
|
||||
// 静态多彩渐变边框
|
||||
&::before {
|
||||
content: '';
|
||||
position: absolute;
|
||||
inset: 0;
|
||||
border-radius: 8px;
|
||||
padding: 2px;
|
||||
background: linear-gradient(
|
||||
135deg,
|
||||
#ff6b6b 0%,
|
||||
#feca57 16.67%,
|
||||
#48dbfb 33.33%,
|
||||
#ff9ff3 50%,
|
||||
#54a0ff 66.67%,
|
||||
#5f27cd 83.33%,
|
||||
#ff6b6b 100%
|
||||
);
|
||||
-webkit-mask: linear-gradient(#fff 0 0) content-box, linear-gradient(#fff 0 0);
|
||||
-webkit-mask-composite: xor;
|
||||
mask-composite: exclude;
|
||||
opacity: 0.9;
|
||||
}
|
||||
|
||||
&:hover {
|
||||
background: linear-gradient(
|
||||
135deg,
|
||||
rgba(255, 107, 107, 0.1),
|
||||
rgba(72, 219, 251, 0.1)
|
||||
);
|
||||
box-shadow: 0 4px 16px rgba(0, 0, 0, 0.15);
|
||||
transform: translateY(-1px);
|
||||
}
|
||||
|
||||
&:active {
|
||||
transform: translateY(0);
|
||||
}
|
||||
|
||||
.trigger-icon {
|
||||
font-size: 18px;
|
||||
transition: transform 0.3s ease;
|
||||
position: relative;
|
||||
z-index: 1;
|
||||
}
|
||||
|
||||
.trigger-text {
|
||||
font-size: 12px;
|
||||
white-space: nowrap;
|
||||
position: relative;
|
||||
z-index: 1;
|
||||
letter-spacing: 0.5px;
|
||||
}
|
||||
|
||||
&:hover .trigger-icon {
|
||||
transform: scale(1.1) rotate(5deg);
|
||||
}
|
||||
}
|
||||
|
||||
.default-trigger {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: 10px;
|
||||
padding: 6px 24px;
|
||||
border-radius: 8px;
|
||||
background: var(--zp-primary-background);
|
||||
color: var(--zp-primary);
|
||||
transition: all 0.3s ease;
|
||||
position: relative;
|
||||
overflow: hidden;
|
||||
|
||||
// 静态多彩渐变边框
|
||||
&::before {
|
||||
content: '';
|
||||
position: absolute;
|
||||
inset: 0;
|
||||
border-radius: 8px;
|
||||
padding: 2px;
|
||||
background: linear-gradient(
|
||||
135deg,
|
||||
#ff6b6b 0%,
|
||||
#feca57 16.67%,
|
||||
#48dbfb 33.33%,
|
||||
#ff9ff3 50%,
|
||||
#54a0ff 66.67%,
|
||||
#5f27cd 83.33%,
|
||||
#ff6b6b 100%
|
||||
);
|
||||
-webkit-mask: linear-gradient(#fff 0 0) content-box, linear-gradient(#fff 0 0);
|
||||
-webkit-mask-composite: xor;
|
||||
mask-composite: exclude;
|
||||
opacity: 0.9;
|
||||
}
|
||||
|
||||
&:hover {
|
||||
background: linear-gradient(
|
||||
135deg,
|
||||
rgba(255, 107, 107, 0.1),
|
||||
rgba(72, 219, 251, 0.1)
|
||||
);
|
||||
box-shadow: 0 4px 16px rgba(0, 0, 0, 0.15);
|
||||
transform: translateY(-1px);
|
||||
}
|
||||
|
||||
&:active {
|
||||
transform: translateY(0);
|
||||
}
|
||||
|
||||
.trigger-icon {
|
||||
font-size: 18px;
|
||||
transition: transform 0.3s ease;
|
||||
position: relative;
|
||||
z-index: 1;
|
||||
}
|
||||
|
||||
.trigger-text {
|
||||
font-size: 14px;
|
||||
font-weight: 500;
|
||||
white-space: nowrap;
|
||||
position: relative;
|
||||
z-index: 1;
|
||||
letter-spacing: 0.5px;
|
||||
}
|
||||
|
||||
&:hover .trigger-icon {
|
||||
transform: scale(1.1) rotate(5deg);
|
||||
}
|
||||
}
|
||||
|
||||
.hover-image {
|
||||
position: absolute;
|
||||
top: 50%;
|
||||
left: 50%;
|
||||
transform: translate(-50%, -50%) scale(1);
|
||||
object-fit: contain;
|
||||
border-radius: 8px;
|
||||
box-shadow: 0 8px 32px rgba(0, 0, 0, 0.2);
|
||||
background: var(--zp-primary-background);
|
||||
opacity: 1;
|
||||
pointer-events: auto;
|
||||
animation: scaleIn 0.3s cubic-bezier(0.4, 0, 0.2, 1);
|
||||
z-index: 10000;
|
||||
cursor: grab;
|
||||
|
||||
@keyframes scaleIn {
|
||||
from {
|
||||
transform: translate(-50%, -50%) scale(0.8);
|
||||
opacity: 0;
|
||||
}
|
||||
to {
|
||||
transform: translate(-50%, -50%) scale(1);
|
||||
opacity: 1;
|
||||
}
|
||||
}
|
||||
|
||||
&:active {
|
||||
cursor: grabbing;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
|
@ -1,5 +1,5 @@
|
|||
<script setup lang="ts">
|
||||
import { FileOutlined, FolderOpenOutlined, EllipsisOutlined, HeartOutlined, HeartFilled } from '@/icon'
|
||||
import { FileOutlined, FolderOpenOutlined, EllipsisOutlined, HeartOutlined, HeartFilled, DragOutlined } from '@/icon'
|
||||
import { useGlobalStore } from '@/store/useGlobalStore'
|
||||
import { fallbackImage, ok } from 'vue3-ts-util'
|
||||
import type { FileNodeInfo } from '@/api/files'
|
||||
|
|
@ -9,6 +9,7 @@ import type { MenuInfo } from 'ant-design-vue/lib/menu/src/interface'
|
|||
import { computed, ref, nextTick, watch } from 'vue'
|
||||
import ContextMenu from './ContextMenu.vue'
|
||||
import ChangeIndicator from './ChangeIndicator.vue'
|
||||
import DraggableImage from './DraggableImage.vue'
|
||||
import { useTagStore } from '@/store/useTagStore'
|
||||
import { CloseCircleOutlined, StarFilled, StarOutlined } from '@/icon'
|
||||
import { Tag } from '@/api/db'
|
||||
|
|
@ -282,8 +283,13 @@ const handleAudioClick = () => {
|
|||
</a-menu>
|
||||
</template>
|
||||
</a-dropdown>
|
||||
<DraggableImage size="192px" v-if="file.type === 'file' && isImageFile(file.fullpath)" :file="file">
|
||||
<div class="float-btn-wrap">
|
||||
<DragOutlined />
|
||||
</div>
|
||||
</DraggableImage>
|
||||
</div>
|
||||
<!-- :key="fullScreenPreviewImageUrl ? undefined : file.fullpath"
|
||||
<!-- :key="fullScreenPreviewImageUrl ? undefined : file.fullpath"
|
||||
这么复杂是因为再全屏查看时可能因为直接删除导致fullpath变化,然后整个预览直接退出-->
|
||||
<div :key="file.fullpath" :class="`idx-${idx} item-content`" v-if="isImageFile(file.name)">
|
||||
|
||||
|
|
|
|||
|
|
@ -0,0 +1,283 @@
|
|||
<script setup lang="ts">
|
||||
import { computed, ref, watch } from 'vue'
|
||||
import { deepComputedEffect } from 'vue3-ts-util'
|
||||
import { message } from 'ant-design-vue'
|
||||
import { t } from '@/i18n'
|
||||
|
||||
interface KVPair {
|
||||
key: string
|
||||
value: any
|
||||
}
|
||||
interface KVPairLocal {
|
||||
key: string
|
||||
value: string
|
||||
}
|
||||
|
||||
interface Props {
|
||||
modelValue: KVPair
|
||||
allKeys?: string[]
|
||||
}
|
||||
|
||||
interface Emits {
|
||||
(e: 'update:modelValue', value: KVPair): void
|
||||
(e: 'remove'): void
|
||||
}
|
||||
|
||||
const props = defineProps<Props>()
|
||||
const emit = defineEmits<Emits>()
|
||||
|
||||
// Key 错误状态
|
||||
const keyError = ref('')
|
||||
|
||||
// 校验 key 是否有效
|
||||
const validateKey = (key: string): boolean => {
|
||||
if (!key.trim()) {
|
||||
keyError.value = t('keyRequired')
|
||||
return false
|
||||
}
|
||||
|
||||
if (props.allKeys && props.allKeys.includes(key.trim())) {
|
||||
keyError.value = t('keyMustBeUnique')
|
||||
return false
|
||||
}
|
||||
|
||||
keyError.value = ''
|
||||
return true
|
||||
}
|
||||
|
||||
const modeInitial= () => {
|
||||
const value = localKv.value.value
|
||||
if (!value) return 'str' // 空值默认为字符串模式
|
||||
|
||||
try {
|
||||
const parsed = JSON.parse(value)
|
||||
// 如果解析成功且不是字符串类型,则认为是 JSON 模式
|
||||
return typeof parsed !== 'string' ? 'json' : 'str'
|
||||
} catch {
|
||||
// 解析失败,是字符串模式
|
||||
return 'str'
|
||||
}
|
||||
}
|
||||
// 组件内部状态:判断当前值是否为 JSON 模式
|
||||
const mode = ref('str' as 'str' | 'json')
|
||||
|
||||
// 本地状态,用于双向绑定
|
||||
const localKv = deepComputedEffect<KVPairLocal>({
|
||||
get () {
|
||||
const val = props.modelValue.value
|
||||
const isString = typeof val === 'string' && mode.value !== 'json'
|
||||
return {
|
||||
...props.modelValue,
|
||||
value: isString
|
||||
? val
|
||||
: JSON.stringify(props.modelValue.value, null, 2)
|
||||
}
|
||||
},
|
||||
set (newValue: KVPairLocal) {
|
||||
// 校验 key,如果无效则不 emit
|
||||
if (!validateKey(newValue.key)) {
|
||||
return
|
||||
}
|
||||
|
||||
const value = newValue.value
|
||||
const allVal = {
|
||||
...newValue,
|
||||
value: mode.value === 'json' ? JSON.parse(value) : value
|
||||
}
|
||||
emit('update:modelValue', allVal)
|
||||
}
|
||||
})
|
||||
|
||||
mode.value = modeInitial()
|
||||
|
||||
|
||||
|
||||
|
||||
// 判断当前值是否为有效 JSON
|
||||
const isValidJson = ref(true)
|
||||
|
||||
// JSON 输入的 computed
|
||||
const jsonInput = ref('{}')
|
||||
|
||||
watch([() => localKv.value.value, () => mode.value], () => {
|
||||
if (mode.value === 'json') {
|
||||
try {
|
||||
const parsed = JSON.parse(localKv.value.value)
|
||||
jsonInput.value = JSON.stringify(parsed, null, 2)
|
||||
} catch (e) {
|
||||
console.warn('Invalid JSON, resetting jsonInput to empty object', localKv.value.value)
|
||||
jsonInput.value = '{}'
|
||||
}
|
||||
}
|
||||
},{ immediate: true })
|
||||
|
||||
|
||||
|
||||
const onJsonUpdate = () => {
|
||||
const newValue = jsonInput.value
|
||||
if (checkIsValidJson(newValue)) {
|
||||
localKv.value.value = newValue
|
||||
isValidJson.value = true
|
||||
} else {
|
||||
isValidJson.value = false
|
||||
}
|
||||
}
|
||||
|
||||
// 字符串输入的 computed
|
||||
const stringInput = computed({
|
||||
get () {
|
||||
return localKv.value.value
|
||||
},
|
||||
set (newValue: string) {
|
||||
|
||||
localKv.value.value = newValue
|
||||
}
|
||||
})
|
||||
|
||||
const checkIsValidJson = (str: string) => {
|
||||
try {
|
||||
JSON.parse(str)
|
||||
return true
|
||||
} catch {
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
// 处理模式切换
|
||||
const handleModeChange = (newMode: 'str' | 'json') => {
|
||||
const currentValue = localKv.value.value
|
||||
|
||||
// 如果当前值为空,允许切换
|
||||
if (!currentValue) {
|
||||
mode.value = newMode
|
||||
return
|
||||
}
|
||||
|
||||
// 检查是否允许切换
|
||||
if (newMode === 'json' && currentValue) {
|
||||
message.warning(t('clearBeforeSwitchToJson'))
|
||||
return
|
||||
}
|
||||
|
||||
if (newMode === 'str' && jsonInput.value.trim()) {
|
||||
message.warning(t('clearBeforeSwitchToString'))
|
||||
console.warn('Switching to string mode requires empty value', {
|
||||
val: jsonInput.value
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
localKv.value.value = '' // 切换模式前清空当前值
|
||||
|
||||
|
||||
mode.value = newMode
|
||||
}
|
||||
|
||||
// 处理删除
|
||||
const handleRemove = () => {
|
||||
emit('remove')
|
||||
}
|
||||
|
||||
// 暴露校验方法给父组件
|
||||
const validate = (): boolean => {
|
||||
const keyValid = validateKey(localKv.value.key)
|
||||
const jsonValid = mode.value === 'json' ? isValidJson.value : true
|
||||
return keyValid && jsonValid
|
||||
}
|
||||
|
||||
// 暴露方法和状态给父组件
|
||||
defineExpose({
|
||||
validate,
|
||||
keyError,
|
||||
isValidJson
|
||||
})
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="kv-pair-editor">
|
||||
<div class="kv-key-wrapper">
|
||||
<a-input v-model:value="localKv.key" :placeholder="t('keyPlaceholder')" class="kv-input kv-key" />
|
||||
<div v-if="keyError" class="key-error-hint">{{ keyError }}</div>
|
||||
</div>
|
||||
|
||||
<div v-if="mode === 'json'" class="kv-value-wrapper">
|
||||
<ATextarea v-model:value="jsonInput" @blur="onJsonUpdate" :placeholder="t('jsonValuePlaceholder')"
|
||||
:auto-size="{ maxRows: 8 }" class="kv-input kv-value" />
|
||||
<div v-if="!isValidJson" class="json-error-hint">{{ t('jsonFormatError') }}</div>
|
||||
</div>
|
||||
|
||||
<ATextarea v-else :auto-size="{ maxRows: 8 }" v-model:value="stringInput" :placeholder="t('stringValuePlaceholder')" class="kv-input kv-value" />
|
||||
|
||||
<a-select :value="mode" size="small" class="mode-selector"
|
||||
:getPopupContainer="(trigger: any) => trigger.parentNode as HTMLDivElement" @update:value="handleModeChange"
|
||||
style="width: 80px">
|
||||
<a-select-option value="str">{{ t('stringMode') }}</a-select-option>
|
||||
<a-select-option value="json">{{ t('jsonMode') }}</a-select-option>
|
||||
</a-select>
|
||||
|
||||
<a-button size="small" danger @click="handleRemove" class="delete-btn">
|
||||
{{ t('delete') }}
|
||||
</a-button>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style scoped lang="scss">
|
||||
.kv-pair-editor {
|
||||
display: flex;
|
||||
gap: 8px;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.kv-input {
|
||||
font-size: 12px;
|
||||
}
|
||||
|
||||
.kv-key-wrapper {
|
||||
min-width: 50px;
|
||||
max-width: 120px;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 4px;
|
||||
}
|
||||
|
||||
.kv-key {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.kv-value {
|
||||
min-width: 200px;
|
||||
}
|
||||
|
||||
.kv-value-wrapper {
|
||||
min-width: 200px;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 4px;
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
.key-error-hint {
|
||||
font-size: 11px;
|
||||
color: #ff4d4f;
|
||||
line-height: 1.2;
|
||||
}
|
||||
|
||||
.json-error-hint {
|
||||
font-size: 11px;
|
||||
color: #ff4d4f;
|
||||
line-height: 1.2;
|
||||
}
|
||||
|
||||
.mode-selector {
|
||||
font-size: 12px;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.delete-btn {
|
||||
font-size: 12px;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
</style>
|
||||
|
|
@ -0,0 +1,309 @@
|
|||
<script setup lang="ts">
|
||||
import { ref } from 'vue'
|
||||
import { message } from 'ant-design-vue'
|
||||
import { t } from '@/i18n'
|
||||
import { updateExif, getImageGenerationInfo } from '@/api'
|
||||
import { parse } from '@/util/stable-diffusion-image-metadata'
|
||||
import { globalEvents, useGlobalEventListen } from '@/util'
|
||||
import type { FileNodeInfo } from '@/api/files'
|
||||
import KvPairEditor from './KvPairEditor.vue'
|
||||
|
||||
interface KVPair {
|
||||
key: string
|
||||
value: any
|
||||
}
|
||||
|
||||
// 组件内部状态
|
||||
const show = ref(false)
|
||||
const file = ref<FileNodeInfo | null>(null)
|
||||
const currentPrompt = ref<string>('')
|
||||
|
||||
// 监听全局事件
|
||||
useGlobalEventListen('openPromptEditor', async (data: { file: FileNodeInfo }) => {
|
||||
file.value = data.file
|
||||
console.log('Received openPromptEditor event for file:', data.file)
|
||||
// 每次打开时都获取最新的提示词数据
|
||||
try {
|
||||
const latestPrompt = await getImageGenerationInfo(data.file.fullpath)
|
||||
currentPrompt.value = latestPrompt
|
||||
} catch (error) {
|
||||
console.error('Failed to fetch latest prompt:', error)
|
||||
currentPrompt.value = ''
|
||||
}
|
||||
|
||||
initializeData()
|
||||
show.value = true
|
||||
})
|
||||
|
||||
// 基础元数据字段(当为空时使用)
|
||||
const defaultMetadata = 'Steps: 20'
|
||||
|
||||
// 计算字段(从其他字段派生,不可编辑)
|
||||
const computedFields = ['hashes', 'resources']
|
||||
|
||||
// 解析当前提示词
|
||||
const parsePrompt = (promptStr: string) => {
|
||||
const parsed = parse(promptStr)
|
||||
// 解析普通参数(排除计算字段和 extraJsonMetaInfo)
|
||||
const otherInfo = Object.entries(parsed)
|
||||
.filter(([key]) =>
|
||||
key !== 'prompt' &&
|
||||
key !== 'negativePrompt' &&
|
||||
!computedFields.includes(key) &&
|
||||
key !== 'extraJsonMetaInfo'
|
||||
)
|
||||
.map(([key, value]) => `${key.charAt(0).toUpperCase() + key.slice(1)}: ${value}`)
|
||||
.join('\n')
|
||||
|
||||
// 解析 extraJsonMetaInfo 为 KV 对
|
||||
const extraJsonMetaInfo = parsed.extraJsonMetaInfo as Record<string, any> | undefined
|
||||
const kvPairs: KVPair[] = []
|
||||
if (extraJsonMetaInfo) {
|
||||
Object.entries(extraJsonMetaInfo).forEach(([key, value]) => {
|
||||
// 根据值的类型决定模式:字符串用字符串模式,其他用JSON模式
|
||||
kvPairs.push({
|
||||
key,
|
||||
value
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
return {
|
||||
prompt: parsed.prompt || '',
|
||||
negativePrompt: parsed.negativePrompt || '',
|
||||
otherInfo: otherInfo || defaultMetadata,
|
||||
kvPairs
|
||||
}
|
||||
}
|
||||
|
||||
// 构建完整提示词
|
||||
const buildPrompt = (positive: string, negative: string, other: string, kvPairs: KVPair[]) => {
|
||||
let result = ''
|
||||
if (positive) result += positive
|
||||
result += `\nNegative prompt: ${negative || ''}`
|
||||
|
||||
// 添加普通参数
|
||||
if (other) {
|
||||
result += `\n${other.split('\n').filter(line => line.trim()).join(', ')}`
|
||||
} else {
|
||||
result += `\n${defaultMetadata}`
|
||||
}
|
||||
|
||||
// 添加 extraJsonMetaInfo
|
||||
if (kvPairs.length > 0) {
|
||||
const extraMeta: Record<string, any> = Object.fromEntries(kvPairs.map(kv => [kv.key.trim(), kv.value]))
|
||||
result += `\nextraJsonMetaInfo: ${JSON.stringify(extraMeta)}`
|
||||
}
|
||||
|
||||
return result.trim()
|
||||
}
|
||||
|
||||
// 初始化数据
|
||||
const positivePrompt = ref('')
|
||||
const negativePrompt = ref('')
|
||||
const otherInfo = ref(defaultMetadata)
|
||||
const kvPairs = ref<KVPair[]>([])
|
||||
const saving = ref(false)
|
||||
const kvEditorRefs = ref<any[]>([])
|
||||
|
||||
// 当文件或提示词变化时重新初始化
|
||||
const initializeData = () => {
|
||||
const data = currentPrompt.value ? parsePrompt(currentPrompt.value) : {
|
||||
prompt: '',
|
||||
negativePrompt: '',
|
||||
otherInfo: defaultMetadata,
|
||||
kvPairs: []
|
||||
}
|
||||
positivePrompt.value = data.prompt
|
||||
negativePrompt.value = data.negativePrompt
|
||||
otherInfo.value = data.otherInfo
|
||||
kvPairs.value = data.kvPairs
|
||||
}
|
||||
|
||||
// 添加新的 KV 对
|
||||
const addKvPair = () => {
|
||||
kvPairs.value.push({ key: '', value: '' })
|
||||
}
|
||||
|
||||
// 删除 KV 对
|
||||
const removeKvPair = (index: number) => {
|
||||
kvPairs.value.splice(index, 1)
|
||||
kvEditorRefs.value.splice(index, 1)
|
||||
}
|
||||
|
||||
// 保存
|
||||
const handleSave = async () => {
|
||||
if (!file.value) return
|
||||
|
||||
// 校验正向提示词不可为空
|
||||
if (!positivePrompt.value.trim()) {
|
||||
message.error(t('positivePromptRequired'))
|
||||
return
|
||||
}
|
||||
|
||||
// 校验所有 KV 编辑器
|
||||
const validators = kvEditorRefs.value
|
||||
.filter(ref => ref && ref.validate)
|
||||
.map(ref => ref.validate())
|
||||
|
||||
if (validators.some(valid => !valid)) {
|
||||
message.error(t('fixErrorsBeforeSave'))
|
||||
return
|
||||
}
|
||||
|
||||
saving.value = true
|
||||
try {
|
||||
|
||||
const fullPrompt = buildPrompt(positivePrompt.value, negativePrompt.value, otherInfo.value, kvPairs.value)
|
||||
await updateExif(file.value.fullpath, fullPrompt)
|
||||
message.success(t('savePromptSuccess'))
|
||||
|
||||
// 关闭模态框并触发全局事件
|
||||
show.value = false
|
||||
globalEvents.emit('promptEditorUpdated')
|
||||
} catch (error: any) {
|
||||
console.error('Save prompt error:', error)
|
||||
if (error.message && !error.message.includes('Invalid JSON')) {
|
||||
message.error(t('savePromptFailed'))
|
||||
}
|
||||
throw error
|
||||
} finally {
|
||||
saving.value = false
|
||||
}
|
||||
}
|
||||
|
||||
// 取消
|
||||
const handleCancel = () => {
|
||||
show.value = false
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<a-modal v-model:visible="show" :title="file ? t('editPromptTitle', { name: file.name }) : ''" :width="'70vw'"
|
||||
:footer="null" :maskClosable="true" destroyOnClose >
|
||||
<div class="prompt-editor-modal" @wheel.stop @keydown.stop @keyup.stop @keypress.stop>
|
||||
<div class="editor-section">
|
||||
<div class="section-label">{{ t('positivePrompt') }}</div>
|
||||
<a-textarea v-model:value="positivePrompt" :placeholder="t('positivePrompt')"
|
||||
:autoSize="{ minRows: 3, maxRows: 8 }" class="prompt-input" />
|
||||
</div>
|
||||
|
||||
<div class="editor-section">
|
||||
<div class="section-label">{{ t('negativePrompt') }}</div>
|
||||
<a-textarea v-model:value="negativePrompt" :placeholder="t('negativePrompt')"
|
||||
:autoSize="{ minRows: 2, maxRows: 6 }" class="prompt-input" />
|
||||
</div>
|
||||
|
||||
<div class="editor-section">
|
||||
<div class="section-label">
|
||||
{{ t('otherInfo') }}
|
||||
<span class="section-hint">({{ t('otherInfoHint') }})</span>
|
||||
</div>
|
||||
<a-textarea v-model:value="otherInfo" :placeholder="t('otherInfo')" :autoSize="{ minRows: 2, maxRows: 6 }"
|
||||
class="prompt-input" />
|
||||
</div>
|
||||
|
||||
<div class="editor-section kv-editor-section">
|
||||
<div class="kv-header">
|
||||
<div class="section-label">{{ t('extraMetaInfoTitle') }}</div>
|
||||
<a-button size="small" @click="addKvPair">{{ t('addKvButton') }}</a-button>
|
||||
</div>
|
||||
<div class="section-hint">
|
||||
{{ t('extraMetaInfoHint') }}
|
||||
</div>
|
||||
|
||||
<div v-if="kvPairs.length === 0" class="empty-state">
|
||||
{{ t('noExtraMetaInfo') }}
|
||||
</div>
|
||||
|
||||
<div v-else class="kv-list">
|
||||
<KvPairEditor
|
||||
v-for="(_, index) in kvPairs"
|
||||
:key="index"
|
||||
:ref="(el: any) => { if (el) kvEditorRefs[index] = el }"
|
||||
v-model="kvPairs[index]"
|
||||
:allKeys="kvPairs.filter((_, i) => i !== index).map(kv => kv.key)"
|
||||
@remove="removeKvPair(index)"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="modal-footer">
|
||||
<a-button @click="handleCancel">{{ t('cancel') }}</a-button>
|
||||
<a-button type="primary" @click="handleSave" :loading="saving">
|
||||
{{ t('savePrompt') }}
|
||||
</a-button>
|
||||
</div>
|
||||
</div>
|
||||
</a-modal>
|
||||
</template>
|
||||
|
||||
<style scoped lang="scss">
|
||||
.prompt-editor-modal {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 16px;
|
||||
max-height: 60vh;
|
||||
overflow-y: auto;
|
||||
}
|
||||
|
||||
.editor-section {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 6px;
|
||||
}
|
||||
|
||||
.section-label {
|
||||
font-size: 12px;
|
||||
color: var(--zp-primary);
|
||||
font-weight: 500;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.section-hint {
|
||||
font-size: 11px;
|
||||
color: var(--zp-secondary);
|
||||
font-weight: 400;
|
||||
}
|
||||
|
||||
.prompt-input {
|
||||
font-size: 13px;
|
||||
}
|
||||
|
||||
.kv-editor-section {
|
||||
border: 1px solid var(--zp-secondary-variant-background);
|
||||
border-radius: 4px;
|
||||
padding: 12px;
|
||||
background: var(--zp-secondary-background);
|
||||
}
|
||||
|
||||
.kv-header {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
margin-bottom: 8px;
|
||||
}
|
||||
|
||||
.kv-list {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.empty-state {
|
||||
text-align: center;
|
||||
padding: 20px;
|
||||
color: var(--zp-secondary);
|
||||
font-size: 12px;
|
||||
}
|
||||
|
||||
.modal-footer {
|
||||
display: flex;
|
||||
justify-content: flex-end;
|
||||
gap: 8px;
|
||||
padding-top: 8px;
|
||||
border-top: 1px solid var(--zp-secondary-variant-background);
|
||||
}
|
||||
</style>
|
||||
|
|
@ -1,11 +1,12 @@
|
|||
import { Button, Input, Modal, message } from 'ant-design-vue'
|
||||
import { Button, Input, Modal, message, Spin } from 'ant-design-vue'
|
||||
import { StyleValue, ref } from 'vue'
|
||||
import * as Path from '@/util/path'
|
||||
import { FileNodeInfo, mkdirs } from '@/api/files'
|
||||
import { setTargetFrameAsCover } from '@/api'
|
||||
import { setTargetFrameAsCover, getImageGenerationInfo } from '@/api'
|
||||
import { parse } from '@/util/stable-diffusion-image-metadata'
|
||||
import { t } from '@/i18n'
|
||||
import { downloadFiles, globalEvents, toRawFileUrl, toStreamVideoUrl, toStreamAudioUrl } from '@/util'
|
||||
import { DownloadOutlined } from '@/icon'
|
||||
import { DownloadOutlined, FileTextOutlined, EditOutlined } from '@/icon'
|
||||
import { isStandalone } from '@/util/env'
|
||||
import { addCustomTag, getDbBasicInfo, rebuildImageIndex, renameFile } from '@/api/db'
|
||||
import { useTagStore } from '@/store/useTagStore'
|
||||
|
|
@ -43,10 +44,12 @@ export const MultiSelectTips = () => (
|
|||
</p>
|
||||
)
|
||||
|
||||
export const openVideoModal = (
|
||||
file: FileNodeInfo,
|
||||
// 合并的视频/音频 modal 实现
|
||||
const openMediaModalImpl = (
|
||||
file: FileNodeInfo,
|
||||
onTagClick?: (id: string| number) => void,
|
||||
onTiktokView?: () => void
|
||||
onTiktokView?: () => void,
|
||||
mediaType: 'video' | 'audio' = 'video'
|
||||
) => {
|
||||
const tagStore = useTagStore()
|
||||
const global = useGlobalStore()
|
||||
|
|
@ -54,17 +57,23 @@ export const openVideoModal = (
|
|||
return !!tagStore.tagMap.get(file.fullpath)?.some(v => v.id === id)
|
||||
}
|
||||
const videoRef = ref<HTMLVideoElement | null>(null)
|
||||
const onSetCurrFrameAsVideoPoster = async () => {
|
||||
if (!videoRef.value) {
|
||||
return
|
||||
const imageGenInfo = ref('')
|
||||
const promptLoading = ref(false)
|
||||
|
||||
// 加载提示词
|
||||
const loadPrompt = async () => {
|
||||
promptLoading.value = true
|
||||
try {
|
||||
const info = await getImageGenerationInfo(file.fullpath)
|
||||
imageGenInfo.value = info
|
||||
} catch (error) {
|
||||
console.error('Load prompt error:', error)
|
||||
imageGenInfo.value = ''
|
||||
} finally {
|
||||
promptLoading.value = false
|
||||
}
|
||||
const video = videoRef.value
|
||||
video.pause()
|
||||
const base64 = video2base64(video)
|
||||
await setTargetFrameAsCover({ path: file.fullpath, base64_img: base64, updated_time: file.date } )
|
||||
file.cover_url = URL.createObjectURL(await base64ToFile(base64, 'cover'))
|
||||
message.success(t('success') + '! ' + t('clearCacheIfNotTakeEffect'))
|
||||
}
|
||||
|
||||
const tagBaseStyle: StyleValue = {
|
||||
margin: '2px',
|
||||
padding: '2px 16px',
|
||||
|
|
@ -75,95 +84,96 @@ export const openVideoModal = (
|
|||
transition: '.5s all ease',
|
||||
'user-select': 'none',
|
||||
}
|
||||
|
||||
const modal = Modal.confirm({
|
||||
width: '80vw',
|
||||
title: file.name,
|
||||
icon: null,
|
||||
content: () => (
|
||||
<div
|
||||
style={{
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center',
|
||||
flexDirection: 'column'
|
||||
}}
|
||||
>
|
||||
<video ref={videoRef} style={{ maxHeight: isStandalone ? '80vh' : '60vh', maxWidth: '100%', minWidth: '70%' }} src={toStreamVideoUrl(file)} controls autoplay></video>
|
||||
<div style={{ marginTop: '4px' }}>
|
||||
<div onClick={openAddNewTagModal} style={{
|
||||
background: 'var(--zp-primary-background)',
|
||||
color: 'var(--zp-luminous)',
|
||||
border: '2px solid var(--zp-luminous)',
|
||||
...tagBaseStyle
|
||||
}}>
|
||||
{ t('addNewCustomTag') }
|
||||
</div>
|
||||
{global.conf!.all_custom_tags.map((tag) =>
|
||||
<div key={tag.id} onClick={() => onTagClick?.(tag.id)} style={{
|
||||
background: isSelected(tag.id) ? tagStore.getColor(tag) : 'var(--zp-primary-background)',
|
||||
color: !isSelected(tag.id) ? tagStore.getColor(tag) : 'white',
|
||||
border: `2px solid ${tagStore.getColor(tag)}`,
|
||||
...tagBaseStyle
|
||||
}}>
|
||||
{ tag.name }
|
||||
</div>)}
|
||||
</div>
|
||||
<div class="actions" style={{ marginTop: '16px' }}>
|
||||
<Button onClick={() => downloadFiles([toRawFileUrl(file, true)])}>
|
||||
{{
|
||||
icon: <DownloadOutlined/>,
|
||||
default: t('download')
|
||||
}}
|
||||
</Button>
|
||||
{onTiktokView && (
|
||||
<Button onClick={onTiktokViewWrapper} type="primary">
|
||||
{{
|
||||
default: t('tiktokView')
|
||||
}}
|
||||
</Button>
|
||||
)}
|
||||
<Button onClick={onSetCurrFrameAsVideoPoster}>
|
||||
{{
|
||||
default: t('setCurrFrameAsVideoPoster')
|
||||
}}
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
),
|
||||
maskClosable: true,
|
||||
wrapClassName: 'hidden-antd-btns-modal'
|
||||
})
|
||||
function onTiktokViewWrapper() {
|
||||
|
||||
// 解析提示词结构
|
||||
const geninfoStruct = () => parse(imageGenInfo.value)
|
||||
|
||||
// 计算文本长度(中文算3个字符)
|
||||
const getTextLength = (text: string): number => {
|
||||
let length = 0
|
||||
for (const char of text) {
|
||||
if (/[\u4e00-\u9fa5]/.test(char)) {
|
||||
length += 3
|
||||
} else {
|
||||
length += 1
|
||||
}
|
||||
}
|
||||
return length
|
||||
}
|
||||
|
||||
// 判断是否为 tag 风格的提示词
|
||||
const isTagStylePrompt = (tags: string[]): boolean => {
|
||||
if (tags.length === 0) return false
|
||||
|
||||
let totalLength = 0
|
||||
for (const tag of tags) {
|
||||
const tagLength = getTextLength(tag)
|
||||
totalLength += tagLength
|
||||
|
||||
if (tagLength > 50) {
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
const avgLength = totalLength / tags.length
|
||||
if (avgLength > 30) {
|
||||
return false
|
||||
}
|
||||
|
||||
return true
|
||||
}
|
||||
|
||||
// 提示词包装函数(支持 tag 风格和自然语言风格)
|
||||
const spanWrap = (text: string) => {
|
||||
if (!text) return ''
|
||||
|
||||
const specBreakTag = 'BREAK'
|
||||
const values = text.replace(/>\s/g, '> ,').replace(/\sBREAK\s/g, ',' + specBreakTag + ',')
|
||||
.split(/[\n,]+/)
|
||||
.map(v => v.trim())
|
||||
.filter(v => v)
|
||||
|
||||
// 判断是否为 tag 风格
|
||||
if (!isTagStylePrompt(values)) {
|
||||
// 自然语言风格
|
||||
return text
|
||||
.split('\n')
|
||||
.map(line => line.trim())
|
||||
.filter(line => line)
|
||||
.map(line => `<p style="margin:0; padding:4px 0;">${line}</p>`)
|
||||
.join('')
|
||||
}
|
||||
|
||||
// Tag 风格
|
||||
const frags: string[] = []
|
||||
let parenthesisActive = false
|
||||
for (let i = 0; i < values.length; i++) {
|
||||
if (values[i] === specBreakTag) {
|
||||
frags.push('<br><span style="color:var(--zp-secondary); font-weight:bold;">BREAK</span><br>')
|
||||
continue
|
||||
}
|
||||
const trimmedValue = values[i]
|
||||
if (!parenthesisActive) parenthesisActive = trimmedValue.includes('(')
|
||||
const styles = ['background: var(--zp-secondary-variant-background)', 'color: var(--zp-primary)', 'padding: 2px 6px', 'border-radius: 4px', 'margin-right: 6px', 'margin-top: 4px', 'display: inline-block']
|
||||
if (parenthesisActive) styles.push('border: 1px solid var(--zp-secondary)')
|
||||
if (getTextLength(trimmedValue) < 32) styles.push('font-size: 0.9em')
|
||||
frags.push(`<span style="${styles.join('; ')}">${trimmedValue}</span>`)
|
||||
if (parenthesisActive) parenthesisActive = !trimmedValue.includes(')')
|
||||
}
|
||||
return frags.join(' ')
|
||||
}
|
||||
|
||||
// 加载提示词
|
||||
loadPrompt()
|
||||
|
||||
const onTiktokViewWrapper = () => {
|
||||
onTiktokView?.()
|
||||
closeImageFullscreenPreview()
|
||||
modal.destroy()
|
||||
}
|
||||
}
|
||||
|
||||
export const openAudioModal = (
|
||||
file: FileNodeInfo,
|
||||
onTagClick?: (id: string| number) => void,
|
||||
onTiktokView?: () => void
|
||||
) => {
|
||||
const tagStore = useTagStore()
|
||||
const global = useGlobalStore()
|
||||
const isSelected = (id: string | number) => {
|
||||
return !!tagStore.tagMap.get(file.fullpath)?.some(v => v.id === id)
|
||||
}
|
||||
const tagBaseStyle: StyleValue = {
|
||||
margin: '2px',
|
||||
padding: '2px 16px',
|
||||
'border-radius': '4px',
|
||||
display: 'inline-block',
|
||||
cursor: 'pointer',
|
||||
'font-weight': 'bold',
|
||||
transition: '.5s all ease',
|
||||
'user-select': 'none',
|
||||
}
|
||||
|
||||
const modal = Modal.confirm({
|
||||
width: '60vw',
|
||||
width: mediaType === 'video' ? '80vw' : '70vw',
|
||||
title: file.name,
|
||||
icon: null,
|
||||
content: () => (
|
||||
|
|
@ -175,30 +185,37 @@ export const openAudioModal = (
|
|||
flexDirection: 'column'
|
||||
}}
|
||||
>
|
||||
<div style={{
|
||||
fontSize: '80px',
|
||||
marginBottom: '16px'
|
||||
}}>🎵</div>
|
||||
<audio style={{ width: '100%', maxWidth: '500px' }} src={toStreamAudioUrl(file)} controls autoplay></audio>
|
||||
{mediaType === 'video' ? (
|
||||
<video ref={videoRef} style={{ maxHeight: isStandalone ? '80vh' : '60vh', maxWidth: '100%', minWidth: '70%' }} src={toStreamVideoUrl(file)} controls autoplay></video>
|
||||
) : (
|
||||
<>
|
||||
<div style={{ fontSize: '80px', marginBottom: '16px' }}>🎵</div>
|
||||
<audio style={{ width: '100%', maxWidth: '500px' }} src={toStreamAudioUrl(file)} controls autoplay></audio>
|
||||
</>
|
||||
)}
|
||||
|
||||
{/* 标签选择区域 */}
|
||||
<div style={{ marginTop: '16px' }}>
|
||||
<div onClick={openAddNewTagModal} style={{
|
||||
background: 'var(--zp-primary-background)',
|
||||
background: 'var(--zp-primary-background)',
|
||||
color: 'var(--zp-luminous)',
|
||||
border: '2px solid var(--zp-luminous)',
|
||||
...tagBaseStyle
|
||||
}}>
|
||||
{ t('addNewCustomTag') }
|
||||
</div>
|
||||
{global.conf!.all_custom_tags.map((tag) =>
|
||||
{global.conf!.all_custom_tags.map((tag) =>
|
||||
<div key={tag.id} onClick={() => onTagClick?.(tag.id)} style={{
|
||||
background: isSelected(tag.id) ? tagStore.getColor(tag) : 'var(--zp-primary-background)',
|
||||
color: !isSelected(tag.id) ? tagStore.getColor(tag) : 'white',
|
||||
background: isSelected(tag.id) ? tagStore.getColor(tag) : 'var(--zp-primary-background)',
|
||||
color: !isSelected(tag.id) ? tagStore.getColor(tag) : 'white',
|
||||
border: `2px solid ${tagStore.getColor(tag)}`,
|
||||
...tagBaseStyle
|
||||
}}>
|
||||
{ tag.name }
|
||||
</div>)}
|
||||
</div>
|
||||
|
||||
{/* 操作按钮 */}
|
||||
<div class="actions" style={{ marginTop: '16px' }}>
|
||||
<Button onClick={() => downloadFiles([toRawFileUrl(file, true)])}>
|
||||
{{
|
||||
|
|
@ -213,19 +230,83 @@ export const openAudioModal = (
|
|||
}}
|
||||
</Button>
|
||||
)}
|
||||
{mediaType === 'video' && (
|
||||
<Button onClick={async () => {
|
||||
if (!videoRef.value) return
|
||||
const video = videoRef.value
|
||||
video.pause()
|
||||
const base64 = video2base64(video)
|
||||
await setTargetFrameAsCover({ path: file.fullpath, base64_img: base64, updated_time: file.date })
|
||||
file.cover_url = URL.createObjectURL(await base64ToFile(base64, 'cover'))
|
||||
message.success(t('success') + '! ' + t('clearCacheIfNotTakeEffect'))
|
||||
}}>
|
||||
{{ default: t('setCurrFrameAsVideoPoster') }}
|
||||
</Button>
|
||||
)}
|
||||
<Button onClick={async () => {
|
||||
await openEditPromptModal(file)
|
||||
await loadPrompt()
|
||||
}} icon={<EditOutlined />}>
|
||||
{{ default: t('editPrompt') }}
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
{/* 提示词显示区域 */}
|
||||
{promptLoading.value ? (
|
||||
<div style={{ marginTop: '24px', width: '100%', textAlign: 'center' }}>
|
||||
<Spin />
|
||||
</div>
|
||||
) : imageGenInfo.value ? (
|
||||
<div style={{ marginTop: '24px', width: '100%', maxWidth: mediaType === 'video' ? '1000px' : '900px', alignSelf: 'flex-start' }}>
|
||||
<div style={{ display: 'flex', alignItems: 'center', gap: '8px', marginBottom: '12px', color: 'var(--zp-primary)', fontSize: '14px', fontWeight: 500 }}>
|
||||
<FileTextOutlined />
|
||||
<span>Prompt</span>
|
||||
</div>
|
||||
{geninfoStruct().prompt && (
|
||||
<div style={{ marginBottom: '12px' }}>
|
||||
<div style={{ fontSize: '12px', color: 'var(--zp-primary)', marginBottom: '6px' }}>Positive</div>
|
||||
<code style={{ fontSize: '13px', display: 'block', padding: '10px 12px', background: 'var(--zp-primary-background)', borderRadius: '8px', whiteSpace: 'pre-wrap', wordBreak: 'break-word', lineHeight: '1.6em' }} innerHTML={spanWrap(geninfoStruct().prompt ?? '')}></code>
|
||||
</div>
|
||||
)}
|
||||
{geninfoStruct().negativePrompt && (
|
||||
<div style={{ marginBottom: '12px' }}>
|
||||
<div style={{ fontSize: '12px', color: 'var(--zp-primary)', marginBottom: '6px' }}>Negative</div>
|
||||
<code style={{ fontSize: '13px', display: 'block', padding: '10px 12px', background: 'var(--zp-primary-background)', borderRadius: '8px', whiteSpace: 'pre-wrap', wordBreak: 'break-word', lineHeight: '1.6em' }} innerHTML={spanWrap(geninfoStruct().negativePrompt ?? '')}></code>
|
||||
</div>
|
||||
)}
|
||||
{/* Meta 信息 */}
|
||||
{Object.entries(geninfoStruct()).filter(([key]) => key !== 'prompt' && key !== 'negativePrompt').length > 0 && (
|
||||
<div>
|
||||
<div style={{ fontSize: '12px', color: 'var(--zp-primary)', marginBottom: '6px' }}>Meta</div>
|
||||
<code style={{ fontSize: '12px', display: 'block', padding: '8px 12px', background: 'var(--zp-secondary-background)', borderRadius: '6px', whiteSpace: 'pre-wrap', wordBreak: 'break-word', lineHeight: '1.5em', color: 'var(--zp-primary)', opacity: 0.7 }}>
|
||||
{Object.entries(geninfoStruct())
|
||||
.filter(([key]) => key !== 'prompt' && key !== 'negativePrompt')
|
||||
.map(([key, value]) => `${key.charAt(0).toUpperCase() + key.slice(1)}: ${value}`)
|
||||
.join('\n')}
|
||||
</code>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
) : null}
|
||||
</div>
|
||||
),
|
||||
maskClosable: true,
|
||||
wrapClassName: 'hidden-antd-btns-modal'
|
||||
})
|
||||
function onTiktokViewWrapper() {
|
||||
onTiktokView?.()
|
||||
closeImageFullscreenPreview()
|
||||
modal.destroy()
|
||||
}
|
||||
}
|
||||
|
||||
export const openVideoModal = (
|
||||
file: FileNodeInfo,
|
||||
onTagClick?: (id: string| number) => void,
|
||||
onTiktokView?: () => void
|
||||
) => openMediaModalImpl(file, onTagClick, onTiktokView, 'video')
|
||||
|
||||
export const openAudioModal = (
|
||||
file: FileNodeInfo,
|
||||
onTagClick?: (id: string| number) => void,
|
||||
onTiktokView?: () => void
|
||||
) => openMediaModalImpl(file, onTagClick, onTiktokView, 'audio')
|
||||
|
||||
export const openRebuildImageIndexModal = () => {
|
||||
Modal.confirm({
|
||||
title: t('confirmRebuildImageIndex'),
|
||||
|
|
@ -286,3 +367,16 @@ export const openAddNewTagModal = () => {
|
|||
})
|
||||
}
|
||||
|
||||
export const openEditPromptModal = async (file: FileNodeInfo) => {
|
||||
globalEvents.off('promptEditorUpdated') // 确保事件监听器不会重复绑定
|
||||
return new Promise<void>((resolve) => {
|
||||
const handler = () => {
|
||||
globalEvents.off('promptEditorUpdated', handler)
|
||||
resolve()
|
||||
}
|
||||
|
||||
globalEvents.on('promptEditorUpdated', handler)
|
||||
globalEvents.emit('openPromptEditor', { file })
|
||||
})
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -1,6 +1,13 @@
|
|||
import type { IIBI18nMap } from '.'
|
||||
|
||||
export const de: Partial<IIBI18nMap> = {
|
||||
blockVisibilitySettings: 'Panel-Einstellungen',
|
||||
blockName_actionBar: 'Aktionsschaltflächen',
|
||||
blockName_infoTags: 'Datei-Info-Tags',
|
||||
blockName_tagsContainer: 'Benutzerdefinierte Tags',
|
||||
blockName_lrLayoutControl: 'Layout-Steuerung',
|
||||
blockName_draggableImage: 'Ziehbares Bild',
|
||||
blockName_tabs: 'Prompt-Registerkarten',
|
||||
serverKeyRequired:
|
||||
'Für die weitere Nutzung ist die Eingabe eines Schlüssels erforderlich, der vom Server konfiguriert wurde.',
|
||||
removeFromSearchScanPathAndQuickMove: 'Schnellzugriff entfernen',
|
||||
|
|
@ -319,5 +326,42 @@ export const de: Partial<IIBI18nMap> = {
|
|||
loadingTip10: '🤖 KI-Agenten-Integration\n\nSie können jetzt KI-Agenten IIB nutzen lassen, um bei Bildverwaltung, Tag-Organisation und intelligenter Suche zu helfen. Über die API-Schnittstelle kann die KI auf alle IIB-Funktionen zugreifen und automatisierte Workflows erstellen.|info',
|
||||
|
||||
// ===== Video Inline Play =====
|
||||
playInline: 'Hier abspielen'
|
||||
playInline: 'Hier abspielen',
|
||||
|
||||
// ===== Prompt-Bearbeitung =====
|
||||
editPrompt: 'bearbeiten',
|
||||
editPromptTitle: 'Prompt bearbeiten - {name}',
|
||||
positivePrompt: 'Positiver Prompt',
|
||||
negativePrompt: 'Negativer Prompt',
|
||||
otherInfo: 'Weitere Informationen',
|
||||
savePrompt: 'Prompt speichern',
|
||||
savePromptSuccess: 'Prompt erfolgreich gespeichert',
|
||||
savePromptFailed: 'Prompt-Speicherung fehlgeschlagen',
|
||||
promptEditedMark: 'Manuell bearbeitet',
|
||||
promptModifiedTip: 'Dieser Prompt wurde manuell bearbeitet und überschreibt den ursprünglichen Prompt aus der Datei',
|
||||
|
||||
// PromptEditorModal bezogen
|
||||
positivePromptRequired: 'Positiver Prompt darf nicht leer sein',
|
||||
fixErrorsBeforeSave: 'Bitte beheben Sie alle Fehler vor dem Speichern',
|
||||
extraMetaInfoTitle: 'Extra Meta Info (KV-Editor)',
|
||||
addKvButton: '+ Hinzufügen',
|
||||
extraMetaInfoHint: 'Kann beliebige komplexe Informationen speichern, eine perfekte Ergänzung zu den Einschränkungen des Params-Formats. Im JSON-Modus werden auch alle gültigen JSON-Werte unterstützt (Objekte, Arrays, Zahlen, Boolesche Werte, etc.)',
|
||||
noExtraMetaInfo: 'Keine Extra Meta Info, klicken Sie auf "Hinzufügen" um Schlüssel-Wert-Paare hinzuzufügen',
|
||||
otherInfoHint: 'Unterstützt nur einfache Zeichenfolgen oder Zahlen, keine speziellen Symbole oder Zeilenumbrüche',
|
||||
|
||||
// KvPairEditor bezogen
|
||||
keyRequired: 'Key darf nicht leer sein',
|
||||
keyMustBeUnique: 'Key existiert bereits, bitte verwenden Sie einen eindeutigen Key',
|
||||
jsonFormatError: 'JSON-Formatfehler, bitte Syntax überprüfen',
|
||||
stringMode: 'Zeichenfolge',
|
||||
jsonMode: 'JSON',
|
||||
delete: 'Löschen',
|
||||
keyPlaceholder: 'Key',
|
||||
jsonValuePlaceholder: 'JSON-Wert',
|
||||
stringValuePlaceholder: 'Zeichenfolgenwert',
|
||||
clearBeforeSwitchToJson: 'Bitte aktuellen Wert leeren vor dem Wechsel zu JSON-Modus',
|
||||
clearBeforeSwitchToString: 'Bitte aktuellen Wert leeren vor dem Wechsel zu Zeichenfolgen-Modus',
|
||||
dragImageToTransfer: 'Klicken Sie, um das Originalbild zu erhalten, greifen und ziehen Sie es zu anderen Apps',
|
||||
dragImage: 'Klicken Sie, um das Originalbild zu erhalten, greifen und ziehen Sie es zu anderen Apps',
|
||||
close: 'Schließen'
|
||||
}
|
||||
|
|
|
|||
|
|
@ -223,6 +223,13 @@ You can specify which snapshot to restore to when starting IIB in the global set
|
|||
resolution: 'Resolution',
|
||||
fileSize: 'File Size',
|
||||
fullscreenview: 'Fullscreen View',
|
||||
blockVisibilitySettings: 'Panel Settings',
|
||||
blockName_actionBar: 'Action Buttons',
|
||||
blockName_infoTags: 'File Info Tags',
|
||||
blockName_tagsContainer: 'Custom Tags',
|
||||
blockName_lrLayoutControl: 'Layout Control',
|
||||
blockName_draggableImage: 'Draggable Image',
|
||||
blockName_tabs: 'Prompt Tabs',
|
||||
promptcompare: 'Compare Prompts',
|
||||
imgCompare: 'Image Comparison',
|
||||
share: 'Share',
|
||||
|
|
@ -579,5 +586,41 @@ You can specify which snapshot to restore to when starting IIB in the global set
|
|||
loadingTip10: '🤖 AI Agent Integration\n\nYou can now let AI agents use IIB to help with image management, tag organization, and smart search. Through the API interface, AI can access all IIB features for automated workflows.|info',
|
||||
|
||||
// ===== Video Inline Play =====
|
||||
playInline: 'Play Here'
|
||||
playInline: 'Play Here',
|
||||
|
||||
// ===== Prompt Editing =====
|
||||
editPrompt: 'Edit',
|
||||
editPromptTitle: 'Edit Prompt - {name}',
|
||||
positivePrompt: 'Positive Prompt',
|
||||
negativePrompt: 'Negative Prompt',
|
||||
otherInfo: 'Other Info',
|
||||
savePrompt: 'Save Prompt',
|
||||
savePromptSuccess: 'Prompt saved successfully',
|
||||
savePromptFailed: 'Failed to save prompt',
|
||||
promptEditedMark: 'Manually edited',
|
||||
promptModifiedTip: 'This prompt has been manually edited and will override the original prompt from the file',
|
||||
|
||||
// PromptEditorModal related
|
||||
positivePromptRequired: 'Positive prompt cannot be empty',
|
||||
fixErrorsBeforeSave: 'Please fix all errors before saving',
|
||||
extraMetaInfoTitle: 'Extra Meta Info (KV Editor)',
|
||||
addKvButton: '+ Add',
|
||||
extraMetaInfoHint: 'Can save any complex information, a perfect supplement to the limitations of params format. In JSON mode, it also supports any valid JSON values (objects, arrays, numbers, booleans, etc.)',
|
||||
noExtraMetaInfo: 'No Extra Meta Info, click "Add" button to add key-value pairs',
|
||||
otherInfoHint: 'Only supports simple strings or numbers, cannot contain special symbols or line breaks',
|
||||
|
||||
// KvPairEditor related
|
||||
keyRequired: 'Key cannot be empty',
|
||||
keyMustBeUnique: 'Key already exists, please use a unique key',
|
||||
jsonFormatError: 'JSON format error, please check syntax',
|
||||
stringMode: 'String',
|
||||
jsonMode: 'JSON',
|
||||
delete: 'Delete',
|
||||
keyPlaceholder: 'Key',
|
||||
jsonValuePlaceholder: 'JSON Value',
|
||||
stringValuePlaceholder: 'String Value',
|
||||
clearBeforeSwitchToJson: 'Please clear current value before switching to JSON mode',
|
||||
clearBeforeSwitchToString: 'Please clear current value before switching to string mode',
|
||||
dragImageToTransfer: 'Click to get original image, grab and drag to other apps',
|
||||
dragImage: 'Click to get original image, grab and drag to other apps'
|
||||
}
|
||||
|
|
|
|||
|
|
@ -350,6 +350,13 @@ export const zhHans = {
|
|||
imgCompare: '图像对比',
|
||||
close: '关闭',
|
||||
fullscreenview: '全屏查看',
|
||||
blockVisibilitySettings: '面板设置',
|
||||
blockName_actionBar: '操作按钮',
|
||||
blockName_infoTags: '文件信息标签',
|
||||
blockName_tagsContainer: '自定义标签',
|
||||
blockName_lrLayoutControl: '布局控制',
|
||||
blockName_draggableImage: '可拖拽图片',
|
||||
blockName_tabs: '提示词标签页',
|
||||
fileName: '文件名',
|
||||
resolution: '分辨率',
|
||||
fileSize: '文件大小',
|
||||
|
|
@ -557,5 +564,41 @@ export const zhHans = {
|
|||
loadingTip10: '🤖 AI Agent 集成\n\n现在你可以让 AI agent 来使用 IIB 帮助进行图像管理、标签整理和智能搜索。通过 API 接口,AI 可以访问所有 IIB 功能,实现自动化工作流程。|info',
|
||||
|
||||
// ===== 视频原地播放 =====
|
||||
playInline: '在此播放'
|
||||
playInline: '在此播放',
|
||||
|
||||
// ===== 提示词编辑 =====
|
||||
editPrompt: '编辑',
|
||||
editPromptTitle: '编辑提示词 - {name}',
|
||||
positivePrompt: '正向提示词',
|
||||
negativePrompt: '负向提示词',
|
||||
otherInfo: '其他信息',
|
||||
savePrompt: '保存提示词',
|
||||
savePromptSuccess: '提示词保存成功',
|
||||
savePromptFailed: '提示词保存失败',
|
||||
promptEditedMark: '已手动编辑',
|
||||
promptModifiedTip: '此提示词已被手动编辑,将覆盖原始文件中的提示词',
|
||||
|
||||
// 新增:PromptEditorModal 相关
|
||||
positivePromptRequired: '正向提示词不能为空',
|
||||
fixErrorsBeforeSave: '请修正所有错误后再保存',
|
||||
extraMetaInfoTitle: 'Extra Meta Info (KV 编辑器)',
|
||||
addKvButton: '+ 添加',
|
||||
extraMetaInfoHint: '可以保存任何的复杂信息,是对params格式缺陷的完美补充。json模式下还支持任何合法 JSON 值(对象、数组、数字、布尔等)',
|
||||
noExtraMetaInfo: '暂无 Extra Meta Info,点击"添加"按钮添加键值对',
|
||||
otherInfoHint: '仅支持简单字符串或数值,不能包含特殊符号或换行',
|
||||
|
||||
// 新增:KvPairEditor 相关
|
||||
keyRequired: 'Key 不能为空',
|
||||
keyMustBeUnique: 'Key 已存在,请使用唯一的 key',
|
||||
jsonFormatError: 'JSON 格式错误,请检查语法',
|
||||
stringMode: '字符串',
|
||||
jsonMode: 'JSON',
|
||||
delete: '删除',
|
||||
keyPlaceholder: 'Key',
|
||||
jsonValuePlaceholder: 'JSON Value',
|
||||
stringValuePlaceholder: '字符串值',
|
||||
clearBeforeSwitchToJson: '切换到 JSON 模式前请先清空当前值',
|
||||
clearBeforeSwitchToString: '切换到字符串模式前请先清空当前值',
|
||||
dragImageToTransfer: '点击获取原图,抓取拖拽至其他应用',
|
||||
dragImage: '点击获取原图,抓取拖拽至其他应用'
|
||||
}
|
||||
|
|
|
|||
|
|
@ -357,6 +357,13 @@ export const zhHant: Partial<IIBI18nMap> = {
|
|||
imgCompare: '圖片對比',
|
||||
close: '關閉',
|
||||
fullscreenview: '全屏查看',
|
||||
blockVisibilitySettings: '面板設置',
|
||||
blockName_actionBar: '操作按鈕',
|
||||
blockName_infoTags: '文件信息標籤',
|
||||
blockName_tagsContainer: '自定義標籤',
|
||||
blockName_lrLayoutControl: '佈局控制',
|
||||
blockName_draggableImage: '可拖拽圖片',
|
||||
blockName_tabs: '提示詞標籤頁',
|
||||
fileName: '文件名稱',
|
||||
resolution: '解析度',
|
||||
fileSize: '文件大小',
|
||||
|
|
@ -559,5 +566,41 @@ export const zhHant: Partial<IIBI18nMap> = {
|
|||
loadingTip10: '🤖 AI Agent 整合\n\n現在您可以讓 AI agent 使用 IIB 來協助進行圖片管理、標籤整理和智慧搜尋。透過 API 介面,AI 可以存取所有 IIB 功能,實現自動化工作流程。|info',
|
||||
|
||||
// ===== 視頻原地播放 =====
|
||||
playInline: '在此播放'
|
||||
playInline: '在此播放',
|
||||
|
||||
// ===== 提示詞編輯 =====
|
||||
editPrompt: '編輯',
|
||||
editPromptTitle: '編輯提示詞 - {name}',
|
||||
positivePrompt: '正向提示詞',
|
||||
negativePrompt: '負向提示詞',
|
||||
otherInfo: '其他信息',
|
||||
savePrompt: '保存提示詞',
|
||||
savePromptSuccess: '提示詞保存成功',
|
||||
savePromptFailed: '提示詞保存失敗',
|
||||
promptEditedMark: '已手動編輯',
|
||||
promptModifiedTip: '此提示詞已被手動編輯,將覆蓋原始文件中的提示詞',
|
||||
|
||||
// 新增:PromptEditorModal 相關
|
||||
positivePromptRequired: '正向提示詞不能為空',
|
||||
fixErrorsBeforeSave: '請修正所有錯誤後再保存',
|
||||
extraMetaInfoTitle: 'Extra Meta Info (KV 編輯器)',
|
||||
addKvButton: '+ 添加',
|
||||
extraMetaInfoHint: '可以保存任何的複雜信息,是對params格式缺陷的完美補充。json模式下還支持任何合法 JSON 值(對象、數組、數字、布爾等)',
|
||||
noExtraMetaInfo: '暫無 Extra Meta Info,點擊"添加"按鈕添加鍵值對',
|
||||
otherInfoHint: '僅支持簡單字符串或數值,不能包含特殊符號或換行',
|
||||
|
||||
// 新增:KvPairEditor 相關
|
||||
keyRequired: 'Key 不能為空',
|
||||
keyMustBeUnique: 'Key 已存在,請使用唯一的 key',
|
||||
jsonFormatError: 'JSON 格式錯誤,請檢查語法',
|
||||
stringMode: '字符串',
|
||||
jsonMode: 'JSON',
|
||||
delete: '刪除',
|
||||
keyPlaceholder: 'Key',
|
||||
jsonValuePlaceholder: 'JSON Value',
|
||||
stringValuePlaceholder: '字符串值',
|
||||
clearBeforeSwitchToJson: '切換到 JSON 模式前請先清空當前值',
|
||||
clearBeforeSwitchToString: '切換到字符串模式前請先清空當前值',
|
||||
dragImageToTransfer: '點擊獲取原圖,抓取拖拽至其他應用',
|
||||
dragImage: '點擊獲取原圖,抓取拖拽至其他應用'
|
||||
}
|
||||
|
|
|
|||
|
|
@ -147,7 +147,7 @@ body {
|
|||
margin: 0;
|
||||
}
|
||||
.ant-modal-wrap,.ant-message, .ant-tooltip {
|
||||
z-index: 10000;
|
||||
z-index: 100000;
|
||||
}
|
||||
|
||||
.hidden-antd-btns-modal .ant-modal-confirm-btns {
|
||||
|
|
|
|||
|
|
@ -5,7 +5,7 @@ import { useTagStore } from '@/store/useTagStore'
|
|||
import { useGlobalStore } from '@/store/useGlobalStore'
|
||||
import { useLocalStorage, onLongPress } from '@vueuse/core'
|
||||
import { copy2clipboardI18n, isVideoFile, isAudioFile } from '@/util'
|
||||
import { openAddNewTagModal } from '@/components/functionalCallableComp'
|
||||
import { openAddNewTagModal, openEditPromptModal } from '@/components/functionalCallableComp'
|
||||
import { toggleCustomTagToImg } from '@/api/db'
|
||||
import { deleteFiles } from '@/api/files'
|
||||
import { getImageGenerationInfo, openFolder, openWithDefaultApp } from '@/api'
|
||||
|
|
@ -31,7 +31,8 @@ import {
|
|||
CopyOutlined,
|
||||
LinkOutlined,
|
||||
FileTextOutlined,
|
||||
InfoCircleOutlined
|
||||
InfoCircleOutlined,
|
||||
EditOutlined
|
||||
} from '@/icon'
|
||||
import { t } from '@/i18n'
|
||||
import type { StyleValue } from 'vue'
|
||||
|
|
@ -837,11 +838,6 @@ const loadCurrentItemPrompt = async () => {
|
|||
imageGenInfo.value = ''
|
||||
return
|
||||
}
|
||||
const nameOrUrl = currentItem.name || currentItem.url
|
||||
if (isVideoFile(nameOrUrl) || isAudioFile(nameOrUrl)) {
|
||||
imageGenInfo.value = ''
|
||||
return
|
||||
}
|
||||
const fullpath = (currentItem as any)?.fullpath || currentItem.id
|
||||
if (!fullpath) {
|
||||
imageGenInfo.value = ''
|
||||
|
|
@ -1251,6 +1247,18 @@ watch(() => autoPlayMode.value, () => {
|
|||
<div class="panel-section prompt-section">
|
||||
<div class="section-title">
|
||||
<FileTextOutlined /> <span>Prompt</span>
|
||||
<button
|
||||
v-if="!promptLoading"
|
||||
@click="async () => {
|
||||
await openEditPromptModal(tiktokStore.currentItem as any)
|
||||
// 重新加载提示词
|
||||
await loadCurrentItemPrompt()
|
||||
}"
|
||||
class="edit-prompt-btn"
|
||||
:title="t('editPrompt')"
|
||||
>
|
||||
<EditOutlined />
|
||||
</button>
|
||||
</div>
|
||||
<div class="prompt-content">
|
||||
<div v-if="promptLoading" class="prompt-empty">...</div>
|
||||
|
|
@ -1831,6 +1839,26 @@ watch(() => autoPlayMode.value, () => {
|
|||
margin-bottom: 12px;
|
||||
text-transform: uppercase;
|
||||
letter-spacing: 0.5px;
|
||||
|
||||
.edit-prompt-btn {
|
||||
margin-left: auto;
|
||||
background: rgba(255, 255, 255, 0.1);
|
||||
border: 1px solid rgba(255, 255, 255, 0.2);
|
||||
border-radius: 6px;
|
||||
padding: 6px 10px;
|
||||
cursor: pointer;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
transition: all 0.2s ease;
|
||||
color: rgba(255, 255, 255, 0.8);
|
||||
|
||||
&:hover {
|
||||
background: rgba(255, 255, 255, 0.2);
|
||||
border-color: rgba(255, 255, 255, 0.3);
|
||||
color: rgba(255, 255, 255, 1);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.tags-content {
|
||||
|
|
@ -1856,7 +1884,6 @@ watch(() => autoPlayMode.value, () => {
|
|||
.natural-text {
|
||||
margin: 0.5em 0;
|
||||
line-height: 1.6em;
|
||||
text-align: justify;
|
||||
color: rgba(255, 255, 255, 0.8);
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -3,7 +3,7 @@ import fileItemCell from '@/components/FileItem.vue'
|
|||
import '@zanllp/vue-virtual-scroller/dist/vue-virtual-scroller.css'
|
||||
// @ts-ignore
|
||||
import { RecycleScroller } from '@zanllp/vue-virtual-scroller'
|
||||
import { toRawFileUrl } from '@/util/file'
|
||||
import { toImageUrl } from '@/util/file'
|
||||
import { getImagesByTags, type MatchImageByTagsReq } from '@/api/db'
|
||||
import { nextTick, watch, ref } from 'vue'
|
||||
import { copy2clipboardI18n } from '@/util'
|
||||
|
|
@ -162,7 +162,7 @@ const onTiktokViewClick = () => {
|
|||
@file-item-click="onFileItemClick"
|
||||
@tiktok-view="(_file, idx) => openTiktokViewWithFiles(images, idx)"
|
||||
:full-screen-preview-image-url="
|
||||
images[previewIdx] ? toRawFileUrl(images[previewIdx]) : ''
|
||||
images[previewIdx] ? toImageUrl(images[previewIdx]) : ''
|
||||
"
|
||||
:selected="multiSelectedIdxs.includes(idx)"
|
||||
@context-menu-click="onContextMenuClickU"
|
||||
|
|
|
|||
|
|
@ -4,7 +4,7 @@ import fileItemCell from '@/components/FileItem.vue'
|
|||
import '@zanllp/vue-virtual-scroller/dist/vue-virtual-scroller.css'
|
||||
// @ts-ignore
|
||||
import { RecycleScroller } from '@zanllp/vue-virtual-scroller'
|
||||
import { toRawFileUrl } from '@/util/file'
|
||||
import { toImageUrl } from '@/util/file'
|
||||
import { getDbBasicInfo, getExpiredDirs, getImagesBySubstr, updateImageData, type DataBaseBasicInfo, SearchBySubstrReq } from '@/api/db'
|
||||
import { copy2clipboardI18n, makeAsyncFunctionSingle, useGlobalEventListen } from '@/util'
|
||||
import fullScreenContextMenu from '@/page/fileTransfer/fullScreenContextMenu.vue'
|
||||
|
|
@ -335,7 +335,7 @@ const { onClearAllSelected, onSelectAll, onReverseSelect } = useKeepMultiSelect(
|
|||
<template v-slot="{ item: file, index: idx }">
|
||||
<!-- idx 和file有可能丢失 -->
|
||||
<file-item-cell :idx="idx" :file="file" v-model:show-menu-idx="showMenuIdx" @file-item-click="onFileItemClick"
|
||||
:full-screen-preview-image-url="images[previewIdx] ? toRawFileUrl(images[previewIdx]) : ''"
|
||||
:full-screen-preview-image-url="images[previewIdx] ? toImageUrl(images[previewIdx]) : ''"
|
||||
:cell-width="cellWidth" :selected="multiSelectedIdxs.includes(idx)"
|
||||
@context-menu-click="onContextMenuClickU" @dragstart="onFileDragStart" @dragend="onFileDragEnd"
|
||||
@tiktok-view="(_file, idx) => openTiktokViewWithFiles(images, idx)"
|
||||
|
|
|
|||
|
|
@ -3,7 +3,7 @@ import fileItemCell from '@/components/FileItem.vue'
|
|||
import '@zanllp/vue-virtual-scroller/dist/vue-virtual-scroller.css'
|
||||
// @ts-ignore
|
||||
import { RecycleScroller } from '@zanllp/vue-virtual-scroller'
|
||||
import { toRawFileUrl } from '@/util/file'
|
||||
import { toImageUrl } from '@/util/file'
|
||||
import { nextTick, watch, reactive } from 'vue'
|
||||
import { copy2clipboardI18n } from '@/util'
|
||||
import fullScreenContextMenu from '@/page/fileTransfer/fullScreenContextMenu.vue'
|
||||
|
|
@ -173,7 +173,7 @@ const onTiktokViewClick = () => {
|
|||
@dragend="onFileDragEnd"
|
||||
@file-item-click="onFileItemClick"
|
||||
@tiktok-view="(_file, idx) => openTiktokViewWithFiles(images, idx)"
|
||||
:full-screen-preview-image-url="images[previewIdx] ? toRawFileUrl(images[previewIdx]) : ''"
|
||||
:full-screen-preview-image-url="images[previewIdx] ? toImageUrl(images[previewIdx]) : ''"
|
||||
:selected="multiSelectedIdxs.includes(idx)"
|
||||
@context-menu-click="onContextMenuClickU"
|
||||
@preview-visible-change="onPreviewVisibleChange"
|
||||
|
|
|
|||
|
|
@ -7,7 +7,7 @@ import { useBatchDownloadStore } from '@/store/useBatchDownloadStore'
|
|||
import { useGlobalStore } from '@/store/useGlobalStore'
|
||||
import { storeToRefs } from 'pinia'
|
||||
import { useFilesDisplay, useHookShareState } from '@/page/fileTransfer/hook'
|
||||
import { getFileTransferDataFromDragEvent, toRawFileUrl } from '@/util/file'
|
||||
import { getFileTransferDataFromDragEvent, toImageUrl } from '@/util/file'
|
||||
import { axiosInst } from '@/api'
|
||||
import { createReactiveQueue } from '@/util'
|
||||
import { message } from 'ant-design-vue'
|
||||
|
|
@ -73,7 +73,7 @@ const onDeleteClick = (idx: number) => {
|
|||
key-field="fullpath" :item-secondary-size="itemSize.second" :gridItems="gridItems">
|
||||
<template v-slot="{ item: file, index: idx }">
|
||||
<file-item :idx="idx" :file="file" :cell-width="cellWidth" enable-close-icon
|
||||
@close-icon-click="onDeleteClick(idx)" :full-screen-preview-image-url="toRawFileUrl(file)"
|
||||
@close-icon-click="onDeleteClick(idx)" :full-screen-preview-image-url="toImageUrl(file)"
|
||||
:enable-right-click-menu="false" />
|
||||
</template>
|
||||
</RecycleScroller>
|
||||
|
|
|
|||
|
|
@ -2,6 +2,7 @@
|
|||
import { getImageGenerationInfo, getImageExif } from '@/api'
|
||||
import type { FileNodeInfo } from '@/api/files'
|
||||
import ExifBrowser from '@/components/ExifBrowser.vue'
|
||||
import DraggableImage from '@/components/DraggableImage.vue'
|
||||
import { useGlobalStore } from '@/store/useGlobalStore'
|
||||
import { useLocalStorage } from '@vueuse/core'
|
||||
import type { MenuInfo } from 'ant-design-vue/lib/menu/src/interface'
|
||||
|
|
@ -18,7 +19,9 @@ import {
|
|||
EllipsisOutlined,
|
||||
fullscreen,
|
||||
SortAscendingOutlined,
|
||||
AppstoreOutlined
|
||||
AppstoreOutlined,
|
||||
EditOutlined,
|
||||
SettingOutlined
|
||||
} from '@/icon'
|
||||
import { t } from '@/i18n'
|
||||
import { createReactiveQueue, unescapeHtml } from '@/util'
|
||||
|
|
@ -29,7 +32,7 @@ import { parse } from '@/util/stable-diffusion-image-metadata'
|
|||
import { useFullscreenLayout } from '@/util/useFullscreenLayout'
|
||||
import { useMouseInElement } from '@vueuse/core'
|
||||
import { closeImageFullscreenPreview } from '@/util/imagePreviewOperation'
|
||||
import { openAddNewTagModal } from '@/components/functionalCallableComp'
|
||||
import { openAddNewTagModal, openEditPromptModal } from '@/components/functionalCallableComp'
|
||||
import { prefix } from '@/util/const'
|
||||
// @ts-ignore
|
||||
import * as Pinyin from 'jian-pinyin'
|
||||
|
|
@ -183,7 +186,7 @@ function getParNode (p: any) {
|
|||
}
|
||||
|
||||
function getTextLength(text: string): number {
|
||||
// 中文字符按3个英文字母计算
|
||||
// chinese characters are counted as 3 English letters
|
||||
let length = 0
|
||||
for (const char of text) {
|
||||
if (/[\u4e00-\u9fa5]/.test(char)) {
|
||||
|
|
@ -434,6 +437,18 @@ Please return only tag names, do not include any other content.`
|
|||
}
|
||||
}
|
||||
|
||||
// 编辑提示词并重新加载
|
||||
const editPromptAndReload = async () => {
|
||||
await openEditPromptModal(props.file)
|
||||
const path = props.file?.fullpath
|
||||
if (path) {
|
||||
q.tasks.forEach((v) => v.cancel())
|
||||
q.pushAction(() => getImageGenerationInfo(path)).res.then((v) => {
|
||||
imageGenInfo.value = v
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
</script>
|
||||
|
||||
<template>
|
||||
|
|
@ -442,6 +457,7 @@ Please return only tag names, do not include any other content.`
|
|||
|
||||
<div v-if="lr">
|
||||
</div>
|
||||
|
||||
<div class="container">
|
||||
<div class="action-bar">
|
||||
<div v-if="!lr" ref="dragHandle" class="icon" style="cursor: grab" :title="t('dragToMovePanel')">
|
||||
|
|
@ -453,6 +469,22 @@ Please return only tag names, do not include any other content.`
|
|||
<FullscreenExitOutlined v-if="showFullContent" />
|
||||
<FullscreenOutlined v-else />
|
||||
</div>
|
||||
<a-dropdown :get-popup-container="getParNode" trigger="click">
|
||||
<div class="icon" style="cursor: pointer">
|
||||
<SettingOutlined />
|
||||
</div>
|
||||
<template #overlay>
|
||||
<div class="block-visibility-settings">
|
||||
<div class="settings-title">{{ $t('blockVisibilitySettings') }}</div>
|
||||
<div class="settings-list">
|
||||
<div class="settings-item" v-for="(_, key) in global.fullscreenMenuBlockVisibility" :key="key">
|
||||
<a-switch v-model:checked="global.fullscreenMenuBlockVisibility[key]" size="small" />
|
||||
<span class="settings-label">{{ $t(`blockName_${key}`) }}</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
</a-dropdown>
|
||||
<div style="display: flex; flex-direction: column; align-items: center; cursor: grab" class="icon"
|
||||
:title="t('fullscreenview')" @click="requestFullscreen">
|
||||
<img :src="fullscreen" style="width: 21px;height: 21px;padding-bottom: 2px;" alt="">
|
||||
|
|
@ -467,7 +499,7 @@ Please return only tag names, do not include any other content.`
|
|||
</template>
|
||||
</a-dropdown>
|
||||
<div flex-placeholder v-if="showFullContent" />
|
||||
<div v-if="showFullContent" class="action-bar">
|
||||
<div block v-if="showFullContent && global.fullscreenMenuBlockVisibility.actionBar" class="action-bar">
|
||||
|
||||
<a-dropdown :trigger="['hover']" :get-popup-container="getParNode">
|
||||
<a-button>{{ t('openContextMenu') }}</a-button>
|
||||
|
|
@ -516,7 +548,6 @@ Please return only tag names, do not include any other content.`
|
|||
}}</a-button>
|
||||
<a-button
|
||||
@click="analyzeTagsWithAI"
|
||||
type="primary"
|
||||
:loading="analyzingTags"
|
||||
v-if="imageGenInfo && global.conf?.all_custom_tags?.length"
|
||||
>
|
||||
|
|
@ -528,11 +559,17 @@ Please return only tag names, do not include any other content.`
|
|||
type="default"
|
||||
>
|
||||
{{ $t('tiktokView') }}
|
||||
</a-button> <a-button
|
||||
@click="editPromptAndReload"
|
||||
>
|
||||
<template #icon><EditOutlined /></template>
|
||||
{{ $t('editPrompt') }}
|
||||
</a-button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="gen-info" v-if="showFullContent">
|
||||
<div class="info-tags">
|
||||
|
||||
<div block v-if="global.fullscreenMenuBlockVisibility.infoTags" class="info-tags">
|
||||
<span class="info-tag">
|
||||
<span class="name">
|
||||
{{ $t('fileName') }}
|
||||
|
|
@ -554,7 +591,7 @@ Please return only tag names, do not include any other content.`
|
|||
</span>
|
||||
</span>
|
||||
</div>
|
||||
<div class="tags-container" v-if="global.conf?.all_custom_tags">
|
||||
<div block class="tags-container" v-if="global.conf?.all_custom_tags && global.fullscreenMenuBlockVisibility.tagsContainer">
|
||||
<div class="sort-tag-switch" @click="tagA2ZClassify = !tagA2ZClassify">
|
||||
<SortAscendingOutlined v-if="!tagA2ZClassify" />
|
||||
<AppstoreOutlined v-else />
|
||||
|
|
@ -583,7 +620,7 @@ Please return only tag names, do not include any other content.`
|
|||
</div>
|
||||
</template>
|
||||
</div>
|
||||
<div class="lr-layout-control">
|
||||
<div block class="lr-layout-control" v-if="global.fullscreenMenuBlockVisibility.lrLayoutControl">
|
||||
<div class="ctrl-item">
|
||||
{{ $t('experimentalLRLayout') }}: <a-switch v-model:checked="lr" size="small" />
|
||||
</div>
|
||||
|
|
@ -600,22 +637,57 @@ Please return only tag names, do not include any other content.`
|
|||
</a-tooltip>
|
||||
</template>
|
||||
</div>
|
||||
<a-tabs v-model:activeKey="promptTabActivedKey">
|
||||
<!-- 可拖拽的原图 -->
|
||||
<DraggableImage block v-if="global.fullscreenMenuBlockVisibility.draggableImage" :file="file">
|
||||
<div class="custom-drag-trigger">
|
||||
<DragOutlined class="trigger-icon" />
|
||||
<span class="trigger-text">{{ $t('dragImageToTransfer') }}</span>
|
||||
</div>
|
||||
</DraggableImage>
|
||||
|
||||
<a-tabs block v-if="global.fullscreenMenuBlockVisibility.tabs" v-model:activeKey="promptTabActivedKey">
|
||||
<a-tab-pane key="structedData" :tab="$t('structuredData')">
|
||||
<div>
|
||||
<template v-if="geninfoStruct.prompt">
|
||||
<br />
|
||||
<h3>Prompt</h3>
|
||||
<div class="section-header">
|
||||
<h3>Prompt</h3>
|
||||
<button
|
||||
class="edit-section-btn"
|
||||
@click="editPromptAndReload"
|
||||
:title="$t('editPrompt')"
|
||||
>
|
||||
<EditOutlined />
|
||||
</button>
|
||||
</div>
|
||||
<code v-html="spanWrap(geninfoStruct.prompt ?? '')"></code>
|
||||
</template>
|
||||
<template v-if="geninfoStruct.negativePrompt">
|
||||
<br />
|
||||
<h3>Negative Prompt</h3>
|
||||
<code v-html="spanWrap(geninfoStruct.negativePrompt ?? '')"></code>
|
||||
<div class="section-header">
|
||||
<h3>Negative Prompt</h3>
|
||||
<button
|
||||
class="edit-section-btn"
|
||||
@click="editPromptAndReload"
|
||||
:title="$t('editPrompt')"
|
||||
>
|
||||
<EditOutlined />
|
||||
</button>
|
||||
</div>
|
||||
<code v-html="spanWrap(geninfoStruct.negativePrompt ?? '')"></code>
|
||||
</template>
|
||||
</div>
|
||||
<template v-if="Object.keys(geninfoStructNoPrompts).length"> <br />
|
||||
<h3>Params</h3>
|
||||
<div class="section-header">
|
||||
<h3>Params</h3>
|
||||
<button
|
||||
class="edit-section-btn"
|
||||
@click="editPromptAndReload"
|
||||
:title="$t('editPrompt')"
|
||||
>
|
||||
<EditOutlined />
|
||||
</button>
|
||||
</div>
|
||||
<table>
|
||||
<tr v-for="txt, key in geninfoStructNoPrompts" :key="key" class="gen-info-frag">
|
||||
<td style="font-weight: 600;text-transform: capitalize;">{{ key }}</td>
|
||||
|
|
@ -629,7 +701,16 @@ Please return only tag names, do not include any other content.`
|
|||
</table>
|
||||
</template>
|
||||
<template v-if="extraJsonMetaInfo && Object.keys(extraJsonMetaInfo).length"> <br />
|
||||
<h3>Extra Meta Info</h3>
|
||||
<div class="section-header">
|
||||
<h3>Extra Meta Info</h3>
|
||||
<button
|
||||
class="edit-section-btn"
|
||||
@click="editPromptAndReload"
|
||||
:title="$t('editPrompt')"
|
||||
>
|
||||
<EditOutlined />
|
||||
</button>
|
||||
</div>
|
||||
<table class="extra-meta-table">
|
||||
<tr v-for="(val, key) in extraJsonMetaInfo" :key="key" class="gen-info-frag">
|
||||
<td style="font-weight: 600;text-transform: capitalize;">{{ key }}</td>
|
||||
|
|
@ -728,7 +809,6 @@ Please return only tag names, do not include any other content.`
|
|||
.natural-text {
|
||||
margin: 0.5em 0;
|
||||
line-height: 1.6em;
|
||||
text-align: justify;
|
||||
color: var(--zp-primary);
|
||||
}
|
||||
|
||||
|
|
@ -915,4 +995,86 @@ Please return only tag names, do not include any other content.`
|
|||
flex-wrap: nowrap;
|
||||
}
|
||||
}
|
||||
|
||||
.section-header {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
margin-bottom: 8px;
|
||||
gap: 6px;
|
||||
|
||||
h3 {
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
.edit-section-btn {
|
||||
margin: 0;
|
||||
padding: 2px 6px;
|
||||
background: rgba(255, 255, 255, 0.05);
|
||||
border: 1px solid rgba(255, 255, 255, 0.15);
|
||||
border-radius: 4px;
|
||||
cursor: pointer;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
transition: all 0.2s ease;
|
||||
color: var(--zp-primary);
|
||||
font-size: 12px;
|
||||
line-height: 1;
|
||||
min-width: 22px;
|
||||
min-height: 22px;
|
||||
|
||||
&:hover {
|
||||
background: rgba(255, 255, 255, 0.1);
|
||||
border-color: rgba(255, 255, 255, 0.25);
|
||||
color: var(--zp-luminous);
|
||||
transform: scale(1.05);
|
||||
}
|
||||
|
||||
&:active {
|
||||
transform: scale(0.95);
|
||||
}
|
||||
|
||||
:deep(.anticon) {
|
||||
font-size: 12px;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.block-visibility-settings {
|
||||
background: var(--zp-primary-background);
|
||||
border-radius: 8px;
|
||||
padding: 12px;
|
||||
min-width: 200px;
|
||||
max-width: 300px;
|
||||
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.15);
|
||||
border: 1px solid var(--zp-secondary);
|
||||
|
||||
.settings-title {
|
||||
font-size: 14px;
|
||||
font-weight: 600;
|
||||
margin-bottom: 12px;
|
||||
padding-bottom: 8px;
|
||||
border-bottom: 1px solid var(--zp-secondary);
|
||||
color: var(--zp-primary);
|
||||
}
|
||||
|
||||
.settings-list {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.settings-item {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
padding: 4px 0;
|
||||
|
||||
.settings-label {
|
||||
flex: 1;
|
||||
font-size: 13px;
|
||||
color: var(--zp-primary);
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
|
|
|||
|
|
@ -16,7 +16,7 @@ import {
|
|||
useGenInfoDiff
|
||||
} from './hook'
|
||||
import { SearchSelect } from 'vue3-ts-util'
|
||||
import { toRawFileUrl } from '@/util/file'
|
||||
import { toImageUrl } from '@/util/file'
|
||||
import { openTiktokViewWithFiles } from '@/util/tiktokHelper'
|
||||
|
||||
import 'multi-nprogress/nprogress.css'
|
||||
|
|
@ -385,7 +385,7 @@ onMounted(() => {
|
|||
<template v-slot="{ item: file, index: idx }">
|
||||
<!-- idx 和file有可能丢失 -->
|
||||
<file-item :idx="parseInt(idx)" :file="file"
|
||||
:full-screen-preview-image-url="sortedFiles[previewIdx] ? toRawFileUrl(sortedFiles[previewIdx]) : ''"
|
||||
:full-screen-preview-image-url="sortedFiles[previewIdx] ? toImageUrl(sortedFiles[previewIdx]) : ''"
|
||||
v-model:show-menu-idx="showMenuIdx" :selected="multiSelectedIdxs.includes(idx)" :cell-width="cellWidth"
|
||||
@file-item-click="onFileItemClick" @dragstart="onFileDragStart" @dragend="onFileDragEnd"
|
||||
@preview-visible-change="onPreviewVisibleChange" @context-menu-click="onContextMenuClick"
|
||||
|
|
|
|||
|
|
@ -4,7 +4,7 @@ import { RecycleScroller } from '@zanllp/vue-virtual-scroller'
|
|||
import '@zanllp/vue-virtual-scroller/dist/vue-virtual-scroller.css'
|
||||
import FileItem from '@/components/FileItem.vue'
|
||||
import { useFilesDisplay, useHookShareState } from '@/page/fileTransfer/hook'
|
||||
import { getFileTransferDataFromDragEvent, toRawFileUrl, uniqueFile } from '@/util/file'
|
||||
import { getFileTransferDataFromDragEvent, toImageUrl, uniqueFile } from '@/util/file'
|
||||
import { ref, watchEffect, toRaw } from 'vue'
|
||||
import { GridViewFile, useGlobalStore } from '@/store/useGlobalStore'
|
||||
import { useTagStore } from '@/store/useTagStore'
|
||||
|
|
@ -52,7 +52,7 @@ watchEffect(() => {
|
|||
key-field="fullpath" :item-secondary-size="itemSize.second" :gridItems="gridItems">
|
||||
<template v-slot="{ item: file, index: idx }">
|
||||
<file-item :idx="idx" :file="file" :cell-width="cellWidth" :enable-close-icon="props.removable"
|
||||
@close-icon-click="onDeleteClick(idx)" :full-screen-preview-image-url="toRawFileUrl(file)"
|
||||
@close-icon-click="onDeleteClick(idx)" :full-screen-preview-image-url="toImageUrl(file)"
|
||||
:extra-tags="file?.tags?.map(tag.tagConvert)" :enable-right-click-menu="false" />
|
||||
</template>
|
||||
</RecycleScroller>
|
||||
|
|
|
|||
|
|
@ -4,7 +4,7 @@ import { RecycleScroller } from '@zanllp/vue-virtual-scroller'
|
|||
import '@zanllp/vue-virtual-scroller/dist/vue-virtual-scroller.css'
|
||||
import FileItem from '@/components/FileItem.vue'
|
||||
import { useFileItemActions, useFilesDisplay, useFileTransfer, useHookShareState, useKeepMultiSelect, usePreview } from '@/page/fileTransfer/hook'
|
||||
import { toRawFileUrl } from '@/util/file'
|
||||
import { toImageUrl } from '@/util/file'
|
||||
import { ref, onMounted } from 'vue'
|
||||
import { GridViewFile, useGlobalStore } from '@/store/useGlobalStore'
|
||||
import { getRandomImages } from '@/api/db'
|
||||
|
|
@ -142,7 +142,7 @@ const onContextMenuClickU: typeof onContextMenuClick = async (e, file, idx) => {
|
|||
<RecycleScroller ref="scroller" class="file-list" :items="files.slice()" :item-size="itemSize.first"
|
||||
key-field="fullpath" :item-secondary-size="itemSize.second" :gridItems="gridItems" @scroll="onScroll">
|
||||
<template v-slot="{ item: file, index: idx }">
|
||||
<file-item :idx="idx" :file="file" :cell-width="cellWidth" :full-screen-preview-image-url="images[previewIdx] ? toRawFileUrl(images[previewIdx]) : ''
|
||||
<file-item :idx="idx" :file="file" :cell-width="cellWidth" :full-screen-preview-image-url="images[previewIdx] ? toImageUrl(images[previewIdx]) : ''
|
||||
" @context-menu-click="onContextMenuClickU" @preview-visible-change="onPreviewVisibleChange"
|
||||
:is-selected-mutil-files="multiSelectedIdxs.length > 1" :selected="multiSelectedIdxs.includes(idx)"
|
||||
@file-item-click="onFileItemClick" @tiktok-view="(_file, idx) => openTiktokViewWithFiles(files, idx)" />
|
||||
|
|
|
|||
|
|
@ -164,10 +164,19 @@ export interface Tab {
|
|||
key: string
|
||||
}
|
||||
|
||||
export type Shortcut = Record<`toggle_tag_${string}` | 'delete' | 'download' | `copy_to_${string}`| `move_to_${string}`, string | undefined>
|
||||
export type Shortcut = Record<`toggle_tag_${string}` | 'delete' | 'download' | `copy_to_${string}`| `move_to_${string}`, string | undefined>
|
||||
|
||||
export type DefaultInitinalPage = `workspace_snapshot_${string}` | 'empty' | 'last-workspace-state'
|
||||
|
||||
export type FullscreenMenuBlockVisibility = {
|
||||
actionBar: boolean
|
||||
infoTags: boolean
|
||||
tagsContainer: boolean
|
||||
lrLayoutControl: boolean
|
||||
draggableImage: boolean
|
||||
tabs: boolean
|
||||
}
|
||||
|
||||
export const copyPane = (pane: TabPane) => {
|
||||
return cloneDeep({
|
||||
...pane,
|
||||
|
|
@ -226,7 +235,8 @@ export const presistKeys = [
|
|||
'magicSwitchTiktokView',
|
||||
'showRandomImageInStartup',
|
||||
'showTiktokNavigator',
|
||||
'autoUpdateIndex'
|
||||
'autoUpdateIndex',
|
||||
'fullscreenMenuBlockVisibility'
|
||||
]
|
||||
|
||||
function cellWidthMap(x: number): number {
|
||||
|
|
@ -381,6 +391,16 @@ export const useGlobalStore = defineStore(
|
|||
const showRandomImageInStartup = ref(true)
|
||||
const showTiktokNavigator = ref(false)
|
||||
|
||||
// Fullscreen menu block visibility settings
|
||||
const fullscreenMenuBlockVisibility = ref<FullscreenMenuBlockVisibility>({
|
||||
actionBar: true,
|
||||
infoTags: true,
|
||||
tagsContainer: true,
|
||||
lrLayoutControl: true,
|
||||
draggableImage: true,
|
||||
tabs: true
|
||||
})
|
||||
|
||||
// ===== Organize Jobs Management =====
|
||||
interface OrganizeJob {
|
||||
job_id: string
|
||||
|
|
@ -467,6 +487,7 @@ export const useGlobalStore = defineStore(
|
|||
magicSwitchTiktokView,
|
||||
showRandomImageInStartup,
|
||||
autoUpdateIndex: ref(true),
|
||||
fullscreenMenuBlockVisibility,
|
||||
// Organize jobs
|
||||
activeOrganizeJobs,
|
||||
showOrganizePanel,
|
||||
|
|
|
|||
|
|
@ -7,6 +7,11 @@ const encode = encodeURIComponent
|
|||
export const toRawFileUrl = (file: FileNodeInfo, download = false) =>
|
||||
`${apiBase.value}/file?path=${encode(file.fullpath)}&t=${encode(file.date)}${download ? `&disposition=${encode(file.name)}` : ''
|
||||
}`
|
||||
|
||||
export const toImageUrl = (file: FileNodeInfo) => {
|
||||
return `${apiBase.value}/img/${encode(file.name)}?path=${encode(file.fullpath)}&t=${encode(file.date)}`
|
||||
}
|
||||
|
||||
export const toImageThumbnailUrl = (file: FileNodeInfo, size: string = '512x512') => {
|
||||
return `${apiBase.value}/image-thumbnail?path=${encode(file.fullpath)}&size=${size}&t=${encode(
|
||||
file.date
|
||||
|
|
|
|||
|
|
@ -107,6 +107,8 @@ export const { useEventListen: useGlobalEventListen, eventEmitter: globalEvents
|
|||
closeTabPane(tabIdx: number, key: string): void
|
||||
updateGlobalSettingDone(): void
|
||||
refreshFileView(args?: { paths?: string[] }): void
|
||||
openPromptEditor(data: { file: { name: string; fullpath: string }}): void
|
||||
promptEditorUpdated(): void
|
||||
}>()
|
||||
|
||||
type AsyncFunction<T> = (...args: any[]) => Promise<T>
|
||||
|
|
|
|||
|
|
@ -79,7 +79,7 @@ export function parse(parameters: string): ImageMeta {
|
|||
const extraJsonMetaInfoMatch = parameters.match(/\nextraJsonMetaInfo:\s*(\{[\s\S]*\})\s*$/);
|
||||
if (extraJsonMetaInfoMatch) {
|
||||
try {
|
||||
metadata.extraJsonMetaInfo = JSON.parse(extraJsonMetaInfoMatch[1]);
|
||||
metadata.extraJsonMetaInfo = JSON.parse(unescapeHtml(extraJsonMetaInfoMatch[1]));
|
||||
// 从原始参数中移除 extraJsonMetaInfo 部分
|
||||
parameters = parameters.replace(/\nextraJsonMetaInfo:\s*\{[\s\S]*\}\s*$/, '');
|
||||
} catch (e) {
|
||||
|
|
|
|||
Loading…
Reference in New Issue