@gallop.software/studio 1.5.9 → 2.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/app/api/studio/[...path]/route.ts +1 -0
- package/app/layout.tsx +20 -0
- package/app/page.tsx +82 -0
- package/bin/studio.mjs +110 -0
- package/dist/handlers/index.js +84 -63
- package/dist/handlers/index.js.map +1 -1
- package/dist/handlers/index.mjs +135 -114
- package/dist/handlers/index.mjs.map +1 -1
- package/dist/index.d.mts +14 -10
- package/dist/index.d.ts +14 -10
- package/dist/index.js +2 -177
- package/dist/index.js.map +1 -1
- package/dist/index.mjs +4 -179
- package/dist/index.mjs.map +1 -1
- package/next.config.mjs +22 -0
- package/package.json +18 -10
- package/src/components/AddNewModal.tsx +402 -0
- package/src/components/ErrorModal.tsx +89 -0
- package/src/components/R2SetupModal.tsx +400 -0
- package/src/components/StudioBreadcrumb.tsx +115 -0
- package/src/components/StudioButton.tsx +200 -0
- package/src/components/StudioContext.tsx +219 -0
- package/src/components/StudioDetailView.tsx +714 -0
- package/src/components/StudioFileGrid.tsx +704 -0
- package/src/components/StudioFileList.tsx +743 -0
- package/src/components/StudioFolderPicker.tsx +342 -0
- package/src/components/StudioModal.tsx +473 -0
- package/src/components/StudioPreview.tsx +399 -0
- package/src/components/StudioSettings.tsx +536 -0
- package/src/components/StudioToolbar.tsx +1448 -0
- package/src/components/StudioUI.tsx +731 -0
- package/src/components/styles/common.ts +236 -0
- package/src/components/tokens.ts +78 -0
- package/src/components/useStudioActions.tsx +497 -0
- package/src/config/index.ts +7 -0
- package/src/config/workspace.ts +52 -0
- package/src/handlers/favicon.ts +152 -0
- package/src/handlers/files.ts +784 -0
- package/src/handlers/images.ts +949 -0
- package/src/handlers/import.ts +190 -0
- package/src/handlers/index.ts +168 -0
- package/src/handlers/list.ts +627 -0
- package/src/handlers/scan.ts +311 -0
- package/src/handlers/utils/cdn.ts +234 -0
- package/src/handlers/utils/files.ts +64 -0
- package/src/handlers/utils/index.ts +4 -0
- package/src/handlers/utils/meta.ts +102 -0
- package/src/handlers/utils/thumbnails.ts +98 -0
- package/src/hooks/useFileList.ts +143 -0
- package/src/index.tsx +36 -0
- package/src/lib/api.ts +176 -0
- package/src/types.ts +119 -0
- package/dist/StudioUI-GJK45R3T.js +0 -6500
- package/dist/StudioUI-GJK45R3T.js.map +0 -1
- package/dist/StudioUI-QZ54STXE.mjs +0 -6500
- package/dist/StudioUI-QZ54STXE.mjs.map +0 -1
- package/dist/chunk-N6JYTJCB.js +0 -68
- package/dist/chunk-N6JYTJCB.js.map +0 -1
- package/dist/chunk-RHI3UROE.mjs +0 -68
- package/dist/chunk-RHI3UROE.mjs.map +0 -1
|
@@ -0,0 +1,714 @@
|
|
|
1
|
+
/** @jsxImportSource @emotion/react */
|
|
2
|
+
'use client'
|
|
3
|
+
|
|
4
|
+
import { useState } from 'react'
|
|
5
|
+
import { css } from '@emotion/react'
|
|
6
|
+
import { useStudio } from './StudioContext'
|
|
7
|
+
import { AlertModal, InputModal, ProgressModal, type ProgressState } from './StudioModal'
|
|
8
|
+
import { R2SetupModal } from './R2SetupModal'
|
|
9
|
+
import { colors, fontSize } from './tokens'
|
|
10
|
+
|
|
11
|
+
const IMAGE_EXTENSIONS = ['.jpg', '.jpeg', '.png', '.gif', '.webp', '.svg', '.ico', '.bmp', '.tiff', '.tif']
|
|
12
|
+
const VIDEO_EXTENSIONS = ['.mp4', '.webm', '.mov', '.avi', '.mkv', '.m4v']
|
|
13
|
+
|
|
14
|
+
function isImageFile(filename: string): boolean {
|
|
15
|
+
const ext = filename.toLowerCase().substring(filename.lastIndexOf('.'))
|
|
16
|
+
return IMAGE_EXTENSIONS.includes(ext)
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
function isVideoFile(filename: string): boolean {
|
|
20
|
+
const ext = filename.toLowerCase().substring(filename.lastIndexOf('.'))
|
|
21
|
+
return VIDEO_EXTENSIONS.includes(ext)
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
const styles = {
|
|
25
|
+
overlay: css`
|
|
26
|
+
position: absolute;
|
|
27
|
+
top: 0;
|
|
28
|
+
left: 0;
|
|
29
|
+
right: 0;
|
|
30
|
+
bottom: 0;
|
|
31
|
+
z-index: 100;
|
|
32
|
+
display: flex;
|
|
33
|
+
background: transparent;
|
|
34
|
+
`,
|
|
35
|
+
container: css`
|
|
36
|
+
display: flex;
|
|
37
|
+
flex: 1;
|
|
38
|
+
margin: 24px;
|
|
39
|
+
background: ${colors.surface};
|
|
40
|
+
border: 1px solid ${colors.border};
|
|
41
|
+
border-radius: 12px;
|
|
42
|
+
overflow: hidden;
|
|
43
|
+
box-shadow: 0 20px 40px rgba(0, 0, 0, 0.2);
|
|
44
|
+
`,
|
|
45
|
+
main: css`
|
|
46
|
+
position: relative;
|
|
47
|
+
flex: 1;
|
|
48
|
+
display: flex;
|
|
49
|
+
flex-direction: column;
|
|
50
|
+
align-items: center;
|
|
51
|
+
justify-content: center;
|
|
52
|
+
padding: 24px;
|
|
53
|
+
background: ${colors.background};
|
|
54
|
+
overflow: auto;
|
|
55
|
+
`,
|
|
56
|
+
headerButtons: css`
|
|
57
|
+
position: absolute;
|
|
58
|
+
top: 16px;
|
|
59
|
+
right: 16px;
|
|
60
|
+
display: flex;
|
|
61
|
+
gap: 8px;
|
|
62
|
+
z-index: 10;
|
|
63
|
+
`,
|
|
64
|
+
copyBtn: css`
|
|
65
|
+
position: relative;
|
|
66
|
+
padding: 8px;
|
|
67
|
+
background: ${colors.surface};
|
|
68
|
+
border: 1px solid ${colors.border};
|
|
69
|
+
border-radius: 8px;
|
|
70
|
+
cursor: pointer;
|
|
71
|
+
transition: all 0.15s ease;
|
|
72
|
+
display: flex;
|
|
73
|
+
align-items: center;
|
|
74
|
+
justify-content: center;
|
|
75
|
+
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.08);
|
|
76
|
+
|
|
77
|
+
&:hover {
|
|
78
|
+
background-color: ${colors.surfaceHover};
|
|
79
|
+
border-color: ${colors.borderHover};
|
|
80
|
+
}
|
|
81
|
+
`,
|
|
82
|
+
copyIcon: css`
|
|
83
|
+
width: 20px;
|
|
84
|
+
height: 20px;
|
|
85
|
+
color: ${colors.textSecondary};
|
|
86
|
+
`,
|
|
87
|
+
statusIcon: css`
|
|
88
|
+
display: flex;
|
|
89
|
+
align-items: center;
|
|
90
|
+
justify-content: center;
|
|
91
|
+
padding: 8px;
|
|
92
|
+
background: ${colors.surface};
|
|
93
|
+
border: 1px solid ${colors.border};
|
|
94
|
+
border-radius: 8px;
|
|
95
|
+
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.08);
|
|
96
|
+
`,
|
|
97
|
+
cloudIcon: css`
|
|
98
|
+
width: 22px;
|
|
99
|
+
height: 22px;
|
|
100
|
+
color: #f59e0b;
|
|
101
|
+
transform: translateY(1px);
|
|
102
|
+
`,
|
|
103
|
+
globeIcon: css`
|
|
104
|
+
width: 20px;
|
|
105
|
+
height: 20px;
|
|
106
|
+
color: #ef4444;
|
|
107
|
+
`,
|
|
108
|
+
tooltip: css`
|
|
109
|
+
position: absolute;
|
|
110
|
+
right: 100%;
|
|
111
|
+
top: 50%;
|
|
112
|
+
transform: translateY(-50%);
|
|
113
|
+
background: #1a1f36;
|
|
114
|
+
color: white;
|
|
115
|
+
padding: 4px 8px;
|
|
116
|
+
border-radius: 4px;
|
|
117
|
+
font-size: 12px;
|
|
118
|
+
white-space: nowrap;
|
|
119
|
+
margin-right: 8px;
|
|
120
|
+
pointer-events: none;
|
|
121
|
+
z-index: 100;
|
|
122
|
+
|
|
123
|
+
&::after {
|
|
124
|
+
content: '';
|
|
125
|
+
position: absolute;
|
|
126
|
+
left: 100%;
|
|
127
|
+
top: 50%;
|
|
128
|
+
transform: translateY(-50%);
|
|
129
|
+
border: 4px solid transparent;
|
|
130
|
+
border-left-color: #1a1f36;
|
|
131
|
+
}
|
|
132
|
+
`,
|
|
133
|
+
mainCloseBtn: css`
|
|
134
|
+
padding: 8px;
|
|
135
|
+
background: ${colors.surface};
|
|
136
|
+
border: 1px solid ${colors.border};
|
|
137
|
+
border-radius: 8px;
|
|
138
|
+
cursor: pointer;
|
|
139
|
+
transition: all 0.15s ease;
|
|
140
|
+
display: flex;
|
|
141
|
+
align-items: center;
|
|
142
|
+
justify-content: center;
|
|
143
|
+
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.08);
|
|
144
|
+
|
|
145
|
+
&:hover {
|
|
146
|
+
background-color: ${colors.surfaceHover};
|
|
147
|
+
border-color: ${colors.borderHover};
|
|
148
|
+
}
|
|
149
|
+
`,
|
|
150
|
+
mainCloseIcon: css`
|
|
151
|
+
width: 20px;
|
|
152
|
+
height: 20px;
|
|
153
|
+
color: ${colors.textSecondary};
|
|
154
|
+
`,
|
|
155
|
+
mediaWrapper: css`
|
|
156
|
+
max-width: 100%;
|
|
157
|
+
max-height: 100%;
|
|
158
|
+
display: flex;
|
|
159
|
+
align-items: center;
|
|
160
|
+
justify-content: center;
|
|
161
|
+
`,
|
|
162
|
+
image: css`
|
|
163
|
+
max-width: 100%;
|
|
164
|
+
max-height: calc(100vh - 200px);
|
|
165
|
+
object-fit: contain;
|
|
166
|
+
border-radius: 8px;
|
|
167
|
+
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.1);
|
|
168
|
+
`,
|
|
169
|
+
video: css`
|
|
170
|
+
max-width: 100%;
|
|
171
|
+
max-height: calc(100vh - 200px);
|
|
172
|
+
border-radius: 8px;
|
|
173
|
+
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.1);
|
|
174
|
+
`,
|
|
175
|
+
filePlaceholder: css`
|
|
176
|
+
display: flex;
|
|
177
|
+
flex-direction: column;
|
|
178
|
+
align-items: center;
|
|
179
|
+
justify-content: center;
|
|
180
|
+
padding: 48px;
|
|
181
|
+
background: ${colors.surface};
|
|
182
|
+
border-radius: 12px;
|
|
183
|
+
border: 1px solid ${colors.border};
|
|
184
|
+
`,
|
|
185
|
+
fileIcon: css`
|
|
186
|
+
width: 80px;
|
|
187
|
+
height: 80px;
|
|
188
|
+
color: ${colors.textMuted};
|
|
189
|
+
margin-bottom: 16px;
|
|
190
|
+
`,
|
|
191
|
+
fileName: css`
|
|
192
|
+
font-size: ${fontSize.lg};
|
|
193
|
+
font-weight: 600;
|
|
194
|
+
color: ${colors.text};
|
|
195
|
+
margin: 0;
|
|
196
|
+
`,
|
|
197
|
+
sidebar: css`
|
|
198
|
+
width: 280px;
|
|
199
|
+
background: ${colors.surface};
|
|
200
|
+
border-left: 1px solid ${colors.border};
|
|
201
|
+
display: flex;
|
|
202
|
+
flex-direction: column;
|
|
203
|
+
overflow: hidden;
|
|
204
|
+
`,
|
|
205
|
+
sidebarHeader: css`
|
|
206
|
+
padding: 16px 20px;
|
|
207
|
+
border-bottom: 1px solid ${colors.border};
|
|
208
|
+
`,
|
|
209
|
+
sidebarTitle: css`
|
|
210
|
+
font-size: ${fontSize.base};
|
|
211
|
+
font-weight: 600;
|
|
212
|
+
color: ${colors.text};
|
|
213
|
+
margin: 0;
|
|
214
|
+
`,
|
|
215
|
+
sidebarContent: css`
|
|
216
|
+
flex: 1;
|
|
217
|
+
padding: 20px;
|
|
218
|
+
overflow: auto;
|
|
219
|
+
`,
|
|
220
|
+
info: css`
|
|
221
|
+
display: flex;
|
|
222
|
+
flex-direction: column;
|
|
223
|
+
gap: 12px;
|
|
224
|
+
margin-bottom: 24px;
|
|
225
|
+
`,
|
|
226
|
+
infoRow: css`
|
|
227
|
+
display: flex;
|
|
228
|
+
justify-content: space-between;
|
|
229
|
+
font-size: ${fontSize.sm};
|
|
230
|
+
`,
|
|
231
|
+
infoLabel: css`
|
|
232
|
+
color: ${colors.textSecondary};
|
|
233
|
+
`,
|
|
234
|
+
infoValue: css`
|
|
235
|
+
color: ${colors.text};
|
|
236
|
+
font-weight: 500;
|
|
237
|
+
text-align: right;
|
|
238
|
+
max-width: 160px;
|
|
239
|
+
overflow: hidden;
|
|
240
|
+
text-overflow: ellipsis;
|
|
241
|
+
white-space: nowrap;
|
|
242
|
+
`,
|
|
243
|
+
infoValueWrap: css`
|
|
244
|
+
color: ${colors.text};
|
|
245
|
+
font-weight: 500;
|
|
246
|
+
text-align: right;
|
|
247
|
+
max-width: 160px;
|
|
248
|
+
word-break: break-all;
|
|
249
|
+
white-space: normal;
|
|
250
|
+
`,
|
|
251
|
+
infoLink: css`
|
|
252
|
+
color: ${colors.primary};
|
|
253
|
+
font-weight: 500;
|
|
254
|
+
text-align: right;
|
|
255
|
+
max-width: 160px;
|
|
256
|
+
word-break: break-all;
|
|
257
|
+
white-space: normal;
|
|
258
|
+
text-decoration: none;
|
|
259
|
+
|
|
260
|
+
&:hover {
|
|
261
|
+
text-decoration: underline;
|
|
262
|
+
}
|
|
263
|
+
`,
|
|
264
|
+
actions: css`
|
|
265
|
+
display: flex;
|
|
266
|
+
flex-direction: column;
|
|
267
|
+
gap: 8px;
|
|
268
|
+
`,
|
|
269
|
+
actionBtn: css`
|
|
270
|
+
display: flex;
|
|
271
|
+
align-items: center;
|
|
272
|
+
gap: 10px;
|
|
273
|
+
width: 100%;
|
|
274
|
+
padding: 12px 14px;
|
|
275
|
+
font-size: ${fontSize.base};
|
|
276
|
+
font-weight: 500;
|
|
277
|
+
background: ${colors.surface};
|
|
278
|
+
border: 1px solid ${colors.border};
|
|
279
|
+
border-radius: 6px;
|
|
280
|
+
cursor: pointer;
|
|
281
|
+
transition: all 0.15s ease;
|
|
282
|
+
color: ${colors.text};
|
|
283
|
+
text-align: left;
|
|
284
|
+
|
|
285
|
+
&:hover:not(:disabled) {
|
|
286
|
+
background-color: ${colors.surfaceHover};
|
|
287
|
+
border-color: ${colors.borderHover};
|
|
288
|
+
}
|
|
289
|
+
|
|
290
|
+
&:disabled {
|
|
291
|
+
opacity: 0.5;
|
|
292
|
+
cursor: not-allowed;
|
|
293
|
+
}
|
|
294
|
+
`,
|
|
295
|
+
actionBtnDanger: css`
|
|
296
|
+
color: ${colors.danger};
|
|
297
|
+
|
|
298
|
+
&:hover:not(:disabled) {
|
|
299
|
+
background-color: ${colors.dangerLight};
|
|
300
|
+
border-color: ${colors.danger};
|
|
301
|
+
}
|
|
302
|
+
`,
|
|
303
|
+
actionIcon: css`
|
|
304
|
+
width: 16px;
|
|
305
|
+
height: 16px;
|
|
306
|
+
flex-shrink: 0;
|
|
307
|
+
`,
|
|
308
|
+
}
|
|
309
|
+
|
|
310
|
+
export function StudioDetailView() {
|
|
311
|
+
const {
|
|
312
|
+
focusedItem,
|
|
313
|
+
setFocusedItem,
|
|
314
|
+
triggerRefresh,
|
|
315
|
+
fileItems,
|
|
316
|
+
// Shared action handlers
|
|
317
|
+
requestDelete,
|
|
318
|
+
requestMove,
|
|
319
|
+
requestSync,
|
|
320
|
+
requestProcess,
|
|
321
|
+
actionState,
|
|
322
|
+
} = useStudio()
|
|
323
|
+
const [showRenameModal, setShowRenameModal] = useState(false)
|
|
324
|
+
const [showR2SetupModal, setShowR2SetupModal] = useState(false)
|
|
325
|
+
const [alertMessage, setAlertMessage] = useState<{ title: string; message: string } | null>(null)
|
|
326
|
+
const [showCopied, setShowCopied] = useState(false)
|
|
327
|
+
const [showFaviconProgress, setShowFaviconProgress] = useState(false)
|
|
328
|
+
const [faviconProgress, setFaviconProgress] = useState<ProgressState>({
|
|
329
|
+
current: 0,
|
|
330
|
+
total: 3,
|
|
331
|
+
percent: 0,
|
|
332
|
+
status: 'processing',
|
|
333
|
+
message: 'Generating favicons...',
|
|
334
|
+
})
|
|
335
|
+
|
|
336
|
+
// Check if an action is in progress
|
|
337
|
+
const isActionInProgress = actionState.showProgress
|
|
338
|
+
|
|
339
|
+
// Check if this is a favicon source file
|
|
340
|
+
const isFaviconSource = focusedItem ?
|
|
341
|
+
focusedItem.name.toLowerCase() === 'favicon.png' ||
|
|
342
|
+
focusedItem.name.toLowerCase() === 'favicon.jpg' : false
|
|
343
|
+
|
|
344
|
+
if (!focusedItem) return null
|
|
345
|
+
|
|
346
|
+
const isImage = isImageFile(focusedItem.name)
|
|
347
|
+
const isVideo = isVideoFile(focusedItem.name)
|
|
348
|
+
const relativePath = '/' + focusedItem.path.replace(/^public\//, '')
|
|
349
|
+
|
|
350
|
+
// For preview: use CDN URL if pushed, otherwise use local path
|
|
351
|
+
const imageSrc = focusedItem.cdnPushed && focusedItem.cdnBaseUrl
|
|
352
|
+
? `${focusedItem.cdnBaseUrl}${relativePath}`
|
|
353
|
+
: relativePath
|
|
354
|
+
|
|
355
|
+
// For display URL: use CDN URL if pushed, otherwise use current origin
|
|
356
|
+
const localOrigin = typeof window !== 'undefined' ? window.location.origin : ''
|
|
357
|
+
const fullUrl = focusedItem.cdnPushed && focusedItem.cdnBaseUrl
|
|
358
|
+
? `${focusedItem.cdnBaseUrl}${relativePath}`
|
|
359
|
+
: `${localOrigin}${relativePath}`
|
|
360
|
+
|
|
361
|
+
const handleClose = () => {
|
|
362
|
+
setFocusedItem(null)
|
|
363
|
+
}
|
|
364
|
+
|
|
365
|
+
const handleCopyPath = () => {
|
|
366
|
+
navigator.clipboard.writeText(fullUrl)
|
|
367
|
+
setShowCopied(true)
|
|
368
|
+
setTimeout(() => setShowCopied(false), 1500)
|
|
369
|
+
}
|
|
370
|
+
|
|
371
|
+
const handleRename = async (newName: string) => {
|
|
372
|
+
setShowRenameModal(false)
|
|
373
|
+
if (newName && newName !== focusedItem.name) {
|
|
374
|
+
try {
|
|
375
|
+
const response = await fetch('/api/studio/rename', {
|
|
376
|
+
method: 'POST',
|
|
377
|
+
headers: { 'Content-Type': 'application/json' },
|
|
378
|
+
body: JSON.stringify({
|
|
379
|
+
oldPath: focusedItem.path,
|
|
380
|
+
newName: newName,
|
|
381
|
+
}),
|
|
382
|
+
})
|
|
383
|
+
|
|
384
|
+
if (response.ok) {
|
|
385
|
+
triggerRefresh()
|
|
386
|
+
setFocusedItem(null)
|
|
387
|
+
} else {
|
|
388
|
+
const data = await response.json()
|
|
389
|
+
setAlertMessage({
|
|
390
|
+
title: 'Rename Failed',
|
|
391
|
+
message: data.error || 'Failed to rename file',
|
|
392
|
+
})
|
|
393
|
+
}
|
|
394
|
+
} catch (error) {
|
|
395
|
+
console.error('Rename error:', error)
|
|
396
|
+
setAlertMessage({
|
|
397
|
+
title: 'Rename Failed',
|
|
398
|
+
message: 'An error occurred while renaming the file',
|
|
399
|
+
})
|
|
400
|
+
}
|
|
401
|
+
}
|
|
402
|
+
}
|
|
403
|
+
|
|
404
|
+
const handleGenerateFavicons = async () => {
|
|
405
|
+
if (!focusedItem) return
|
|
406
|
+
|
|
407
|
+
setShowFaviconProgress(true)
|
|
408
|
+
setFaviconProgress({
|
|
409
|
+
current: 0,
|
|
410
|
+
total: 3,
|
|
411
|
+
percent: 0,
|
|
412
|
+
status: 'processing',
|
|
413
|
+
message: 'Generating favicons...',
|
|
414
|
+
})
|
|
415
|
+
|
|
416
|
+
try {
|
|
417
|
+
const response = await fetch('/api/studio/generate-favicon', {
|
|
418
|
+
method: 'POST',
|
|
419
|
+
headers: { 'Content-Type': 'application/json' },
|
|
420
|
+
body: JSON.stringify({
|
|
421
|
+
imagePath: '/' + focusedItem.path.replace(/^public\//, ''),
|
|
422
|
+
}),
|
|
423
|
+
})
|
|
424
|
+
|
|
425
|
+
if (!response.ok) {
|
|
426
|
+
const error = await response.json()
|
|
427
|
+
setFaviconProgress({
|
|
428
|
+
current: 0,
|
|
429
|
+
total: 3,
|
|
430
|
+
percent: 0,
|
|
431
|
+
status: 'error',
|
|
432
|
+
message: error.error || 'Failed to generate favicons',
|
|
433
|
+
})
|
|
434
|
+
return
|
|
435
|
+
}
|
|
436
|
+
|
|
437
|
+
const reader = response.body?.getReader()
|
|
438
|
+
const decoder = new TextDecoder()
|
|
439
|
+
|
|
440
|
+
if (reader) {
|
|
441
|
+
let buffer = ''
|
|
442
|
+
while (true) {
|
|
443
|
+
const { done, value } = await reader.read()
|
|
444
|
+
if (done) break
|
|
445
|
+
|
|
446
|
+
buffer += decoder.decode(value, { stream: true })
|
|
447
|
+
const lines = buffer.split('\n')
|
|
448
|
+
buffer = lines.pop() || ''
|
|
449
|
+
|
|
450
|
+
for (const line of lines) {
|
|
451
|
+
if (line.startsWith('data: ')) {
|
|
452
|
+
try {
|
|
453
|
+
const data = JSON.parse(line.slice(6))
|
|
454
|
+
|
|
455
|
+
if (data.type === 'start') {
|
|
456
|
+
setFaviconProgress(prev => ({
|
|
457
|
+
...prev,
|
|
458
|
+
total: data.total,
|
|
459
|
+
}))
|
|
460
|
+
} else if (data.type === 'progress') {
|
|
461
|
+
setFaviconProgress({
|
|
462
|
+
current: data.current,
|
|
463
|
+
total: data.total,
|
|
464
|
+
percent: data.percent,
|
|
465
|
+
status: 'processing',
|
|
466
|
+
message: data.message,
|
|
467
|
+
})
|
|
468
|
+
} else if (data.type === 'complete') {
|
|
469
|
+
setFaviconProgress({
|
|
470
|
+
current: data.processed,
|
|
471
|
+
total: data.processed,
|
|
472
|
+
percent: 100,
|
|
473
|
+
status: data.errors > 0 ? 'error' : 'complete',
|
|
474
|
+
message: data.message,
|
|
475
|
+
})
|
|
476
|
+
} else if (data.type === 'error') {
|
|
477
|
+
setFaviconProgress(prev => ({
|
|
478
|
+
...prev,
|
|
479
|
+
status: 'error',
|
|
480
|
+
message: data.message,
|
|
481
|
+
}))
|
|
482
|
+
}
|
|
483
|
+
} catch { /* ignore parse errors */ }
|
|
484
|
+
}
|
|
485
|
+
}
|
|
486
|
+
}
|
|
487
|
+
}
|
|
488
|
+
} catch (error) {
|
|
489
|
+
console.error('Favicon generation error:', error)
|
|
490
|
+
setFaviconProgress({
|
|
491
|
+
current: 0,
|
|
492
|
+
total: 3,
|
|
493
|
+
percent: 0,
|
|
494
|
+
status: 'error',
|
|
495
|
+
message: 'An error occurred while generating favicons',
|
|
496
|
+
})
|
|
497
|
+
}
|
|
498
|
+
}
|
|
499
|
+
|
|
500
|
+
const renderMedia = () => {
|
|
501
|
+
if (isImage) {
|
|
502
|
+
return <img css={styles.image} src={imageSrc} alt={focusedItem.name} />
|
|
503
|
+
}
|
|
504
|
+
if (isVideo) {
|
|
505
|
+
return <video css={styles.video} src={imageSrc} controls />
|
|
506
|
+
}
|
|
507
|
+
return (
|
|
508
|
+
<div css={styles.filePlaceholder}>
|
|
509
|
+
<svg css={styles.fileIcon} fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
|
510
|
+
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={1.5} d="M7 21h10a2 2 0 002-2V9.414a1 1 0 00-.293-.707l-5.414-5.414A1 1 0 0012.586 3H7a2 2 0 00-2 2v14a2 2 0 002 2z" />
|
|
511
|
+
</svg>
|
|
512
|
+
<p css={styles.fileName}>{focusedItem.name}</p>
|
|
513
|
+
</div>
|
|
514
|
+
)
|
|
515
|
+
}
|
|
516
|
+
|
|
517
|
+
return (
|
|
518
|
+
<>
|
|
519
|
+
{alertMessage && (
|
|
520
|
+
<AlertModal
|
|
521
|
+
title={alertMessage.title}
|
|
522
|
+
message={alertMessage.message}
|
|
523
|
+
onClose={() => setAlertMessage(null)}
|
|
524
|
+
/>
|
|
525
|
+
)}
|
|
526
|
+
|
|
527
|
+
<R2SetupModal
|
|
528
|
+
isOpen={showR2SetupModal}
|
|
529
|
+
onClose={() => setShowR2SetupModal(false)}
|
|
530
|
+
/>
|
|
531
|
+
|
|
532
|
+
{showRenameModal && (
|
|
533
|
+
<InputModal
|
|
534
|
+
title="Rename File"
|
|
535
|
+
message="Enter a new name for the file:"
|
|
536
|
+
defaultValue={focusedItem.name}
|
|
537
|
+
placeholder="Enter new filename"
|
|
538
|
+
confirmLabel="Rename"
|
|
539
|
+
onConfirm={handleRename}
|
|
540
|
+
onCancel={() => setShowRenameModal(false)}
|
|
541
|
+
/>
|
|
542
|
+
)}
|
|
543
|
+
|
|
544
|
+
{showFaviconProgress && (
|
|
545
|
+
<ProgressModal
|
|
546
|
+
title="Generating Favicons"
|
|
547
|
+
progress={faviconProgress}
|
|
548
|
+
onClose={() => setShowFaviconProgress(false)}
|
|
549
|
+
/>
|
|
550
|
+
)}
|
|
551
|
+
|
|
552
|
+
<div css={styles.overlay} onClick={handleClose}>
|
|
553
|
+
<div css={styles.container} onClick={(e) => e.stopPropagation()}>
|
|
554
|
+
<div css={styles.main}>
|
|
555
|
+
<div css={styles.headerButtons}>
|
|
556
|
+
{/* Cloud status icons */}
|
|
557
|
+
{focusedItem.cdnPushed && !focusedItem.isRemote && (
|
|
558
|
+
<span css={styles.statusIcon} title="Pushed to CDN">
|
|
559
|
+
<svg css={styles.cloudIcon} fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
|
560
|
+
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M3 15a4 4 0 004 4h9a5 5 0 10-.1-9.999 5.002 5.002 0 10-9.78 2.096A4.001 4.001 0 003 15z" />
|
|
561
|
+
</svg>
|
|
562
|
+
</span>
|
|
563
|
+
)}
|
|
564
|
+
{focusedItem.isRemote && (
|
|
565
|
+
<span css={styles.statusIcon} title="Remote image">
|
|
566
|
+
<svg css={styles.globeIcon} fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
|
567
|
+
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M21 12a9 9 0 01-9 9m9-9a9 9 0 00-9-9m9 9H3m9 9a9 9 0 01-9-9m9 9c1.657 0 3-4.03 3-9s-1.343-9-3-9m0 18c-1.657 0-3-4.03-3-9s1.343-9 3-9m-9 9a9 9 0 019-9" />
|
|
568
|
+
</svg>
|
|
569
|
+
</span>
|
|
570
|
+
)}
|
|
571
|
+
<button css={styles.copyBtn} onClick={handleCopyPath} title="Copy file path">
|
|
572
|
+
{showCopied && <span css={styles.tooltip}>Copied!</span>}
|
|
573
|
+
<svg css={styles.copyIcon} fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
|
574
|
+
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M9 5H7a2 2 0 00-2 2v12a2 2 0 002 2h10a2 2 0 002-2V7a2 2 0 00-2-2h-2M9 5a2 2 0 002 2h2a2 2 0 002-2M9 5a2 2 0 012-2h2a2 2 0 012 2" />
|
|
575
|
+
</svg>
|
|
576
|
+
</button>
|
|
577
|
+
<button css={styles.mainCloseBtn} onClick={handleClose} aria-label="Close">
|
|
578
|
+
<svg css={styles.mainCloseIcon} fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
|
579
|
+
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M6 18L18 6M6 6l12 12" />
|
|
580
|
+
</svg>
|
|
581
|
+
</button>
|
|
582
|
+
</div>
|
|
583
|
+
<div css={styles.mediaWrapper}>
|
|
584
|
+
{renderMedia()}
|
|
585
|
+
</div>
|
|
586
|
+
</div>
|
|
587
|
+
|
|
588
|
+
<div css={styles.sidebar}>
|
|
589
|
+
<div css={styles.sidebarHeader}>
|
|
590
|
+
<h3 css={styles.sidebarTitle}>Details</h3>
|
|
591
|
+
</div>
|
|
592
|
+
|
|
593
|
+
<div css={styles.sidebarContent}>
|
|
594
|
+
<div css={styles.info}>
|
|
595
|
+
<div css={styles.infoRow}>
|
|
596
|
+
<span css={styles.infoLabel}>Name</span>
|
|
597
|
+
<span css={styles.infoValueWrap}>{focusedItem.name}</span>
|
|
598
|
+
</div>
|
|
599
|
+
<div css={styles.infoRow}>
|
|
600
|
+
<span css={styles.infoLabel}>Path</span>
|
|
601
|
+
<span css={styles.infoValueWrap}>{focusedItem.path.replace(/^public\//, '')}</span>
|
|
602
|
+
</div>
|
|
603
|
+
{focusedItem.size !== undefined && (
|
|
604
|
+
<div css={styles.infoRow}>
|
|
605
|
+
<span css={styles.infoLabel}>Size</span>
|
|
606
|
+
<span css={styles.infoValue}>{formatFileSize(focusedItem.size)}</span>
|
|
607
|
+
</div>
|
|
608
|
+
)}
|
|
609
|
+
{focusedItem.dimensions && (
|
|
610
|
+
<div css={styles.infoRow}>
|
|
611
|
+
<span css={styles.infoLabel}>Dimensions</span>
|
|
612
|
+
<span css={styles.infoValue}>{focusedItem.dimensions.width} × {focusedItem.dimensions.height}</span>
|
|
613
|
+
</div>
|
|
614
|
+
)}
|
|
615
|
+
<div css={styles.infoRow}>
|
|
616
|
+
<span css={styles.infoLabel}>CDN Status</span>
|
|
617
|
+
<span css={styles.infoValue}>
|
|
618
|
+
{focusedItem.cdnPushed
|
|
619
|
+
? (focusedItem.isRemote ? 'Remote' : 'Pushed')
|
|
620
|
+
: 'Local'}
|
|
621
|
+
</span>
|
|
622
|
+
</div>
|
|
623
|
+
<div css={styles.infoRow}>
|
|
624
|
+
<span css={styles.infoLabel}>URL</span>
|
|
625
|
+
<a
|
|
626
|
+
href={fullUrl}
|
|
627
|
+
target="_blank"
|
|
628
|
+
rel="noopener noreferrer"
|
|
629
|
+
css={styles.infoLink}
|
|
630
|
+
title={fullUrl}
|
|
631
|
+
>
|
|
632
|
+
{fullUrl}
|
|
633
|
+
</a>
|
|
634
|
+
</div>
|
|
635
|
+
</div>
|
|
636
|
+
|
|
637
|
+
<div css={styles.actions}>
|
|
638
|
+
<button
|
|
639
|
+
css={styles.actionBtn}
|
|
640
|
+
onClick={() => setShowRenameModal(true)}
|
|
641
|
+
disabled={focusedItem.isProtected}
|
|
642
|
+
>
|
|
643
|
+
<svg css={styles.actionIcon} fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
|
644
|
+
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M11 5H6a2 2 0 00-2 2v11a2 2 0 002 2h11a2 2 0 002-2v-5m-1.414-9.414a2 2 0 112.828 2.828L11.828 15H9v-2.828l8.586-8.586z" />
|
|
645
|
+
</svg>
|
|
646
|
+
Rename
|
|
647
|
+
</button>
|
|
648
|
+
<button
|
|
649
|
+
css={styles.actionBtn}
|
|
650
|
+
onClick={() => requestMove([focusedItem.path])}
|
|
651
|
+
disabled={isActionInProgress || focusedItem.isProtected}
|
|
652
|
+
>
|
|
653
|
+
<svg css={styles.actionIcon} fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
|
654
|
+
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M8 7h12m0 0l-4-4m4 4l-4 4m0 6H4m0 0l4 4m-4-4l4-4" />
|
|
655
|
+
</svg>
|
|
656
|
+
Move
|
|
657
|
+
</button>
|
|
658
|
+
<button
|
|
659
|
+
css={styles.actionBtn}
|
|
660
|
+
onClick={() => requestSync([focusedItem.path], fileItems)}
|
|
661
|
+
disabled={isActionInProgress || focusedItem.isProtected || (focusedItem.cdnPushed && !focusedItem.isRemote)}
|
|
662
|
+
title={focusedItem.cdnPushed && !focusedItem.isRemote ? 'Already in R2' : undefined}
|
|
663
|
+
>
|
|
664
|
+
<svg css={styles.actionIcon} fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
|
665
|
+
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M7 16a4 4 0 01-.88-7.903A5 5 0 1115.9 6L16 6a5 5 0 011 9.9M15 13l-3-3m0 0l-3 3m3-3v12" />
|
|
666
|
+
</svg>
|
|
667
|
+
Push to CDN
|
|
668
|
+
</button>
|
|
669
|
+
<button
|
|
670
|
+
css={styles.actionBtn}
|
|
671
|
+
onClick={() => requestProcess([focusedItem.path])}
|
|
672
|
+
disabled={isActionInProgress || focusedItem.isProtected}
|
|
673
|
+
>
|
|
674
|
+
<svg css={styles.actionIcon} fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
|
675
|
+
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M4 16l4.586-4.586a2 2 0 012.828 0L16 16m-2-2l1.586-1.586a2 2 0 012.828 0L20 14m-6-6h.01M6 20h12a2 2 0 002-2V6a2 2 0 00-2-2H6a2 2 0 00-2 2v12a2 2 0 002 2z" />
|
|
676
|
+
</svg>
|
|
677
|
+
Process Image
|
|
678
|
+
</button>
|
|
679
|
+
{isFaviconSource && (
|
|
680
|
+
<button
|
|
681
|
+
css={styles.actionBtn}
|
|
682
|
+
onClick={handleGenerateFavicons}
|
|
683
|
+
disabled={showFaviconProgress || focusedItem.isProtected}
|
|
684
|
+
>
|
|
685
|
+
<svg css={styles.actionIcon} fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
|
686
|
+
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M5 3v4M3 5h4M6 17v4m-2-2h4m5-16l2.286 6.857L21 12l-5.714 2.143L13 21l-2.286-6.857L5 12l5.714-2.143L13 3z" />
|
|
687
|
+
</svg>
|
|
688
|
+
Generate Favicons
|
|
689
|
+
</button>
|
|
690
|
+
)}
|
|
691
|
+
<button
|
|
692
|
+
css={[styles.actionBtn, styles.actionBtnDanger]}
|
|
693
|
+
onClick={() => requestDelete([focusedItem.path])}
|
|
694
|
+
disabled={isActionInProgress || focusedItem.isProtected}
|
|
695
|
+
>
|
|
696
|
+
<svg css={styles.actionIcon} fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
|
697
|
+
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M19 7l-.867 12.142A2 2 0 0116.138 21H7.862a2 2 0 01-1.995-1.858L5 7m5 4v6m4-6v6m1-10V4a1 1 0 00-1-1h-4a1 1 0 00-1 1v3M4 7h16" />
|
|
698
|
+
</svg>
|
|
699
|
+
Delete
|
|
700
|
+
</button>
|
|
701
|
+
</div>
|
|
702
|
+
</div>
|
|
703
|
+
</div>
|
|
704
|
+
</div>
|
|
705
|
+
</div>
|
|
706
|
+
</>
|
|
707
|
+
)
|
|
708
|
+
}
|
|
709
|
+
|
|
710
|
+
function formatFileSize(bytes: number): string {
|
|
711
|
+
if (bytes < 1024) return `${bytes} B`
|
|
712
|
+
if (bytes < 1024 * 1024) return `${(bytes / 1024).toFixed(1)} KB`
|
|
713
|
+
return `${(bytes / (1024 * 1024)).toFixed(1)} MB`
|
|
714
|
+
}
|