Logankunfall commited on
Commit
f4541b5
·
verified ·
1 Parent(s): 54e050d

Upload 21 files

Browse files
Files changed (3) hide show
  1. backend/app.py +0 -110
  2. frontend/assets/app.js +25 -201
  3. frontend/index.html +0 -27
backend/app.py CHANGED
@@ -62,116 +62,6 @@ def update_config(update: ConfigUpdate) -> Dict[str, Any]:
62
  return cfg.model_dump()
63
 
64
 
65
- @app.get("/api/browse-dir")
66
- def api_browse_dir(path: str = "") -> Dict[str, Any]:
67
- """
68
- 浏览服务器上的目录,返回子目录列表。
69
- 用于网页版目录选择器,支持手机和电脑。
70
- """
71
- # 默认从用户主目录或根目录开始
72
- if not path:
73
- if platform.system() == "Windows":
74
- # Windows: 列出所有驱动器
75
- import string
76
- drives = []
77
- for letter in string.ascii_uppercase:
78
- drive_path = f"{letter}:\\"
79
- if os.path.exists(drive_path):
80
- drives.append({"name": f"{letter}:", "path": drive_path, "type": "drive"})
81
- return {"current": "", "parent": "", "items": drives, "is_root": True, "is_drive_list": True}
82
- else:
83
- # Linux/HF Space 环境:使用主目录
84
- home_dir = os.path.expanduser("~")
85
- # 如果是 HF Space 环境,检查 /home/user 或工作目录
86
- if os.environ.get("SPACE_ID"):
87
- # HF Space 通常在 /home/user 目录
88
- if os.path.exists("/home/user"):
89
- path = "/home/user"
90
- else:
91
- path = home_dir
92
- else:
93
- path = home_dir
94
-
95
- # 规范化路径
96
- try:
97
- p = Path(path).resolve()
98
- if not p.exists():
99
- raise HTTPException(status_code=404, detail=f"路径不存在: {path}")
100
- if not p.is_dir():
101
- raise HTTPException(status_code=400, detail=f"不是目录: {path}")
102
- except Exception as e:
103
- raise HTTPException(status_code=400, detail=str(e))
104
-
105
- # 获取父目录
106
- parent = str(p.parent) if p.parent != p else ""
107
-
108
- # Windows根目录特殊处理
109
- is_root = False
110
- if platform.system() == "Windows":
111
- # 如果是驱动器根目录 (如 C:\),parent设为空表示返回驱动器列表
112
- if len(str(p)) <= 3: # C:\ 或类似
113
- parent = ""
114
- is_root = True
115
- else:
116
- if str(p) == "/":
117
- parent = ""
118
- is_root = True
119
-
120
- # 列出子目录
121
- items = []
122
- try:
123
- for item in sorted(p.iterdir()):
124
- if item.is_dir():
125
- try:
126
- # 尝试访问目录以确保有权限
127
- list(item.iterdir())
128
- items.append({
129
- "name": item.name,
130
- "path": str(item),
131
- "type": "folder"
132
- })
133
- except PermissionError:
134
- # 无权限访问的目录仍然显示,但标记为不可访问
135
- items.append({
136
- "name": item.name,
137
- "path": str(item),
138
- "type": "folder",
139
- "accessible": False
140
- })
141
- except Exception:
142
- pass
143
- except PermissionError:
144
- raise HTTPException(status_code=403, detail=f"无权限访问: {path}")
145
- except Exception as e:
146
- raise HTTPException(status_code=500, detail=str(e))
147
-
148
- return {
149
- "current": str(p),
150
- "parent": parent,
151
- "items": items,
152
- "is_root": is_root
153
- }
154
-
155
-
156
- @app.post("/api/create-dir")
157
- def api_create_dir(payload: Dict[str, Any]) -> Dict[str, Any]:
158
- """
159
- 创建新目录
160
- """
161
- path = (payload or {}).get("path", "")
162
- if not path:
163
- raise HTTPException(status_code=400, detail="未提供路径")
164
-
165
- try:
166
- p = Path(path)
167
- p.mkdir(parents=True, exist_ok=True)
168
- return {"ok": True, "path": str(p.resolve())}
169
- except PermissionError:
170
- raise HTTPException(status_code=403, detail=f"无权限创建目录: {path}")
171
- except Exception as e:
172
- raise HTTPException(status_code=500, detail=str(e))
173
-
174
-
175
  @app.post("/api/open-dir")
176
  def api_open_dir(payload: Dict[str, Any]) -> Dict[str, Any]:
