feat(metadata): add editable generation information and EXIF caching
- Add prompt editor modal with support for editing positive/negative prompts - Add key-value editor for custom metadata fields (string and JSON modes) - Cache EXIF data in database for faster retrieval - Track manually edited prompts with exif_edited flag - Add validation for required fields and unique key constraints - Add full internationalization support (EN, ZH-Hans, ZH-Hant, DE) - Update changelog with new features and screenshot - Clean up gitignore to use wildcard for video filespull/934/head
parent
fcc23b5337
commit
41ad78f6b3
|
|
@ -27,7 +27,4 @@ iib.db-wal
|
|||
|
||||
CLAUDE.md
|
||||
.claude/*
|
||||
videos/启动&添加文件夹构建索引.mp4
|
||||
videos/图像搜索和链接跳转.mp4
|
||||
videos/ai智能文件整理.mp4
|
||||
videos/skills安装&启动.mp4
|
||||
videos/*
|
||||
|
|
|
|||
|
|
@ -1,6 +1,29 @@
|
|||
[跳到中文](#中文)
|
||||
# English
|
||||
|
||||
## 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
|
||||
- **Multi-language Support**: Full internationalization for all editing features (English, Chinese, German)
|
||||
|
||||
**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.
|
||||
|
|
@ -727,6 +750,29 @@ Triggered under the same circumstances as above, there will be a button to updat
|
|||
|
||||
# 中文
|
||||
|
||||
## 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 添加了原地播放功能。
|
||||
|
|
|
|||
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-d9bd93cc.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,
|
||||
|
|
@ -807,11 +808,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 ""
|
||||
|
|
@ -859,6 +873,35 @@ def infinite_image_browsing_api(app: FastAPI, **kwargs):
|
|||
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)])
|
||||
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)
|
||||
|
|
|
|||
|
|
@ -56,9 +56,11 @@ declare module '@vue/runtime-core' {
|
|||
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
|
|
@ -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-d9bd93cc.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};
|
||||
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
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,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-d9bd93cc.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,dM as f,aS as n}from"./index-d9bd93cc.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};
|
||||
|
|
@ -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 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-d9bd93cc.js";import{F as j,s as L}from"./FileItem-d24296ad.js";import{u as H,b as O,j as W}from"./index-41624be1.js";import"./index-71593fa5.js";import"./shortcut-77300e08.js";import"./_isIterateeCall-7124c9f9.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};
|
||||
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{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};
|
||||
import{F as w,s as y}from"./FileItem-d24296ad.js";import{u as k,b as x}from"./index-41624be1.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-d9bd93cc.js";import"./index-71593fa5.js";import"./shortcut-77300e08.js";import"./_isIterateeCall-7124c9f9.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 +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-d9bd93cc.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-41624be1.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
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-d9bd93cc.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
|
|
@ -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};
|
||||
|
|
@ -0,0 +1 @@
|
|||
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-d9bd93cc.js";import{F as ve,s as ge}from"./FileItem-d24296ad.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-41624be1.js";import{M as Ie,L as ye,R as be,f as xe}from"./MultiSelectKeep-f14d9552.js";import"./index-71593fa5.js";import"./shortcut-77300e08.js";import"./_isIterateeCall-7124c9f9.js";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 Ge=pe(Fe,[["__scopeId","data-v-49082269"]]);export{Ge 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
|
|
@ -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-d9bd93cc.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,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 = (
|
||||
// 合并的视频/音频 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',
|
||||
|
|
@ -76,94 +85,95 @@ export const openVideoModal = (
|
|||
'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,11 +185,16 @@ 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)',
|
||||
|
|
@ -199,6 +214,8 @@ export const openAudioModal = (
|
|||
{ 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 })
|
||||
})
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -319,5 +319,39 @@ 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: 'Unterstützt alle gültigen JSON-Werte (Objekte, Arrays, Zahlen, Boolesche Werte, etc.). String-Modus fügt automatisch doppelte Anführungszeichen hinzu.',
|
||||
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'
|
||||
}
|
||||
|
|
|
|||
|
|
@ -579,5 +579,39 @@ 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: 'Supports any valid JSON values (objects, arrays, numbers, booleans, etc.). String mode will automatically add double quotes.',
|
||||
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'
|
||||
}
|
||||
|
|
|
|||
|
|
@ -557,5 +557,39 @@ 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: '支持任何合法 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: '切换到字符串模式前请先清空当前值'
|
||||
}
|
||||
|
|
|
|||
|
|
@ -559,5 +559,39 @@ 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: '支持任何合法 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: '切換到字符串模式前請先清空當前值'
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -18,7 +18,8 @@ import {
|
|||
EllipsisOutlined,
|
||||
fullscreen,
|
||||
SortAscendingOutlined,
|
||||
AppstoreOutlined
|
||||
AppstoreOutlined,
|
||||
EditOutlined
|
||||
} from '@/icon'
|
||||
import { t } from '@/i18n'
|
||||
import { createReactiveQueue, unescapeHtml } from '@/util'
|
||||
|
|
@ -29,7 +30,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'
|
||||
|
|
@ -434,6 +435,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>
|
||||
|
|
@ -516,7 +529,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,6 +540,11 @@ 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>
|
||||
|
|
@ -605,17 +622,44 @@ Please return only tag names, do not include any other content.`
|
|||
<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>
|
||||
<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 +673,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 +781,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 +967,48 @@ 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;
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
|
|
|||
|
|
@ -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