refactor: file-browser component
file browser component polishing
Alessandro committed
Oct 14, 2025 at 14:39 UTC
e7684b007cdb6d68b5602210a4328a32c8ee27a4
8 files changed
+628
-663
webui/components/chat/input/bottom-actions.html
+4
-1
@@ -2,6 +2,9 @@
2
<head>
3
<script type="module">
4
import { store } from "/components/chat/input/input-store.js";
5
+ import { store as historyStore } from "/components/modals/history-store.js";
6
+ import { store as contextStore } from "/components/modals/context-store.js";
7
+ import { store as fileBrowserStore } from "/components/modals/file-browser-store.js";
8
</script>
9
</head>
10
<body>
@@ -29,7 +32,7 @@
32
<p>Import knowledge</p>
33
</button>
34
32
- <button class="text-button" id="work_dir_browser" @click="fileBrowserModalProxy.openModal()">
35
+ <button class="text-button" id="work_dir_browser" @click="$store.fileBrowser.open()">
36
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 123.37 92.59">
37
<path d="m5.72,11.5l-3.93,8.73h119.77s-3.96-8.73-3.96-8.73h-60.03c-1.59,0-2.88-1.29-2.88-2.88V1.75H13.72v6.87c0,1.59-1.29,2.88-2.88,2.88h-5.12Z" fill="none" stroke="currentColor" stroke-linejoin="round" stroke-width="7"></path>
38
<path d="m6.38,20.23H1.75l7.03,67.03c.11,1.07.55,2.02,1.2,2.69.55.55,1.28.89,2.11.89h97.1c.82,0,1.51-.33,2.05-.87.68-.68,1.13-1.67,1.28-2.79l9.1-66.94H6.38Z" fill="none" stroke="currentColor" stroke-linejoin="round" stroke-width="8"></path>
webui/components/modals/file-browser-store.js
new
+244
@@ -0,0 +1,244 @@
1
+import { createStore } from "/js/AlpineStore.js";
2
+import { fetchApi } from "/js/api.js";
3
+
4
+// Model migrated from legacy file_browser.js (lift-and-shift)
5
+const model = {
6
+ // Reactive state
7
+ isLoading: false,
8
+ browser: {
9
+ title: "File Browser",
10
+ currentPath: "",
11
+ entries: [],
12
+ parentPath: "",
13
+ sortBy: "name",
14
+ sortDirection: "asc",
15
+ },
16
+ history: [], // navigation stack
17
+ initialPath: "", // Store path for open() call
18
+ closePromise: null,
19
+ error: null,
20
+
21
+ // --- Lifecycle -----------------------------------------------------------
22
+ init() {
23
+ // Nothing special to do here; all methods available immediately
24
+ },
25
+
26
+ // --- Public API (called from button/link) --------------------------------
27
+ async open() {
28
+ if (this.isLoading) return; // Prevent double-open
29
+
30
+ this.isLoading = true;
31
+ this.error = null;
32
+ this.history = [];
33
+
34
+ try {
35
+ // Open modal FIRST (immediate UI feedback)
36
+ this.closePromise = window.openModal('modals/file-browser.html');
37
+
38
+ // Setup cleanup on modal close
39
+ if (this.closePromise && typeof this.closePromise.then === 'function') {
40
+ this.closePromise.then(() => {
41
+ this.destroy();
42
+ });
43
+ }
44
+
45
+ // Use stored initial path or default
46
+ const path = this.initialPath || this.browser.currentPath || "$WORK_DIR";
47
+ this.browser.currentPath = path;
48
+
49
+ // Fetch files
50
+ await this.fetchFiles(this.browser.currentPath);
51
+
52
+ } catch (error) {
53
+ console.error("File browser error:", error);
54
+ this.error = error?.message || "Failed to load files";
55
+ this.isLoading = false;
56
+ }
57
+ },
58
+
59
+ handleClose() {
60
+ // Close the modal manually
61
+ window.closeModal();
62
+ },
63
+
64
+ destroy() {
65
+ // Reset state when modal closes
66
+ this.isLoading = false;
67
+ this.history = [];
68
+ this.initialPath = "";
69
+ this.browser.entries = [];
70
+ },
71
+
72
+ // --- Helpers -------------------------------------------------------------
73
+ isArchive(filename) {
74
+ const archiveExts = ["zip", "tar", "gz", "rar", "7z"];
75
+ const ext = filename.split(".").pop().toLowerCase();
76
+ return archiveExts.includes(ext);
77
+ },
78
+
79
+ formatFileSize(size) {
80
+ if (size === 0) return "0 Bytes";
81
+ const k = 1024;
82
+ const sizes = ["Bytes", "KB", "MB", "GB", "TB"];
83
+ const i = Math.floor(Math.log(size) / Math.log(k));
84
+ return parseFloat((size / Math.pow(k, i)).toFixed(2)) + " " + sizes[i];
85
+ },
86
+
87
+ formatDate(dateString) {
88
+ const options = {
89
+ year: "numeric",
90
+ month: "short",
91
+ day: "numeric",
92
+ hour: "2-digit",
93
+ minute: "2-digit",
94
+ };
95
+ return new Date(dateString).toLocaleDateString(undefined, options);
96
+ },
97
+
98
+ // --- Sorting -------------------------------------------------------------
99
+ toggleSort(column) {
100
+ if (this.browser.sortBy === column) {
101
+ this.browser.sortDirection =
102
+ this.browser.sortDirection === "asc" ? "desc" : "asc";
103
+ } else {
104
+ this.browser.sortBy = column;
105
+ this.browser.sortDirection = "asc";
106
+ }
107
+ },
108
+
109
+ sortFiles(entries) {
110
+ return [...entries].sort((a, b) => {
111
+ // Folders first
112
+ if (a.is_dir !== b.is_dir) return a.is_dir ? -1 : 1;
113
+ const dir = this.browser.sortDirection === "asc" ? 1 : -1;
114
+ switch (this.browser.sortBy) {
115
+ case "name":
116
+ return dir * a.name.localeCompare(b.name);
117
+ case "size":
118
+ return dir * (a.size - b.size);
119
+ case "date":
120
+ return dir * (new Date(a.modified) - new Date(b.modified));
121
+ default:
122
+ return 0;
123
+ }
124
+ });
125
+ },
126
+
127
+ // --- Navigation ----------------------------------------------------------
128
+ async fetchFiles(path = "") {
129
+ this.isLoading = true;
130
+ try {
131
+ const response = await fetchApi(`/get_work_dir_files?path=${encodeURIComponent(path)}`);
132
+ if (response.ok) {
133
+ const data = await response.json();
134
+ this.browser.entries = data.data.entries;
135
+ this.browser.currentPath = data.data.current_path;
136
+ this.browser.parentPath = data.data.parent_path;
137
+ } else {
138
+ console.error("Error fetching files:", await response.text());
139
+ this.browser.entries = [];
140
+ }
141
+ } catch (e) {
142
+ window.toastFrontendError("Error fetching files: " + e.message, "File Browser Error");
143
+ this.browser.entries = [];
144
+ } finally {
145
+ this.isLoading = false;
146
+ }
147
+ },
148
+
149
+ async navigateToFolder(path) {
150
+ if (this.browser.currentPath !== path) this.history.push(this.browser.currentPath);
151
+ await this.fetchFiles(path);
152
+ },
153
+
154
+ async navigateUp() {
155
+ if (this.browser.parentPath) {
156
+ this.history.push(this.browser.currentPath);
157
+ await this.fetchFiles(this.browser.parentPath);
158
+ }
159
+ },
160
+
161
+ // --- File actions --------------------------------------------------------
162
+ async deleteFile(file) {
163
+ if (!confirm(`Are you sure you want to delete ${file.name}?`)) return;
164
+ try {
165
+ const resp = await fetchApi("/delete_work_dir_file", {
166
+ method: "POST",
167
+ headers: { "Content-Type": "application/json" },
168
+ body: JSON.stringify({ path: file.path, currentPath: this.browser.currentPath }),
169
+ });
170
+ if (resp.ok) {
171
+ this.browser.entries = this.browser.entries.filter((e) => e.path !== file.path);
172
+ alert("File deleted successfully.");
173
+ } else {
174
+ alert(`Error deleting file: ${await resp.text()}`);
175
+ }
176
+ } catch (e) {
177
+ window.toastFrontendError("Error deleting file: " + e.message, "File Delete Error");
178
+ }
179
+ },
180
+
181
+ async handleFileUpload(event) {
182
+ try {
183
+ const files = event.target.files;
184
+ if (!files.length) return;
185
+ const formData = new FormData();
186
+ formData.append("path", this.browser.currentPath);
187
+ for (let f of files) {
188
+ const ext = f.name.split(".").pop().toLowerCase();
189
+ if (!["zip", "tar", "gz", "rar", "7z"].includes(ext) && f.size > 100 * 1024 * 1024) {
190
+ alert(`File ${f.name} exceeds 100MB limit.`);
191
+ continue;
192
+ }
193
+ formData.append("files[]", f);
194
+ }
195
+ const resp = await fetchApi("/upload_work_dir_files", { method: "POST", body: formData });
196
+ if (resp.ok) {
197
+ const data = await resp.json();
198
+ this.browser.entries = data.data.entries;
199
+ this.browser.currentPath = data.data.current_path;
200
+ this.browser.parentPath = data.data.parent_path;
201
+ if (data.failed && data.failed.length) {
202
+ const msg = data.failed.map((f) => `${f.name}: ${f.error}`).join("\n");
203
+ alert(`Some files failed to upload:\n${msg}`);
204
+ }
205
+ } else {
206
+ alert(await resp.text());
207
+ }
208
+ } catch (e) {
209
+ window.toastFrontendError("Error uploading files: " + e.message, "File Upload Error");
210
+ } finally {
211
+ event.target.value = ""; // reset input so same file can be reselected
212
+ }
213
+ },
214
+
215
+ downloadFile(file) {
216
+ const link = document.createElement("a");
217
+ link.href = `/download_work_dir_file?path=${encodeURIComponent(file.path)}`;
218
+ link.download = file.name;
219
+ document.body.appendChild(link);
220
+ link.click();
221
+ document.body.removeChild(link);
222
+ },
223
+};
224
+
225
+export const store = createStore("fileBrowser", model);
226
+
227
+window.openFileLink = async function (path) {
228
+ try {
229
+ const resp = await window.sendJsonData("/file_info", { path });
230
+ if (!resp.exists) {
231
+ window.toastFrontendError("File does not exist.", "File Error");
232
+ return;
233
+ }
234
+ if (resp.is_dir) {
235
+ // Set initial path and open via store
236
+ store.initialPath = resp.abs_path;
237
+ await store.open();
238
+ } else {
239
+ store.downloadFile({ path: resp.abs_path, name: resp.file_name });
240
+ }
241
+ } catch (e) {
242
+ window.toastFrontendError("Error opening file: " + e.message, "File Open Error");
243
+ }
244
+};
webui/components/modals/file-browser.html
new
+347
@@ -0,0 +1,347 @@
1
+<html>
2
+<head>
3
+ <title>File Browser</title>
4
+ <script type="module">
5
+ import { store } from "/components/modals/file-browser-store.js";
6
+ </script>
7
+</head>
8
+<body>
9
+ <div x-data>
10
+ <template x-if="$store.fileBrowser">
11
+ <div class="file-browser-root">
12
+
13
+ <!-- Loading State -->
14
+ <div x-show="$store.fileBrowser.isLoading" class="loading-state">
15
+ <div class="loading-spinner"></div>
16
+ <p>Loading files...</p>
17
+ </div>
18
+
19
+ <!-- File Browser Content -->
20
+ <div x-show="!$store.fileBrowser.isLoading" class="file-browser-content">
21
+ <!-- Path navigator -->
22
+ <div class="path-navigator">
23
+ <button class="text-button back-button" @click="$store.fileBrowser.navigateUp()" aria-label="Navigate Up">
24
+ <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 10.5 15">
25
+ <path d="m.75,5.25L5.25.75m0,0l4.5,4.5M5.25.75v13.5" fill="none" stroke="currentColor" stroke-linecap="round" stroke-linejoin="round" stroke-width="1.5" />
26
+ </svg>
27
+ Up
28
+ </button>
29
+ <div id="current-path"><span id="path-text" x-text="$store.fileBrowser.browser.currentPath"></span></div>
30
+ </div>
31
+
32
+ <!-- Files list -->
33
+ <div class="files-list">
34
+ <div class="file-header">
35
+ <div class="file-cell" @click="$store.fileBrowser.toggleSort('name')">Name <span x-show="$store.fileBrowser.browser.sortBy === 'name'" x-text="$store.fileBrowser.browser.sortDirection === 'asc' ? '↑' : '↓'"></span></div>
36
+ <div class="file-cell-size" @click="$store.fileBrowser.toggleSort('size')">Size <span x-show="$store.fileBrowser.browser.sortBy === 'size'" x-text="$store.fileBrowser.browser.sortDirection === 'asc' ? '↑' : '↓'"></span></div>
37
+ <div class="file-cell-date" @click="$store.fileBrowser.toggleSort('date')">Modified <span x-show="$store.fileBrowser.browser.sortBy === 'date'" x-text="$store.fileBrowser.browser.sortDirection === 'asc' ? '↑' : '↓'"></span></div>
38
+ </div>
39
+
40
+ <!-- File list entries -->
41
+ <template x-if="$store.fileBrowser.browser.entries.length">
42
+ <template x-for="file in $store.fileBrowser.sortFiles($store.fileBrowser.browser.entries)" :key="file.path">
43
+ <div class="file-item" :data-is-dir="file.is_dir">
44
+ <div class="file-name" @click="file.is_dir ? $store.fileBrowser.navigateToFolder(file.path) : $store.fileBrowser.downloadFile(file)">
45
+ <img :src="'/public/' + (file.type === 'unknown' ? 'file' : ($store.fileBrowser.isArchive(file.name) ? 'archive' : file.type)) + '.svg'" class="file-icon" :alt="file.type" />
46
+ <span x-text="file.name"></span>
47
+ </div>
48
+ <div class="file-size" x-text="$store.fileBrowser.formatFileSize(file.size)"></div>
49
+ <div class="file-date" x-text="$store.fileBrowser.formatDate(file.modified)"></div>
50
+ <div class="file-actions">
51
+ <button class="action-button download-button" @click.stop="$store.fileBrowser.downloadFile(file)">
52
+ <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 19.5 19.5"><path d="m.75,14.25v2.25c0,1.24,1.01,2.25,2.25,2.25h13.5c1.24,0,2.25-1.01,2.25-2.25v-2.25m-4.5-4.5l-4.5,4.5m0,0l-4.5-4.5m4.5,4.5V.75" fill="none" stroke="currentColor" stroke-linecap="round" stroke-linejoin="round" stroke-width="1.5"/></svg>
53
+ </button>
54
+ <button class="delete-button" @click.stop="$store.fileBrowser.deleteFile(file)">
55
+ <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 15.03 22.53" fill="currentColor"><path d="m14.55,7.82H4.68L14.09,3.19c.83-.41,1.17-1.42.77-2.25-.41-.83-1.42-1.17-2.25-.77l-3.16,1.55-.15-.31c-.22-.44-.59-.76-1.05-.92-.46-.16-.96-.13-1.39.09l-2.08,1.02c-.9.44-1.28,1.54-.83,2.44l.15.31-3.16,1.55c-.83.41-1.17,1.42-.77,2.25.29.59.89.94,1.51.94.25,0,.5-.06.74-.17l.38-.19s.09.03.14.03h11.14v11.43c0,.76-.62,1.38-1.38,1.38h-.46v-11.28c0-.26-.21-.47-.47-.47s-.47.21-.47.47v11.28h-2.39v-11.28c0-.26-.21-.47-.47-.47s-.47.21-.47.47v11.28h-2.39v-11.28c0-.26-.21-.47-.47-.47s-.47.21-.47.47v11.28h-.46c-.76,0-1.38-.62-1.38-1.38v-9.9c0-.26-.21-.47-.47-.47s-.47.21-.47.47v9.9c0,1.28,1.04,2.32,2.32,2.32h8.55c1.28,0,2.32-1.04,2.32-2.32v-11.91c0-.26-.21-.47-.47-.47Z" stroke-width="0"/></svg>
56
+ </button>
57
+ </div>
58
+ </div>
59
+ </template>
60
+ </template>
61
+
62
+ <!-- Empty state -->
63
+ <template x-if="!$store.fileBrowser.browser.entries.length">
64
+ <div class="no-files">No files found</div>
65
+ </template>
66
+ </div>
67
+ </div>
68
+ </div>
69
+
70
+ </div>
71
+ </template>
72
+
73
+ <!-- Modal Footer (outside template x-if so it exists immediately) -->
74
+ <template x-if="$store.fileBrowser">
75
+ <div class="modal-footer" data-modal-footer>
76
+ <label class="btn btn-upload">
77
+ <svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor"><path stroke-linecap="round" stroke-linejoin="round" d="M3 16.5v2.25A2.25 2.25 0 0 0 5.25 21h13.5A2.25 2.25 0 0 0 21 18.75V16.5m-13.5-9L12 3m0 0 4.5 4.5M12 3v13.5"/></svg>
78
+ Upload Files
79
+ <input type="file" multiple accept="*" @change="$store.fileBrowser.handleFileUpload" style="display:none;" />
80
+ </label>
81
+ <button class="btn btn-cancel" @click="$store.fileBrowser.handleClose()">Close Browser</button>
82
+ </div>
83
+ </template>
84
+ </div>
85
+
86
+ <style>
87
+ /* File Browser Root */
88
+ .file-browser-root {
89
+ display: flex;
90
+ flex-direction: column;
91
+ width: 100%;
92
+ height: 100%;
93
+ min-height: 400px;
94
+ }
95
+
96
+ /* Loading State */
97
+ .loading-state {
98
+ display: flex;
99
+ flex-direction: column;
100
+ align-items: center;
101
+ justify-content: center;
102
+ padding: var(--spacing-lg);
103
+ min-height: 200px;
104
+ }
105
+
106
+ .loading-spinner {
107
+ width: 40px;
108
+ height: 40px;
109
+ border: 3px solid var(--color-border);
110
+ border-top: 3px solid var(--color-primary);
111
+ border-radius: 50%;
112
+ animation: spin 1s linear infinite;
113
+ margin-bottom: var(--spacing-md);
114
+ }
115
+
116
+ @keyframes spin {
117
+ 0% { transform: rotate(0deg); }
118
+ 100% { transform: rotate(360deg); }
119
+ }
120
+
121
+ .loading-state p {
122
+ color: var(--color-text-secondary);
123
+ margin: 0;
124
+ }
125
+
126
+ /* File Browser Content */
127
+ .file-browser-content {
128
+ display: flex;
129
+ flex-direction: column;
130
+ padding: var(--spacing-sm) var(--spacing-sm);
131
+ }
132
+
133
+ /* File Browser Styles */
134
+ .files-list,
135
+ .file-header,
136
+ .file-item {
137
+ width: 100%;
138
+ border-radius: 4px;
139
+ overflow: hidden;
140
+ }
141
+
142
+ /* Header Styles */
143
+ .file-header {
144
+ display: grid;
145
+ grid-template-columns: 2fr 0.6fr 1fr 80px;
146
+ background: var(--secondary-bg);
147
+ padding: 8px 0;
148
+ font-weight: bold;
149
+ border-bottom: 1px solid var(--border-color);
150
+ color: var(--color-primary);
151
+ }
152
+ .file-cell,
153
+ .file-cell-size,
154
+ .file-cell-date {
155
+ color: var(--color-primary);
156
+ padding: 4px;
157
+ cursor: pointer;
158
+ }
159
+
160
+ /* File Item Styles */
161
+ .file-item {
162
+ display: grid;
163
+ grid-template-columns: 2fr 0.6fr 1fr 80px;
164
+ align-items: center;
165
+ padding: 8px 0;
166
+ font-size: 0.875rem;
167
+ border-top: 1px solid var(--color-border);
168
+ transition: background-color 0.2s;
169
+ white-space: nowrap;
170
+ overflow: hidden;
171
+ color: var(--color-text);
172
+ }
173
+ .file-item:hover {
174
+ background-color: var(--color-secondary);
175
+ }
176
+
177
+ /* File Icon and Name */
178
+ .file-icon {
179
+ width: 1.8rem;
180
+ height: 1.8rem;
181
+ margin: 0 1rem 0 0.7rem;
182
+ vertical-align: middle;
183
+ font-size: var(--font-size-sm);
184
+ }
185
+ .file-name {
186
+ display: flex;
187
+ align-items: center;
188
+ font-weight: 500;
189
+ margin-right: var(--spacing-sm);
190
+ overflow: hidden;
191
+ }
192
+ .file-name > span {
193
+ white-space: nowrap;
194
+ overflow: hidden;
195
+ text-overflow: ellipsis;
196
+ }
197
+ .file-size,
198
+ .file-date {
199
+ color: var(--text-secondary);
200
+ }
201
+ /* No Files Message */
202
+ .no-files {
203
+ padding: 32px;
204
+ text-align: center;
205
+ color: var(--text-secondary);
206
+ }
207
+ /* Light Mode Adjustments */
208
+ .light-mode .file-item:hover {
209
+ background-color: var(--color-secondary-light);
210
+ }
211
+
212
+ /* Path Navigator Styles */
213
+ .path-navigator {
214
+ display: flex;
215
+ align-items: center;
216
+ gap: 24px;
217
+ background-color: var(--color-message-bg);
218
+ padding: 0.5rem var(--spacing-sm);
219
+ margin: 0 0 var(--spacing-sm) 0;
220
+ border: 1px solid var(--color-border);
221
+ border-radius: 8px;
222
+ }
223
+ .nav-button {
224
+ padding: 4px 12px;
225
+ border: 1px solid var(--color-border);
226
+ border-radius: 4px;
227
+ background: var(--color-background);
228
+ color: var(--color-text);
229
+ cursor: pointer;
230
+ transition: background-color 0.2s;
231
+ }
232
+ .nav-button:hover {
233
+ background: var(--hover-bg);
234
+ }
235
+ .nav-button.back-button {
236
+ background-color: var(--color-secondary);
237
+ color: var(--color-text);
238
+ }
239
+ .nav-button.back-button:hover {
240
+ background-color: var(--color-secondary-dark);
241
+ }
242
+ #current-path {
243
+ opacity: 0.9;
244
+ }
245
+ #path-text {
246
+ font-family: 'Roboto Mono', monospace;
247
+ -webkit-font-optical-sizing: auto;
248
+ font-optical-sizing: auto;
249
+ opacity: 0.9;
250
+ }
251
+
252
+ /* Folder Specific Styles */
253
+ .file-item[data-is-dir="true"] {
254
+ cursor: pointer;
255
+ }
256
+ .file-item[data-is-dir="true"]:hover {
257
+ background-color: var(--color-secondary);
258
+ }
259
+
260
+ /* Upload Button Styles */
261
+ .btn-upload {
262
+ display: inline-flex;
263
+ align-items: center;
264
+ padding: 8px 16px;
265
+ background: #4248f1;
266
+ gap: 0.5rem;
267
+ color: white;
268
+ border-radius: 4px;
269
+ cursor: pointer;
270
+ transition: background-color 0.3s ease-in-out;
271
+ }
272
+ .btn-upload > svg {
273
+ width: 20px;
274
+ }
275
+ .btn-upload:hover {
276
+ background-color: #353bc5;
277
+ }
278
+ .btn-upload:active {
279
+ background-color: #2b309c;
280
+ }
281
+ /* Delete Button Styles */
282
+ .delete-button {
283
+ background: none;
284
+ border: none;
285
+ color: var(--color-primary);
286
+ cursor: pointer;
287
+ width: 32px;
288
+ padding: 4px 8px;
289
+ border-radius: 4px;
290
+ transition: opacity 0.2s, background-color 0.2s;
291
+ }
292
+ .delete-button:hover {
293
+ color: #ff7878;
294
+ }
295
+ .delete-button:active {
296
+ opacity: 0.6;
297
+ }
298
+
299
+ /* File Actions */
300
+ .file-actions {
301
+ display: flex;
302
+ gap: var(--spacing-xs);
303
+ }
304
+ .action-button {
305
+ background: none;
306
+ border: none;
307
+ cursor: pointer;
308
+ width: 32px;
309
+ padding: 6px 8px;
310
+ border-radius: 4px;
311
+ transition: background-color 0.2s;
312
+ }
313
+ .download-button {
314
+ color: var(--color-primary);
315
+ }
316
+ .download-button:hover {
317
+ background-color: var(--color-border);
318
+ }
319
+ .light-mode .download-button:hover {
320
+ background-color: #c6d4de;
321
+ }
322
+ /* Responsive Design */
323
+ @media (max-width: 768px) {
324
+ .file-header,
325
+ .file-item {
326
+ grid-template-columns: 1fr 0.5fr 80px;
327
+ }
328
+ .file-cell-date,
329
+ .file-date {
330
+ display: none;
331
+ }
332
+ }
333
+ @media (max-width: 540px) {
334
+ .file-header,
335
+ .file-item {
336
+ grid-template-columns: 1fr 80px;
337
+ }
338
+ .file-cell-size,
339
+ .file-size,
340
+ .file-cell-date,
341
+ .file-date {
342
+ display: none;
343
+ }
344
+ }
345
+ </style>
346
+</body>
347
+</html>
webui/css/file_browser.css
deleted
-249
@@ -1,249 +0,0 @@
1
-/* File Browser Styles */
2
-
3
-.files-list,
4
-.file-header,
5
-.file-item {
6
- width: 100%;
7
- border-radius: 4px;
8
- overflow: hidden;
9
-}
10
-
11
-/* Header Styles */
12
-.file-header {
13
- display: grid;
14
- grid-template-columns: 2fr 0.6fr 1fr 80px;
15
- background: var(--secondary-bg);
16
- padding: 8px 0;
17
- font-weight: bold;
18
- border-bottom: 1px solid var(--border-color);
19
- color: var(--color-primary);
20
-}
21
-
22
-.file-cell,
23
-.file-cell-size,
24
-.file-cell-date {
25
- color: var(--color-primary);
26
- padding: 4px;
27
- cursor: pointer;
28
-}
29
-
30
-/* File Item Styles */
31
-.file-item {
32
- display: grid;
33
- grid-template-columns: 2fr 0.6fr 1fr 80px;
34
- align-items: center;
35
- padding: 8px 0;
36
- font-size: 0.875rem;
37
- border-top: 1px solid var(--color-border);
38
- transition: background-color 0.2s;
39
- white-space: nowrap;
40
- overflow: hidden;
41
- color: var(--color-text);
42
-}
43
-
44
-.file-item:hover {
45
- background-color: var(--color-secondary);
46
-}
47
-
48
-/* File Icon and Name */
49
-.file-icon {
50
- width: 1.8rem;
51
- height: 1.8rem;
52
- margin: 0 1rem 0 0.7rem;
53
- vertical-align: middle;
54
- font-size: var(--font-size-sm);
55
-}
56
-
57
-.file-name {
58
- display: flex;
59
- align-items: center;
60
- font-weight: 500;
61
- margin-right: var(--spacing-sm);
62
- overflow: hidden;
63
-}
64
-
65
-.file-name > span {
66
- white-space: nowrap;
67
- overflow: hidden;
68
- text-overflow: ellipsis;
69
-}
70
-
71
-.file-size,
72
-.file-date {
73
- color: var(--text-secondary);
74
-}
75
-
76
-/* No Files Message */
77
-.no-files {
78
- padding: 32px;
79
- text-align: center;
80
- color: var(--text-secondary);
81
-}
82
-
83
-/* Light Mode Adjustments */
84
-.light-mode .file-item:hover {
85
- background-color: var(--color-secondary-light);
86
-}
87
-
88
-/* Path Navigator Styles */
89
-.path-navigator {
90
- display: flex;
91
- align-items: center;
92
- gap: 24px;
93
- background-color: var(--color-message-bg);
94
- padding: 0.5rem var(--spacing-sm);
95
- margin-bottom: 0.3rem;
96
- border: 1px solid var(--color-border);
97
- border-radius: 8px;
98
-}
99
-
100
-.nav-button {
101
- padding: 4px 12px;
102
- border: 1px solid var(--color-border);
103
- border-radius: 4px;
104
- background: var(--color-background);
105
- color: var(--color-text);
106
- cursor: pointer;
107
- transition: background-color 0.2s;
108
-}
109
-
110
-.nav-button:hover {
111
- background: var(--hover-bg);
112
-}
113
-
114
-.nav-button.back-button {
115
- background-color: var(--color-secondary);
116
- color: var(--color-text);
117
-}
118
-
119
-.nav-button.back-button:hover {
120
- background-color: var(--color-secondary-dark);
121
-}
122
-
123
-#current-path {
124
- opacity: 0.9;
125
-}
126
-
127
-#path-text {
128
- font-family: 'Roboto Mono', monospace;
129
- font-optical-sizing: auto;
130
- -webkit-font-optical-sizing: auto;
131
- opacity: 0.9;
132
-}
133
-
134
-/* Folder Specific Styles */
135
-.file-item[data-is-dir="true"] {
136
- cursor: pointer;
137
-}
138
-
139
-.file-item[data-is-dir="true"]:hover {
140
- background-color: var(--color-secondary);
141
-}
142
-
143
-/* Upload Button Styles */
144
-.upload-button,
145
-.btn-upload {
146
- display: inline-flex;
147
- align-items: center;
148
- padding: 8px 16px;
149
- background-color: var(--color-primary);
150
- color: white;
151
- border-radius: 4px;
152
- cursor: pointer;
153
- transition: background-color 0.3s ease-in-out;
154
-}
155
-
156
-.btn-upload {
157
- background: #4248f1;
158
- gap: 0.5rem;
159
- margin: 0 auto;
160
-}
161
-
162
-.btn-upload > svg {
163
- width: 20px;
164
-}
165
-
166
-.upload-button:hover,
167
-.btn-upload:hover {
168
- background-color: #353bc5;
169
-}
170
-
171
-.upload-button:active,
172
-.btn-upload:active {
173
- background-color: #2b309c;
174
-}
175
-
176
-/* Delete Button Styles */
177
-.delete-button {
178
- background: none;
179
- border: none;
180
- color: var(--color-primary);
181
- cursor: pointer;
182
- width: 32px;
183
- padding: 4px 8px;
184
- border-radius: 4px;
185
- transition: opacity 0.2s, background-color 0.2s;
186
-}
187
-
188
-.delete-button:hover {
189
- color: #ff7878;
190
-}
191
-
192
-.delete-button:active {
193
- opacity: 0.6;
194
-}
195
-
196
-/* File Actions */
197
-.file-actions {
198
- display: flex;
199
- gap: var(--spacing-xs);
200
-}
201
-
202
-.action-button {
203
- background: none;
204
- border: none;
205
- cursor: pointer;
206
- width: 32px;
207
- padding: 6px 8px;
208
- border-radius: 4px;
209
- transition: background-color 0.2s;
210
-}
211
-
212
-.download-button {
213
- color: var(--color-primary);
214
-}
215
-
216
-.download-button:hover {
217
- background-color: var(--color-border);
218
-}
219
-
220
-.light-mode .download-button:hover {
221
- background-color: #c6d4de;
222
-}
223
-
224
-/* Responsive Design */
225
-@media (max-width: 768px) {
226
- .file-header,
227
- .file-item {
228
- grid-template-columns: 1fr 0.5fr 80px;
229
- }
230
-
231
- .file-cell-date,
232
- .file-date {
233
- display: none;
234
- }
235
-}
236
-
237
-@media (max-width: 540px) {
238
- .file-header,
239
- .file-item {
240
- grid-template-columns: 1fr 80px;
241
- }
242
-
243
- .file-cell-size,
244
- .file-size,
245
- .file-cell-date,
246
- .file-date {
247
- display: none;
248
- }
249
-}
webui/css/modals2.css
+18
@@ -66,6 +66,24 @@ display: flex;
66
padding: 0 1rem 1rem 1rem;
67
}
68
69
+/* Modal with footer support */
70
+.modal-inner.modal-with-footer {
71
+ display: flex;
72
+ flex-direction: column;
73
+}
74
+
75
+.modal-inner.modal-with-footer .modal-scroll {
76
+ flex: 1;
77
+ min-height: 0;
78
+ overflow-y: auto;
79
+}
80
+
81
+.modal-footer-slot {
82
+ flex-shrink: 0;
83
+ border-top: 1px solid var(--color-border);
84
+ background: var(--color-background);
85
+}
86
+
87
.modal-x {
88
position: absolute;
89
top: 1rem;
webui/index.html
-145
@@ -11,7 +11,6 @@
11
<link rel="stylesheet" href="components/messages/action-buttons/simple-action-buttons.css">
12
<link rel="stylesheet" href="css/toast.css">
13
<link rel="stylesheet" href="css/settings.css">
14
- <link rel="stylesheet" href="css/file_browser.css">
14
<link rel="stylesheet" href="css/modals.css">
15
<link rel="stylesheet" href="css/modals2.css">
16
<link rel="stylesheet" href="css/speech.css">
@@ -1185,150 +1184,6 @@
1184
</template>
1185
</div>
1186
1188
- <!-- work_dir Browser Modal -->
1189
-
1190
- <div id="fileBrowserModal" x-data="fileBrowserModalProxy">
1191
- <template x-teleport="body">
1192
- <div x-show="isOpen" class="modal-overlay" @click.self="handleClose()"
1193
- @keydown.escape.window="handleClose()" x-transition>
1194
- <div class="modal-container">
1195
- <div class="modal-header">
1196
- <h2 class="modal-title" x-text="browser.title"></h2>
1197
- <button class="modal-close" @click="handleClose()">×</button>
1198
- </div>
1199
- <div class="modal-content">
1200
- <div x-show="isLoading" class="loading-spinner">
1201
- Loading...
1202
- </div>
1203
- <div x-show="!isLoading">
1204
- <div class="path-navigator">
1205
- <!-- Up Button -->
1206
- <button class="text-button back-button" @click="navigateUp()" aria-label="Navigate Up">
1207
- <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 10.5 15">
1208
- <path d="m.75,5.25L5.25.75m0,0l4.5,4.5M5.25.75v13.5" fill="none"
1209
- stroke="currentColor" stroke-linecap="round" stroke-linejoin="round"
1210
- stroke-width="1.5"></path>
1211
- </svg>
1212
- Up
1213
- </button>
1214
-
1215
- <div id="current-path">
1216
- <span id="path-text" x-text="browser.currentPath"></span>
1217
- </div>
1218
- </div>
1219
-
1220
- <div class="files-list">
1221
- <!-- Header -->
1222
- <div class="file-header">
1223
- <div class="file-cell" @click="toggleSort('name')">
1224
- Name
1225
- <span x-show="browser.sortBy === 'name'"
1226
- x-text="browser.sortDirection === 'asc' ? '↑' : '↓'">
1227
- </span>
1228
- </div>
1229
- <div class="file-cell-size" @click="toggleSort('size')">
1230
- Size
1231
- <span x-show="browser.sortBy === 'size'"
1232
- x-text="browser.sortDirection === 'asc' ? '↑' : '↓'">
1233
- </span>
1234
- </div>
1235
- <div class="file-cell-date" @click="toggleSort('date')">
1236
- Modified
1237
- <span x-show="browser.sortBy === 'date'"
1238
- x-text="browser.sortDirection === 'asc' ? '↑' : '↓'">
1239
- </span>
1240
- </div>
1241
- </div>
1242
-
1243
- <!-- File List -->
1244
- <template x-if="browser.entries.length">
1245
- <template x-for="file in sortFiles(browser.entries)" :key="file.path">
1246
- <div class="file-item" :data-is-dir="file.is_dir">
1247
- <div class="file-name"
1248
- @click="file.is_dir ? navigateToFolder(file.path) : downloadFile(file)">
1249
- <img :src="'/public/' + (file.type === 'unknown' ? 'file' : (isArchive(file.name) ? 'archive' : file.type)) + '.svg'"
1250
- class="file-icon" :alt="file.type">
1251
- <span x-text="file.name"></span>
1252
- </div>
1253
- <div class="file-size" x-text="formatFileSize(file.size)"></div>
1254
- <div class="file-date" x-text="formatDate(file.modified)"></div>
1255
-
1256
- <div class="file-actions">
1257
- <button class="action-button download-button"
1258
- @click.stop="downloadFile(file)">
1259
- <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 19.5 19.5">
1260
- <path
1261
- d="m.75,14.25v2.25c0,1.24,1.01,2.25,2.25,2.25h13.5c1.24,0,2.25-1.01,2.25-2.25v-2.25m-4.5-4.5l-4.5,4.5m0,0l-4.5-4.5m4.5,4.5V.75"
1262
- fill="none" stroke="currentColor" stroke-linecap="round"
1263
- stroke-linejoin="round" stroke-width="1.5"></path>
1264
- </svg>
1265
- </button>
1266
- <button class="delete-button" @click.stop="deleteFile(file)">
1267
- <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 15.03 22.53"
1268
- fill="currentColor">
1269
- <path
1270
- d="m14.55,7.82H4.68L14.09,3.19c.83-.41,1.17-1.42.77-2.25-.41-.83-1.42-1.17-2.25-.77l-3.16,1.55-.15-.31c-.22-.44-.59-.76-1.05-.92-.46-.16-.96-.13-1.39.09l-2.08,1.02c-.9.44-1.28,1.54-.83,2.44l.15.31-3.16,1.55c-.83.41-1.17,1.42-.77,2.25.29.59.89.94,1.51.94.25,0,.5-.06.74-.17l.38-.19s.09.03.14.03h11.14v11.43c0,.76-.62,1.38-1.38,1.38h-.46v-11.28c0-.26-.21-.47-.47-.47s-.47.21-.47.47v11.28h-2.39v-11.28c0-.26-.21-.47-.47-.47s-.47.21-.47.47v11.28h-2.39v-11.28c0-.26-.21-.47-.47-.47s-.47.21-.47.47v11.28h-.46c-.76,0-1.38-.62-1.38-1.38v-9.9c0-.26-.21-.47-.47-.47s-.47.21-.47.47v9.9c0,1.28,1.04,2.32,2.32,2.32h8.55c1.28,0,2.32-1.04,2.32-2.32v-11.91c0-.26-.21-.47-.47-.47ZM5.19,2.46l2.08-1.02c.12-.06.25-.09.39-.09.09,0,.19.02.28.05.22.08.4.23.5.44l.15.31-.19.09-3.46,1.7-.15-.31c-.21-.43-.03-.96.4-1.17Zm-3.19,5.62c-.36.18-.8.03-.98-.33-.18-.36-.03-.8.33-.98l5.8-2.85,2.72-1.34,3.16-1.55c.1-.05.21-.07.32-.07.27,0,.53.15.66.41.09.17.1.37.04.56-.06.18-.19.33-.37.42L2,8.08Z"
1271
- stroke-width="0"></path>
1272
- </svg>
1273
- </button>
1274
- </div>
1275
- </div>
1276
- </template>
1277
- </template>
1278
-
1279
- <!-- Empty State -->
1280
- <template x-if="!browser.entries.length">
1281
- <div class="no-files">
1282
- No files found
1283
- </div>
1284
- </template>
1285
- </div>
1286
- </div>
1287
- </div>
1288
- <div class="modal-footer">
1289
- <div id="buttons-container">
1290
- <label class="btn btn-upload"><svg xmlns="http://www.w3.org/2000/svg" fill="none"
1291
- viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor">
1292
- <path stroke-linecap="round" stroke-linejoin="round"
1293
- d="M3 16.5v2.25A2.25 2.25 0 0 0 5.25 21h13.5A2.25 2.25 0 0 0 21 18.75V16.5m-13.5-9L12 3m0 0 4.5 4.5M12 3v13.5">
1294
- </path>
1295
- </svg>
1296
- Upload Files
1297
- <input type="file" multiple="" accept="all" @change="handleFileUpload"
1298
- style="display: none;">
1299
- </label>
1300
- <button class="btn btn-cancel" @click="handleClose()">Close Browser</button>
1301
- </div>
1302
- </div>
1303
- </div>
1304
- </div>
1305
- </template>
1306
- </div>
1307
-
1308
- <!-- generic modal -->
1309
-
1310
- <div id="genericModal" x-data="genericModalProxy">
1311
- <template x-teleport="body">
1312
- <div x-show="isOpen" class="modal-overlay" @click.self="handleClose()"
1313
- @keydown.escape.window="handleClose()" x-transition>
1314
- <div class="modal-container">
1315
- <div class="modal-header">
1316
- <h2 class="modal-title" x-text="title"></h2>
1317
- <button class="modal-close" @click="handleClose()">×</button>
1318
- </div>
1319
- <div class="modal-description" x-text="description"></div>
1320
- <div class="modal-content" id="viewer">
1321
- <div class="html-pre" x-html="html"></div>
1322
- </div>
1323
- <!-- <div class="modal-footer">
1324
- <div id="buttons-container">
1325
- <button class="btn btn-cancel" @click="handleClose()">Close</button>
1326
- </div>
1327
- </div> -->
1328
- </div>
1329
- </template>
1330
- </div>
1331
-
1187
<!-- Full Screen Input Modal -->
1188
<x-component path="modals/full-screen-input.html"></x-component>
1189
webui/js/file_browser.js
deleted
-268
@@ -1,268 +0,0 @@
1
-const fileBrowserModalProxy = {
2
- isOpen: false,
3
- isLoading: false,
4
-
5
- browser: {
6
- title: "File Browser",
7
- currentPath: "",
8
- entries: [],
9
- parentPath: "",
10
- sortBy: "name",
11
- sortDirection: "asc",
12
- },
13
-
14
- // Initialize navigation history
15
- history: [],
16
-
17
- async openModal(path) {
18
- const modalEl = document.getElementById("fileBrowserModal");
19
- const modalAD = Alpine.$data(modalEl);
20
-
21
- modalAD.isOpen = true;
22
- modalAD.isLoading = true;
23
- modalAD.history = []; // reset history when opening modal
24
-
25
- // Initialize currentPath to root if it's empty
26
- if (path) modalAD.browser.currentPath = path;
27
- else if (!modalAD.browser.currentPath)
28
- modalAD.browser.currentPath = "$WORK_DIR";
29
-
30
- await modalAD.fetchFiles(modalAD.browser.currentPath);
31
- },
32
-
33
- isArchive(filename) {
34
- const archiveExts = ["zip", "tar", "gz", "rar", "7z"];
35
- const ext = filename.split(".").pop().toLowerCase();
36
- return archiveExts.includes(ext);
37
- },
38
-
39
- async fetchFiles(path = "") {
40
- this.isLoading = true;
41
- try {
42
- const response = await fetchApi(
43
- `/get_work_dir_files?path=${encodeURIComponent(path)}`
44
- );
45
-
46
- if (response.ok) {
47
- const data = await response.json();
48
- this.browser.entries = data.data.entries;
49
- this.browser.currentPath = data.data.current_path;
50
- this.browser.parentPath = data.data.parent_path;
51
- } else {
52
- console.error("Error fetching files:", await response.text());
53
- this.browser.entries = [];
54
- }
55
- } catch (error) {
56
- window.toastFrontendError("Error fetching files: " + error.message, "File Browser Error");
57
- this.browser.entries = [];
58
- } finally {
59
- this.isLoading = false;
60
- }
61
- },
62
-
63
- async navigateToFolder(path) {
64
- // Push current path to history before navigating
65
- if (this.browser.currentPath !== path) {
66
- this.history.push(this.browser.currentPath);
67
- }
68
- await this.fetchFiles(path);
69
- },
70
-
71
- async navigateUp() {
72
- if (this.browser.parentPath !== "") {
73
- // Push current path to history before navigating up
74
- this.history.push(this.browser.currentPath);
75
- await this.fetchFiles(this.browser.parentPath);
76
- }
77
- },
78
-
79
- sortFiles(entries) {
80
- return [...entries].sort((a, b) => {
81
- // Folders always come first
82
- if (a.is_dir !== b.is_dir) {
83
- return a.is_dir ? -1 : 1;
84
- }
85
-
86
- const direction = this.browser.sortDirection === "asc" ? 1 : -1;
87
- switch (this.browser.sortBy) {
88
- case "name":
89
- return direction * a.name.localeCompare(b.name);
90
- case "size":
91
- return direction * (a.size - b.size);
92
- case "date":
93
- return direction * (new Date(a.modified) - new Date(b.modified));
94
- default:
95
- return 0;
96
- }
97
- });
98
- },
99
-
100
- toggleSort(column) {
101
- if (this.browser.sortBy === column) {
102
- this.browser.sortDirection =
103
- this.browser.sortDirection === "asc" ? "desc" : "asc";
104
- } else {
105
- this.browser.sortBy = column;
106
- this.browser.sortDirection = "asc";
107
- }
108
- },
109
-
110
- async deleteFile(file) {
111
- if (!confirm(`Are you sure you want to delete ${file.name}?`)) {
112
- return;
113
- }
114
-
115
- try {
116
- const response = await fetchApi("/delete_work_dir_file", {
117
- method: "POST",
118
- headers: {
119
- "Content-Type": "application/json",
120
- },
121
- body: JSON.stringify({
122
- path: file.path,
123
- currentPath: this.browser.currentPath,
124
- }),
125
- });
126
-
127
- if (response.ok) {
128
- const data = await response.json();
129
- this.browser.entries = this.browser.entries.filter(
130
- (entry) => entry.path !== file.path
131
- );
132
- alert("File deleted successfully.");
133
- } else {
134
- alert(`Error deleting file: ${await response.text()}`);
135
- }
136
- } catch (error) {
137
- window.toastFrontendError("Error deleting file: " + error.message, "File Delete Error");
138
- alert("Error deleting file");
139
- }
140
- },
141
-
142
- async handleFileUpload(event) {
143
- try {
144
- const files = event.target.files;
145
- if (!files.length) return;
146
-
147
- const formData = new FormData();
148
- formData.append("path", this.browser.currentPath);
149
-
150
- for (let i = 0; i < files.length; i++) {
151
- const ext = files[i].name.split(".").pop().toLowerCase();
152
- if (!["zip", "tar", "gz", "rar", "7z"].includes(ext)) {
153
- if (files[i].size > 100 * 1024 * 1024) {
154
- // 100MB
155
- alert(
156
- `File ${files[i].name} exceeds the maximum allowed size of 100MB.`
157
- );
158
- continue;
159
- }
160
- }
161
- formData.append("files[]", files[i]);
162
- }
163
-
164
- // Proceed with upload after validation
165
- const response = await fetchApi("/upload_work_dir_files", {
166
- method: "POST",
167
- body: formData,
168
- });
169
-
170
- if (response.ok) {
171
- const data = await response.json();
172
- // Update the file list with new data
173
- this.browser.entries = data.data.entries.map((entry) => ({
174
- ...entry,
175
- uploadStatus: data.failed.includes(entry.name) ? "failed" : "success",
176
- }));
177
- this.browser.currentPath = data.data.current_path;
178
- this.browser.parentPath = data.data.parent_path;
179
-
180
- // Show success message
181
- if (data.failed && data.failed.length > 0) {
182
- const failedFiles = data.failed
183
- .map((file) => `${file.name}: ${file.error}`)
184
- .join("\n");
185
- alert(`Some files failed to upload:\n${failedFiles}`);
186
- }
187
- } else {
188
- alert(data.message);
189
- }
190
- } catch (error) {
191
- window.toastFrontendError("Error uploading files: " + error.message, "File Upload Error");
192
- alert("Error uploading files");
193
- }
194
- },
195
-
196
- downloadFile(file) {
197
- const link = document.createElement("a");
198
- link.href = `/download_work_dir_file?path=${encodeURIComponent(file.path)}`;
199
- link.download = file.name;
200
- document.body.appendChild(link);
201
- link.click();
202
- document.body.removeChild(link);
203
- },
204
-
205
- // Helper Functions
206
- formatFileSize(size) {
207
- if (size === 0) return "0 Bytes";
208
- const k = 1024;
209
- const sizes = ["Bytes", "KB", "MB", "GB", "TB"];
210
- const i = Math.floor(Math.log(size) / Math.log(k));
211
- return parseFloat((size / Math.pow(k, i)).toFixed(2)) + " " + sizes[i];
212
- },
213
-
214
- formatDate(dateString) {
215
- const options = {
216
- year: "numeric",
217
- month: "short",
218
- day: "numeric",
219
- hour: "2-digit",
220
- minute: "2-digit",
221
- };
222
- return new Date(dateString).toLocaleDateString(undefined, options);
223
- },
224
-
225
- handleClose() {
226
- this.isOpen = false;
227
- },
228
-};
229
-
230
-// Wait for Alpine to be ready
231
-document.addEventListener("alpine:init", () => {
232
- Alpine.data("fileBrowserModalProxy", () => ({
233
- init() {
234
- Object.assign(this, fileBrowserModalProxy);
235
- // Ensure immediate file fetch when modal opens
236
- this.$watch("isOpen", async (value) => {
237
- if (value) {
238
- await this.fetchFiles(this.browser.currentPath);
239
- }
240
- });
241
- },
242
- }));
243
-});
244
-
245
-// Keep the global assignment for backward compatibility
246
-window.fileBrowserModalProxy = fileBrowserModalProxy;
247
-
248
-openFileLink = async function (path) {
249
- try {
250
- const resp = await window.sendJsonData("/file_info", { path });
251
- if (!resp.exists) {
252
- window.toastFrontendError("File does not exist.", "File Error");
253
- return;
254
- }
255
-
256
- if (resp.is_dir) {
257
- fileBrowserModalProxy.openModal(resp.abs_path);
258
- } else {
259
- fileBrowserModalProxy.downloadFile({
260
- path: resp.abs_path,
261
- name: resp.file_name,
262
- });
263
- }
264
- } catch (e) {
265
- window.toastFrontendError("Error opening file: " + e.message, "File Open Error");
266
- }
267
-};
268
-window.openFileLink = openFileLink;
webui/js/modals.js
+15
@@ -74,6 +74,7 @@ function createModalElement(name) {
74
<div class="modal-scroll">
75
<div class="modal-bd"></div>
76
</div>
77
+ <div class="modal-footer-slot" style="display: none;"></div>
78
</div>
79
`;
80
@@ -96,6 +97,8 @@ function createModalElement(name) {
97
title: newModal.querySelector(".modal-title"),
98
body: newModal.querySelector(".modal-bd"),
99
close: close_button,
100
+ footerSlot: newModal.querySelector(".modal-footer-slot"),
101
+ inner: newModal.querySelector(".modal-inner"),
102
styles: [],
103
scripts: [],
104
};
@@ -135,6 +138,18 @@ export function openModal(modalPath) {
138
if (doc.body && doc.body.classList) {
139
modal.body.classList.add(...doc.body.classList);
140
}
141
+
142
+ // Some modals have a footer. Check if it exists and move it to footer slot
143
+ // Use requestAnimationFrame to let Alpine mount the component first
144
+ requestAnimationFrame(() => {
145
+ const componentFooter = modal.body.querySelector('[data-modal-footer]');
146
+ if (componentFooter && modal.footerSlot) {
147
+ // Move footer outside modal-scroll scrollable area
148
+ modal.footerSlot.appendChild(componentFooter);
149
+ modal.footerSlot.style.display = 'block';
150
+ modal.inner.classList.add('modal-with-footer');
151
+ }
152
+ });
153
})
154
.catch((error) => {
155
console.error("Error loading modal content:", error);