177
  """
 
62
  return cfg.model_dump()
63
 
64
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
65
  @app.post("/api/open-dir")
66
  def api_open_dir(payload: Dict[str, Any]) -> Dict[str, Any]:
67
  """
frontend/assets/app.js CHANGED
@@ -972,215 +972,45 @@
972
  ensureCountField("tab-i2i", "i2i-count");
973
  ensureCountField("tab-inpaint", "inpaint-count");
974
 
975
- // ===== 网页版目录浏览器 =====
976
- let currentBrowsePath = "";
977
- let isDriveListMode = false; // 标记是否在驱动器选择界面
978
-
979
- async function loadDirList(path = "") {
980
- try {
981
- const res = await fetch(`/api/browse-dir?path=${encodeURIComponent(path)}`);
982
- if (!res.ok) {
983
- const body = await res.json().catch(() => ({}));
984
- throw new Error(body?.detail || "加载目录失败");
985
- }
986
- return await res.json();
987
- } catch (e) {
988
- throw e;
989
- }
990
- }
991
-
992
- function renderDirList(data) {
993
- const listEl = byId("dir-list");
994
- const pathInput = byId("dir-current-path");
995
- if (!listEl || !pathInput) return;
996
-
997
- currentBrowsePath = data.current || "";
998
- isDriveListMode = !!data.is_drive_list; // 是否在驱动器列表模式
999
- pathInput.value = currentBrowsePath || (isDriveListMode ? "(请选择驱动器)" : "");
1000
-
1001
- if (!data.items || data.items.length === 0) {
1002
- listEl.innerHTML = '<div class="dir-browser-empty">此目录为空或无子目录</div>';
1003
- return;
1004
- }
1005
-
1006
- listEl.innerHTML = data.items.map(item => {
1007
- const icon = item.type === "drive" ? "💾" : "📁";
1008
- const accessible = item.accessible !== false;
1009
- const cls = accessible ? "dir-item" : "dir-item inaccessible";
1010
- return `<div class="${cls}" data-path="${item.path}" data-accessible="${accessible}">
1011
- <span class="dir-item-icon">${icon}</span>
1012
- <span class="dir-item-name">${item.name}</span>
1013
- </div>`;
1014
- }).join("");
1015
-
1016
- // 绑定点击事件
1017
- listEl.querySelectorAll(".dir-item").forEach(el => {
1018
- el.addEventListener("click", async () => {
1019
- if (el.dataset.accessible === "false") {
1020
- toast("无权限访问此目录", "error");
1021
- return;
1022
- }
1023
- const p = el.dataset.path;
1024
- if (p) {
1025
- try {
1026
- loading.show();
1027
- const data = await loadDirList(p);
1028
- renderDirList(data);
1029
- } catch (e) {
1030
- toast(String(e?.message || e), "error");
1031
- } finally {
1032
- loading.hide();
1033
- }
1034
- }
1035
- });
1036
- });
1037
- }
1038
-
1039
- function openDirBrowser() {
1040
- const modal = byId("dir-browser-modal");
1041
- if (modal) {
1042
- modal.classList.remove("hidden");
1043
- // 加载当前配置的目录或根目录
1044
- const currentDir = byId("cfg-output-dir")?.value || "";
1045
- loading.show();
1046
- loadDirList(currentDir)
1047
- .then(data => renderDirList(data))
1048
- .catch(e => {
1049
- // 如果当前目录无效,加载根目录
1050
- return loadDirList("").then(data => renderDirList(data));
1051
- })
1052
- .catch(e => toast(String(e?.message || e), "error"))
1053
- .finally(() => loading.hide());
1054
- }
1055
- }
1056
-
1057
- function closeDirBrowser() {
1058
- const modal = byId("dir-browser-modal");
1059
- if (modal) modal.classList.add("hidden");
1060
- }
1061
-
1062
- // 目录浏览器事件绑定
1063
- const dirCloseBtn = byId("dir-browser-close");
1064
- if (dirCloseBtn) dirCloseBtn.addEventListener("click", closeDirBrowser);
1065
-
1066
- const dirCancelBtn = byId("dir-cancel");
1067
- if (dirCancelBtn) dirCancelBtn.addEventListener("click", closeDirBrowser);
1068
-
1069
- const dirConfirmBtn = byId("dir-confirm");
1070
- if (dirConfirmBtn) {
1071
- dirConfirmBtn.addEventListener("click", () => {
1072
- // 检查是否在驱动器列表模式(没有选择具体目录)
1073
- if (isDriveListMode || !currentBrowsePath) {
1074
- toast("请先进入一个目录再选择", "error");
1075
- return;
1076
- }
1077
-
1078
- const input = byId("cfg-output-dir");
1079
- if (input) {
1080
- input.value = currentBrowsePath;
1081
- autoSaveConfig(); // 自动保存
1082
- toast("已选择保存目录: " + currentBrowsePath, "success");
1083
- }
1084
- closeDirBrowser();
1085
- });
1086
- }
1087
-
1088
- const dirGoUpBtn = byId("dir-go-up");
1089
- if (dirGoUpBtn) {
1090
- dirGoUpBtn.addEventListener("click", async () => {
1091
- try {
1092
- loading.show();
1093
- // 获取当前目录信息以获取父目录
1094
- const data = await loadDirList(currentBrowsePath);
1095
- if (data.parent) {
1096
- const parentData = await loadDirList(data.parent);
1097
- renderDirList(parentData);
1098
- } else {
1099
- // 已经是根目录,显示驱动器列表(Windows)或根目录
1100
- const rootData = await loadDirList("");
1101
- renderDirList(rootData);
1102
- }
1103
- } catch (e) {
1104
- toast(String(e?.message || e), "error");
1105
- } finally {
1106
- loading.hide();
1107
- }
1108
- });
1109
- }
1110
-
1111
- const dirGoHomeBtn = byId("dir-go-home");
1112
- if (dirGoHomeBtn) {
1113
- dirGoHomeBtn.addEventListener("click", async () => {
1114
- try {
1115
- loading.show();
1116
- const data = await loadDirList("");
1117
- renderDirList(data);
1118
- } catch (e) {
1119
- toast(String(e?.message || e), "error");
1120
- } finally {
1121
- loading.hide();
1122
- }
1123
- });
1124
- }
1125
-
1126
- const dirNewFolderBtn = byId("dir-new-folder");
1127
- if (dirNewFolderBtn) {
1128
- dirNewFolderBtn.addEventListener("click", async () => {
1129
- const folderName = prompt("请输入新文件夹名称:");
1130
- if (!folderName || !folderName.trim()) return;
1131
-
1132
- const newPath = currentBrowsePath
1133
- ? `${currentBrowsePath}/${folderName.trim()}`.replace(/\\/g, "/")
1134
- : folderName.trim();
1135
-
1136
  try {
1137
- loading.show();
1138
- const res = await fetch("/api/create-dir", {
1139
- method: "POST",
1140
- headers: { "Content-Type": "application/json" },
1141
- body: JSON.stringify({ path: newPath }),
1142
  });
1143
- if (!res.ok) {
1144
- const body = await res.json().catch(() => ({}));
1145
- throw new Error(body?.detail || "创建文件夹失败");
 
 
 
1146
  }
1147
- toast("文件夹创建成功", "success");
1148
- // 刷新当前目录
1149
- const data = await loadDirList(currentBrowsePath);
1150
- renderDirList(data);
1151
  } catch (e) {
1152
- toast(String(e?.message || e), "error");
1153
- } finally {
1154
- loading.hide();
 
1155
  }
1156
- });
1157
- }
1158
-
1159
- // 模态框背景点击关闭
1160
- const modalOverlay = byId("dir-browser-modal")?.querySelector(".modal-overlay");
1161
- if (modalOverlay) {
1162
- modalOverlay.addEventListener("click", closeDirBrowser);
1163
  }
1164
 
1165
- // 选择保存目录按钮 - 使用网页版目录浏览器(手机和电脑都可用)
1166
  const selOutBtn = byId("btn-select-output-dir");
1167
  if (selOutBtn) {
1168
- selOutBtn.addEventListener("click", async () => {
1169
- openDirBrowser();
1170
- });
1171
  }
1172
 
1173
  // 打开保存目录按钮
1174
  const openOutBtn = byId("btn-open-output-dir");
1175
  if (openOutBtn) {
1176
  openOutBtn.addEventListener("click", async () => {
1177
- // 移动端检测:手机无法打开服务器上的目录,但可以浏览
1178
- if (isMobileDevice()) {
1179
- // 手机端打开目录浏览器查看
1180
- openDirBrowser();
1181
- return;
1182
- }
1183
-
1184
  try {
1185
  loading.show();
1186
  const p = byId("cfg-output-dir")?.value || "";
@@ -1195,13 +1025,7 @@
1195
  });
1196
  if (!res.ok) {
1197
  const body = await res.json().catch(() => ({}));
1198
- const isHF = res.status === 501;
1199
- if (isHF) {
1200
- toast("HF 环境不支持打开系统目录", "info");
1201
- } else {
1202
- throw new Error(body?.detail || "打开目录失败");
1203
- }
1204
- return;
1205
  }
1206
  toast("已打开保存目录", "success");
1207
  } catch (e) {
 
972
  ensureCountField("tab-i2i", "i2i-count");
973
  ensureCountField("tab-inpaint", "inpaint-count");
974
 
975
+ // ===== 选择保存目录 =====
976
+ // 使用 File System Access API(支持的浏览器)或手动输入
977
+ async function selectOutputDirectory() {
978
+ // 尝试使用浏览器原生目录选择器
979
+ if ('showDirectoryPicker' in window) {
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
980
  try {
981
+ const dirHandle = await window.showDirectoryPicker({
982
+ mode: 'readwrite'
 
 
 
983
  });
984
+ // 获取目录名称作为路径提示
985
+ const input = byId("cfg-output-dir");
986
+ if (input) {
987
+ input.value = dirHandle.name;
988
+ autoSaveConfig();
989
+ toast("已选择目录: " + dirHandle.name + "(注意:这是本地目录名,服务器需手动填写完整路径)", "info");
990
  }
 
 
 
 
991
  } catch (e) {
992
+ // 用户取消或不支持
993
+ if (e.name !== 'AbortError') {
994
+ toast("请在输入框中直接填写服务器保存路径", "info");
995
+ }
996
  }
997
+ } else {
998
+ // 不支持目录选择API,提示用户手动输入
999
+ toast("请在输入框中直接填写保存目录路径", "info");
1000
+ byId("cfg-output-dir")?.focus();
1001
+ }
 
 
1002
  }
1003
 
1004
+ // 选择保存目录按钮
1005
  const selOutBtn = byId("btn-select-output-dir");
1006
  if (selOutBtn) {
1007
+ selOutBtn.addEventListener("click", selectOutputDirectory);
 
 
1008
  }
1009
 
1010
  // 打开保存目录按钮
1011
  const openOutBtn = byId("btn-open-output-dir");
1012
  if (openOutBtn) {
1013
  openOutBtn.addEventListener("click", async () => {
 
 
 
 
 
 
 
1014
  try {
1015
  loading.show();
1016
  const p = byId("cfg-output-dir")?.value || "";
 
1025
  });
1026
  if (!res.ok) {
1027
  const body = await res.json().catch(() => ({}));
1028
+ throw new Error(body?.detail || "打开目录失败");
 
 
 
 
 
 
1029
  }
1030
  toast("已打开保存目录", "success");
1031
  } catch (e) {
frontend/index.html CHANGED
@@ -500,33 +500,6 @@
500
  <div class="loading-text">处理中...</div>
501
  </div>
502
 
503
- <!-- 目录浏览器模态框 -->
504
- <div id="dir-browser-modal" class="modal hidden">
505
- <div class="modal-overlay"></div>
506
- <div class="modal-content dir-browser">
507
- <div class="modal-header">
508
- <span class="modal-title">选择保存目录</span>
509
- <button class="modal-close" id="dir-browser-close">×</button>
510
- </div>
511
- <div class="dir-browser-path">
512
- <span>当前路径:</span>
513
- <input type="text" id="dir-current-path" readonly />
514
- </div>
515
- <div class="dir-browser-actions">
516
- <button id="dir-go-up" class="dir-action-btn">⬆ 上级目录</button>
517
- <button id="dir-go-home" class="dir-action-btn">🏠 主目录</button>
518
- <button id="dir-new-folder" class="dir-action-btn">📁 新建文件夹</button>
519
- </div>
520
- <div class="dir-browser-list" id="dir-list">
521
- <!-- 目录列表将动态填充 -->
522
- </div>
523
- <div class="dir-browser-footer">
524
- <button id="dir-cancel" class="secondary">取消</button>
525
- <button id="dir-confirm" class="primary">选择此目录</button>
526
- </div>
527
- </div>
528
- </div>
529
-
530
  <audio id="sound-player" src="" preload="auto"></audio>
531
  <script src="assets/app.js"></script>
532
  </body>
 
500
  <div class="loading-text">处理中...</div>
501
  </div>
502
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
503
  <audio id="sound-player" src="" preload="auto"></audio>
504
  <script src="assets/app.js"></script>
505
  </body>