most-box 0.0.1 → 0.0.2

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/public/app.jsx CHANGED
@@ -1,12 +1,12 @@
1
1
  import React, { useState, useEffect, useRef } from 'react'
2
2
  import {
3
3
  Upload, Sun, Moon, Image as ImageIcon, Trash2, Folder,
4
- FolderPlus, Film, Music, ChevronRight, FileText,
4
+ Film, Music, ChevronRight, FileText,
5
5
  X, Check, Copy, Download, ArrowUpDown, Star, Files, HardDrive, Search, Info,
6
- FolderOpen, Power
6
+ FolderOpen, Power, Edit2, Menu, Eye, Loader
7
7
  } from 'lucide-react'
8
8
 
9
- // === API ===
9
+ // === 接口 ===
10
10
  const API = {
11
11
  async fetch(url, options = {}) {
12
12
  const res = await fetch(url, options)
@@ -64,7 +64,7 @@ const API = {
64
64
  })
65
65
  }
66
66
 
67
- // === Helpers ===
67
+ // === 工具函数 ===
68
68
  function formatSize(bytes) {
69
69
  if (!bytes || bytes <= 0) return '0 B'
70
70
  if (bytes < 1024) return `${bytes} B`
@@ -119,41 +119,135 @@ function getFileSubtype(fileName) {
119
119
  const imgExts = ['jpg', 'jpeg', 'png', 'gif', 'webp', 'svg', 'bmp', 'ico', 'tiff', 'heic', 'heif']
120
120
  const vidExts = ['mp4', 'webm', 'mov', 'avi', 'mkv', 'flv', 'wmv', 'm4v', 'mpeg', '3gp']
121
121
  const audExts = ['mp3', 'wav', 'ogg', 'flac', 'aac', 'm4a', 'wma', 'opus']
122
+ const txtExts = ['txt', 'md', 'js', 'ts', 'jsx', 'tsx', 'css', 'scss', 'less', 'json', 'xml', 'html', 'htm', 'yaml', 'yml', 'toml', 'ini', 'cfg', 'conf', 'log', 'sh', 'bash', 'py', 'rb', 'go', 'rs', 'java', 'c', 'cpp', 'h', 'hpp', 'cs', 'php', 'sql', 'graphql', 'env', 'gitignore', 'dockerfile', 'readme']
122
123
  if (imgExts.includes(ext)) return 'image'
123
124
  if (vidExts.includes(ext)) return 'video'
124
125
  if (audExts.includes(ext)) return 'audio'
126
+ if (txtExts.includes(ext)) return 'text'
125
127
  return 'file'
126
128
  }
127
129
 
128
- // === Welcome Guide ===
129
- function WelcomeGuide({ onClose }) {
130
+ // === 引导页 ===
131
+ function WelcomeGuide({ onClose, onShutdown }) {
130
132
  const [step, setStep] = useState(0)
133
+ const [customPath, setCustomPath] = useState('')
134
+ const [saving, setSaving] = useState(false)
135
+ const [saved, setSaved] = useState(false)
136
+ const [defaultPath, setDefaultPath] = useState('')
137
+
138
+ useEffect(() => {
139
+ API.getDataPath().then(config => {
140
+ setDefaultPath(config.dataPath || '')
141
+ }).catch(() => { })
142
+ }, [])
143
+
131
144
  const steps = [
132
145
  { title: '欢迎使用', content: '拖拽文件到上传区,或点击选择文件。上传后复制链接发给朋友即可。' },
133
- { title: '下载文件', content: '点击「下载文件」,粘贴分享链接即可从 P2P 网络下载文件。' }
146
+ { title: '下载文件', content: '点击「下载文件」,粘贴分享链接即可从 P2P 网络下载文件。' },
147
+ { title: '设置存储位置', content: '选择文件存储的文件夹位置(可选,默认使用系统盘)', isOptional: true }
134
148
  ]
135
149
  const current = steps[step]
136
150
 
151
+ const handleSavePath = async () => {
152
+ if (!customPath.trim()) return
153
+ setSaving(true)
154
+ try {
155
+ await API.saveConfig({ dataPath: customPath.trim() })
156
+ } catch (err) {
157
+ console.error('Save path error:', err)
158
+ setSaving(false)
159
+ return
160
+ }
161
+ setSaving(false)
162
+ setSaved(true)
163
+ }
164
+
165
+ const isLastStep = step === steps.length - 1
166
+ const isPathStep = step === 2
167
+
137
168
  return (
138
- <ModalOverlay onClose={onClose}>
139
- <div style={{ width: 360, padding: 28, borderRadius: 16, background: '#fff', textAlign: 'center' }} onClick={e => e.stopPropagation()}>
140
- <h2 style={{ fontSize: 18, fontWeight: 600, marginBottom: 12 }}>{current.title}</h2>
141
- <p style={{ fontSize: 13, color: '#64748b', lineHeight: 1.6, marginBottom: 20 }}>{current.content}</p>
142
- <div style={{ display: 'flex', justifyContent: 'center', gap: 6, marginBottom: 20 }}>
143
- {steps.map((_, i) => (
144
- <div key={i} style={{ width: 6, height: 6, borderRadius: '50%', background: i === step ? '#3b82f6' : '#e2e8f0' }} />
145
- ))}
146
- </div>
147
- <button onClick={step === steps.length - 1 ? onClose : () => setStep(step + 1)} style={{ padding: '10px 32px', borderRadius: 10, border: 'none', background: '#3b82f6', color: '#fff', cursor: 'pointer', fontSize: 14 }}>
148
- {step === steps.length - 1 ? '开始使用' : '下一步'}
149
- </button>
169
+ <ModalOverlay onClose={onClose} closeOnOverlayClick={false}>
170
+ <div className="welcome-modal" onClick={e => e.stopPropagation()}>
171
+ {saved ? (
172
+ <>
173
+ <div className="welcome-success-icon">
174
+ <Check size={24} color="#22c55e" />
175
+ </div>
176
+ <h2>设置已保存</h2>
177
+ <p>存储位置已更改,需要重启应用生效。</p>
178
+ <button
179
+ onClick={onShutdown}
180
+ className="btn primary"
181
+ >
182
+ 好的
183
+ </button>
184
+ </>
185
+ ) : (
186
+ <>
187
+ <h2>{current.title}</h2>
188
+ <p>{current.content}</p>
189
+
190
+ {isPathStep && (
191
+ <div className="welcome-path-section">
192
+ <div>
193
+ <div className="path-label">当前存储位置</div>
194
+ <div className="path-value">{defaultPath || '未设置'}</div>
195
+ </div>
196
+ <div>
197
+ <div className="path-label">自定义位置</div>
198
+ <input
199
+ type="text"
200
+ value={customPath}
201
+ onChange={(e) => setCustomPath(e.target.value)}
202
+ placeholder="如 D:\"
203
+ className="path-input"
204
+ />
205
+ <div className="path-hint">不填则使用当前位置,修改后需重启应用</div>
206
+ </div>
207
+ </div>
208
+ )}
209
+
210
+ <div className="welcome-steps">
211
+ {steps.map((_, i) => (
212
+ <div key={i} className={`welcome-step-dot ${i === step ? 'active' : ''}`} />
213
+ ))}
214
+ </div>
215
+
216
+ <div className="welcome-actions">
217
+ {isPathStep && (
218
+ <button
219
+ onClick={onClose}
220
+ className="btn secondary"
221
+ >
222
+ 跳过
223
+ </button>
224
+ )}
225
+ <button
226
+ onClick={() => {
227
+ if (isPathStep && customPath) {
228
+ handleSavePath()
229
+ } else if (isLastStep) {
230
+ onClose()
231
+ } else {
232
+ setStep(step + 1)
233
+ }
234
+ }}
235
+ disabled={isPathStep && saving}
236
+ className="btn primary"
237
+ style={{ opacity: isPathStep && saving ? 0.6 : 1, cursor: isPathStep && saving ? 'not-allowed' : 'pointer' }}
238
+ >
239
+ {isPathStep ? (saving ? '保存中...' : '保存并完成') : (isLastStep ? '开始使用' : '下一步')}
240
+ </button>
241
+ </div>
242
+ </>
243
+ )}
150
244
  </div>
151
245
  </ModalOverlay>
152
246
  )
153
247
  }
154
248
 
155
- // === About Modal ===
156
- function SettingsModal({ onClose, addToast }) {
249
+ // === 设置弹窗 ===
250
+ function SettingsModal({ onClose, addToast, isDarkMode, handleShutdown }) {
157
251
  const [dataPath, setStoragePath] = useState('')
158
252
  const [originalPath, setOriginalPath] = useState('')
159
253
  const [isDefault, setIsDefault] = useState(false)
@@ -201,93 +295,78 @@ function SettingsModal({ onClose, addToast }) {
201
295
 
202
296
  return (
203
297
  <ModalOverlay onClose={onClose}>
204
- <div style={{ width: 420, padding: 28, borderRadius: 16, background: '#fff' }} onClick={e => e.stopPropagation()}>
205
- <div style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center', marginBottom: 20 }}>
206
- <h2 style={{ fontSize: 18, fontWeight: 600 }}>设置</h2>
207
- <button onClick={onClose} style={{ background: 'none', border: 'none', cursor: 'pointer', color: '#94a3b8' }}><X size={18} /></button>
298
+ <div className="settings-modal" onClick={e => e.stopPropagation()}>
299
+ <div className="modal-header">
300
+ <h2 className="settings-title">设置</h2>
301
+ <button onClick={onClose} className="modal-close-btn"><X size={18} /></button>
208
302
  </div>
209
303
 
210
304
  <div style={{ marginBottom: 20 }}>
211
- <label style={{ display: 'block', fontSize: 13, fontWeight: 500, marginBottom: 8, color: '#374151' }}>存储位置</label>
212
- <div style={{ display: 'flex', gap: 8, alignItems: 'center' }}>
305
+ <label className="settings-label">存储位置</label>
306
+ <div className="settings-row">
213
307
  <input
214
308
  type="text"
215
309
  value={dataPath}
216
310
  onChange={(e) => setStoragePath(e.target.value)}
217
- placeholder="输入完整路径,如 D:\most-data"
311
+ placeholder=" D:\most-data"
218
312
  disabled={loading}
219
- style={{ flex: 1, padding: '10px 12px', borderRadius: 8, border: '1.5px solid #e5e7eb', fontSize: 13, outline: 'none' }}
313
+ className="settings-input"
220
314
  />
221
- <button onClick={handleSavePath} disabled={saving || loading || !isPathChanged} style={{ padding: '10px 16px', borderRadius: 8, border: 'none', background: '#3b82f6', color: '#fff', cursor: saving || loading || !isPathChanged ? 'not-allowed' : 'pointer', fontSize: 13, fontWeight: 500, whiteSpace: 'nowrap', opacity: saving || loading || !isPathChanged ? 0.5 : 1 }}>
315
+ <button onClick={handleSavePath} disabled={saving || loading || !isPathChanged} className="btn primary" style={{ whiteSpace: 'nowrap', opacity: saving || loading || !isPathChanged ? 0.5 : 1 }}>
222
316
  {saving ? '保存中...' : '保存'}
223
317
  </button>
224
318
  {!isDefault && (
225
- <button onClick={handleResetPath} disabled={saving || loading} style={{ padding: '10px 12px', borderRadius: 8, border: '1px solid #e5e7eb', background: '#fff', color: '#6b7280', cursor: saving || loading ? 'not-allowed' : 'pointer', fontSize: 13, whiteSpace: 'nowrap', opacity: saving || loading ? 0.5 : 1 }}>
319
+ <button onClick={handleResetPath} disabled={saving || loading} className="btn secondary" style={{ whiteSpace: 'nowrap', opacity: saving || loading ? 0.5 : 1 }}>
226
320
  恢复默认
227
321
  </button>
228
322
  )}
229
323
  </div>
230
- <p style={{ fontSize: 11, color: '#9ca3af', marginTop: 8 }}>重启后生效</p>
324
+ <p className="settings-hint">修改后需重启应用</p>
231
325
  </div>
232
326
 
233
- <div style={{ borderTop: '1px solid #e5e7eb', paddingTop: 16 }}>
234
- <div style={{ textAlign: 'center', marginBottom: 16 }}>
235
- <h3 style={{ fontSize: 16, fontWeight: 600, marginBottom: 4 }}>MostBox</h3>
236
- <p style={{ fontSize: 12, color: '#9ca3af' }}>版本 0.0.1</p>
327
+ <div className="settings-divider">
328
+ <div className="settings-about">
329
+ <h3>MostBox</h3>
330
+ <p>版本 0.0.1</p>
237
331
  </div>
238
- <p style={{ fontSize: 12, color: '#6b7280', textAlign: 'center' }}>Hyperswarm · Hyperdrive · IPFS</p>
332
+ <p style={{ fontSize: 12, textAlign: 'center', color: 'var(--text-secondary)' }}>Hyperswarm · Hyperdrive · IPFS</p>
239
333
  </div>
240
334
 
241
- <button onClick={onClose} style={{ width: '100%', marginTop: 20, padding: 10, borderRadius: 10, border: 'none', background: '#3b82f6', color: '#fff', cursor: 'pointer', fontSize: 14 }}>
242
- 关闭
335
+ <button onClick={() => { onClose(); handleShutdown(); }} className="btn danger full" style={{ marginTop: 20 }}>
336
+ <Power size={16} /> 关闭服务
243
337
  </button>
244
338
  </div>
245
339
  </ModalOverlay>
246
340
  )
247
341
  }
248
342
 
249
- // === Toast ===
250
- const TOAST_COLORS = { success: '#22c55e', error: '#ef4444', warning: '#f59e0b', info: '#3b82f6' }
251
-
252
343
  function Toast({ message, type, onDone, index }) {
253
344
  useEffect(() => {
254
345
  const t = setTimeout(onDone, 3000)
255
346
  return () => clearTimeout(t)
256
347
  }, [])
257
348
  return (
258
- <div style={{
259
- position: 'fixed', bottom: 24 + index * 60, right: 24, zIndex: 9999,
260
- background: TOAST_COLORS[type] || TOAST_COLORS.info, color: '#fff',
261
- padding: '12px 20px', borderRadius: 12, fontSize: 13, fontWeight: 500,
262
- boxShadow: '0 4px 16px rgba(0,0,0,0.2)',
263
- animation: 'toastSlideIn 0.2s ease'
264
- }}>
349
+ <div className={`toast ${type}`} style={{ bottom: 24 + index * 60 }}>
265
350
  {message}
266
351
  </div>
267
352
  )
268
353
  }
269
354
 
270
- // === Modal Overlay ===
271
- function ModalOverlay({ children, onClose }) {
355
+ // === 遮罩层 ===
356
+ function ModalOverlay({ children, onClose, closeOnOverlayClick = false }) {
357
+ const handleOverlayClick = (e) => {
358
+ if (closeOnOverlayClick && e.target === e.currentTarget) {
359
+ onClose?.()
360
+ }
361
+ }
272
362
  return (
273
- <div style={{ position: 'fixed', inset: 0, zIndex: 200, background: 'rgba(0,0,0,0.5)', backdropFilter: 'blur(4px)', display: 'flex', alignItems: 'center', justifyContent: 'center' }} onClick={onClose}>
363
+ <div className="modal-overlay" onClick={handleOverlayClick}>
274
364
  {children}
275
365
  </div>
276
366
  )
277
367
  }
278
368
 
279
- // === Shared Styles ===
280
- const iconContainerStyle = {
281
- width: 56, height: 56, borderRadius: 12, marginBottom: 10,
282
- display: 'flex', alignItems: 'center', justifyContent: 'center'
283
- }
284
-
285
- const textEllipsisStyle = {
286
- fontSize: 12, fontWeight: 500, textAlign: 'center', maxWidth: '100%',
287
- overflow: 'hidden', textOverflow: 'ellipsis', whiteSpace: 'nowrap'
288
- }
289
-
290
- // === Breadcrumb Generator ===
369
+ // === 面包屑生成器 ===
291
370
  function generateBreadcrumbs(currentPath) {
292
371
  if (!currentPath) return []
293
372
  return [
@@ -299,18 +378,14 @@ function generateBreadcrumbs(currentPath) {
299
378
  ]
300
379
  }
301
380
 
302
- // === Refresh Handler Factory ===
381
+ // === 刷新处理器工厂 ===
303
382
  const createRefreshHandler = (setter, apiMethod) => async () => {
304
383
  try { setter(await apiMethod()) }
305
384
  catch (err) { console.error(err) }
306
385
  }
307
386
 
308
- // === File Card ===
387
+ // === 文件卡片 ===
309
388
  function FileCard({ file, isSelected, isDarkMode, onSelect, onPreview }) {
310
- const bgSecondary = isDarkMode ? '#1e293b' : '#ffffff'
311
- const accentBlue = isDarkMode ? '#60a5fa' : '#3b82f6'
312
- const borderColor = isDarkMode ? 'rgba(255,255,255,0.08)' : 'rgba(0,0,0,0.06)'
313
- const textColor = isDarkMode ? '#e5e7eb' : '#111827'
314
389
  const subtype = getFileSubtype(file.fileName)
315
390
 
316
391
  return (
@@ -318,179 +393,161 @@ function FileCard({ file, isSelected, isDarkMode, onSelect, onPreview }) {
318
393
  data-id={file.cid}
319
394
  onClick={() => onSelect(file.cid)}
320
395
  onDoubleClick={() => onPreview(file)}
321
- style={{
322
- display: 'flex', flexDirection: 'column', alignItems: 'center',
323
- padding: 16, borderRadius: 12, cursor: 'pointer',
324
- background: isSelected ? accentBlue + '15' : bgSecondary,
325
- border: `1px solid ${isSelected ? accentBlue : borderColor}`,
326
- transition: 'all 0.15s'
327
- }}
396
+ className={`card ${isSelected ? 'selected' : ''}`}
328
397
  >
329
- <div style={{
330
- ...iconContainerStyle,
331
- background: file.starred ? 'linear-gradient(135deg, #fbbf24 0%, #f59e0b 100%)' : 'linear-gradient(135deg, #a78bfa 0%, #8b5cf6 100%)'
332
- }}>
398
+ <div className={`card-icon ${file.starred ? 'starred' : 'file'}`}>
333
399
  {subtype === 'image' && <ImageIcon size={24} color="#fff" />}
334
400
  {subtype === 'video' && <Film size={24} color="#fff" />}
335
401
  {subtype === 'audio' && <Music size={24} color="#fff" />}
336
402
  {subtype === 'file' && <FileText size={24} color="#fff" />}
337
403
  </div>
338
- <p style={{ ...textEllipsisStyle, color: textColor }}>
404
+ <p className="card-name">
339
405
  {parseName(file.fileName).name}
340
406
  </p>
341
407
  </div>
342
408
  )
343
409
  }
344
410
 
345
- // === Folder Card ===
411
+ // === 文件夹卡片 ===
346
412
  function FolderCard({ folder, isDarkMode, onClick }) {
347
- const bgSecondary = isDarkMode ? '#1e293b' : '#ffffff'
348
- const borderColor = isDarkMode ? 'rgba(255,255,255,0.08)' : 'rgba(0,0,0,0.06)'
349
- const textColor = isDarkMode ? '#e5e7eb' : '#111827'
350
-
351
413
  return (
352
414
  <div
353
415
  onClick={onClick}
354
- style={{
355
- display: 'flex', flexDirection: 'column', alignItems: 'center',
356
- padding: 16, borderRadius: 12, cursor: 'pointer',
357
- background: bgSecondary,
358
- border: `1px solid ${borderColor}`,
359
- transition: 'all 0.15s'
360
- }}
416
+ className="card"
361
417
  >
362
- <div style={{ ...iconContainerStyle, background: 'linear-gradient(135deg, #818cf8 0%, #6366f1 100%)' }}>
418
+ <div className="card-icon folder">
363
419
  <Folder size={28} color="#fff" />
364
420
  </div>
365
- <p style={{ ...textEllipsisStyle, color: textColor }}>
421
+ <p className="card-name">
366
422
  {folder.name}
367
423
  </p>
368
424
  </div>
369
425
  )
370
426
  }
371
427
 
372
- // === Confirm Modal ===
373
- function ConfirmModal({ title, message, confirmText, onConfirm, onClose, danger }) {
374
- const isDarkMode = false
375
- const bgSecondary = '#ffffff'
376
- const bgTertiary = '#f1f5f9'
377
- const textPrimary = '#0f172a'
378
- const textSecondary = '#64748b'
379
- const borderColor = 'rgba(0,0,0,0.06)'
428
+ // === 确认弹窗 ===
429
+ function ConfirmModal({ title, message, confirmText, onConfirm, onClose, danger, isDarkMode, closeOnOverlayClick }) {
380
430
  return (
381
- <ModalOverlay onClose={onClose}>
382
- <div style={{ width: 360, padding: 24, borderRadius: 16, background: bgSecondary, border: `1px solid ${borderColor}` }} onClick={e => e.stopPropagation()}>
383
- <h3 style={{ fontSize: 16, fontWeight: 600, marginBottom: 12 }}>{title}</h3>
384
- <p style={{ fontSize: 13, color: textSecondary, marginBottom: 20 }}>{message}</p>
385
- <div style={{ display: 'flex', gap: 8 }}>
386
- <button onClick={onClose} style={{ flex: 1, padding: 10, borderRadius: 8, border: `1px solid ${borderColor}`, background: 'transparent', cursor: 'pointer', fontSize: 13 }}>取消</button>
387
- <button onClick={onConfirm} style={{ flex: 1, padding: 10, borderRadius: 8, border: 'none', background: danger ? '#ef4444' : '#3b82f6', color: '#fff', cursor: 'pointer', fontSize: 13, fontWeight: 500 }}>{confirmText}</button>
431
+ <ModalOverlay onClose={onClose} closeOnOverlayClick={closeOnOverlayClick}>
432
+ <div className="confirm-modal" onClick={e => e.stopPropagation()}>
433
+ <h3>{title}</h3>
434
+ <p>{message}</p>
435
+ <div className="modal-actions">
436
+ <button onClick={onClose} className="btn secondary">取消</button>
437
+ <button onClick={onConfirm} className={`btn ${danger ? 'danger' : 'primary'}`}>{confirmText}</button>
388
438
  </div>
389
439
  </div>
390
440
  </ModalOverlay>
391
441
  )
392
442
  }
393
443
 
394
- // === Input Modal ===
395
- function InputModal({ title, placeholder, defaultValue, confirmText, onConfirm, onClose }) {
444
+ // === 输入弹窗 ===
445
+ function InputModal({ title, placeholder, defaultValue, confirmText, onConfirm, onClose, isDarkMode, isLoading, loadingText }) {
396
446
  const [value, setValue] = useState(defaultValue || '')
397
- const bgSecondary = '#ffffff'
398
- const bgTertiary = '#f1f5f9'
399
- const textPrimary = '#0f172a'
400
- const textSecondary = '#64748b'
401
- const borderColor = 'rgba(0,0,0,0.06)'
402
447
  return (
403
448
  <ModalOverlay onClose={onClose}>
404
- <div style={{ width: 360, padding: 24, borderRadius: 16, background: bgSecondary, border: `1px solid ${borderColor}` }} onClick={e => e.stopPropagation()}>
405
- <h3 style={{ fontSize: 16, fontWeight: 600, marginBottom: 16 }}>{title}</h3>
449
+ <div className="input-modal" onClick={e => e.stopPropagation()}>
450
+ <h3>{title}</h3>
406
451
  <input
407
452
  type="text"
408
453
  value={value}
409
454
  onChange={(e) => setValue(e.target.value)}
410
455
  placeholder={placeholder}
411
456
  autoFocus
412
- onKeyDown={(e) => { if (e.key === 'Enter' && value.trim()) onConfirm(value.trim()) }}
413
- style={{ width: '100%', padding: '10px 12px', borderRadius: 8, border: `1.5px solid ${borderColor}`, fontSize: 13, outline: 'none', background: bgTertiary, color: textPrimary, marginBottom: 16 }}
457
+ onKeyDown={(e) => { if (e.key === 'Enter' && value.trim() && !isLoading) onConfirm(value.trim()) }}
458
+ className="modal-input"
414
459
  />
415
- <div style={{ display: 'flex', gap: 8 }}>
416
- <button onClick={onClose} style={{ flex: 1, padding: 10, borderRadius: 8, border: `1px solid ${borderColor}`, background: 'transparent', cursor: 'pointer', fontSize: 13 }}>取消</button>
417
- <button onClick={() => value.trim() && onConfirm(value.trim())} disabled={!value.trim()} style={{ flex: 1, padding: 10, borderRadius: 8, border: 'none', background: value.trim() ? '#3b82f6' : bgTertiary, color: value.trim() ? '#fff' : textSecondary, cursor: value.trim() ? 'pointer' : 'not-allowed', fontSize: 13, fontWeight: 500 }}>{confirmText}</button>
460
+ <div className="modal-actions">
461
+ <button onClick={onClose} disabled={isLoading} className="btn secondary">取消</button>
462
+ <button onClick={() => value.trim() && onConfirm(value.trim())} disabled={!value.trim() || isLoading} className="btn primary" style={{ opacity: isLoading ? 0.7 : 1 }}>{isLoading ? (loadingText || '处理中...') : confirmText}</button>
418
463
  </div>
419
464
  </div>
420
465
  </ModalOverlay>
421
466
  )
422
467
  }
423
468
 
424
- // === Move Modal ===
469
+ // === 移动弹窗 ===
425
470
  function MoveModal({ items, allFolders, currentPath, isDarkMode, onMove, onClose }) {
426
471
  const [targetPath, setTargetPath] = useState('')
427
- const bgSecondary = isDarkMode ? '#1e293b' : '#ffffff'
428
- const bgTertiary = isDarkMode ? '#334155' : '#f1f5f9'
429
- const textPrimary = isDarkMode ? '#f8fafc' : '#0f172a'
430
- const textSecondary = isDarkMode ? '#94a3b8' : '#64748b'
431
- const borderColor = isDarkMode ? 'rgba(255,255,255,0.08)' : 'rgba(0,0,0,0.06)'
432
- const accentBlue = isDarkMode ? '#60a5fa' : '#3b82f6'
472
+ const [customPath, setCustomPath] = useState(currentPath)
433
473
 
434
474
  const breadcrumbParts = generateBreadcrumbs(targetPath)
435
475
 
476
+ const handleConfirm = () => {
477
+ const finalPath = targetPath || customPath.trim()
478
+ onMove(finalPath)
479
+ }
480
+
436
481
  return (
437
482
  <ModalOverlay onClose={onClose}>
438
- <div style={{ width: 400, padding: 24, borderRadius: 16, background: bgSecondary, border: `1px solid ${borderColor}` }} onClick={e => e.stopPropagation()}>
439
- <div style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center', marginBottom: 16 }}>
440
- <h3 style={{ fontSize: 16, fontWeight: 600 }}>移动到</h3>
441
- <button onClick={onClose} style={{ background: 'none', border: 'none', cursor: 'pointer', color: textSecondary }}><X size={18} /></button>
483
+ <div className="move-modal" onClick={e => e.stopPropagation()}>
484
+ <div className="modal-header">
485
+ <h3>移动到</h3>
486
+ <button onClick={onClose} className="modal-close-btn"><X size={18} /></button>
442
487
  </div>
443
- <p style={{ fontSize: 12, color: textSecondary, marginBottom: 12 }}>已选 {items.length} 个项目</p>
444
- <div style={{ display: 'flex', gap: 6, marginBottom: 12, flexWrap: 'wrap' }}>
488
+ <p style={{ fontSize: 12, color: 'var(--text-secondary)', marginBottom: 12 }}>已选 {items.length} 个项目</p>
489
+ <div className="move-new-folder">
490
+ <input
491
+ type="text"
492
+ value={customPath}
493
+ onChange={(e) => setCustomPath(e.target.value)}
494
+ placeholder="输入路径创建嵌套文件夹"
495
+ />
496
+ </div>
497
+ <p style={{ fontSize: 11, color: 'var(--text-muted)', marginBottom: 12 }}>如 图片/壁纸</p>
498
+ <div className="move-breadcrumb">
445
499
  {breadcrumbParts.map((part, i) => (
446
500
  <React.Fragment key={part.path}>
447
- {i > 0 && <span style={{ color: textSecondary }}>/</span>}
501
+ {i > 0 && <span style={{ color: 'var(--text-secondary)' }}>/</span>}
448
502
  <button
449
503
  key={part.path}
450
- onClick={() => setTargetPath(part.path)}
451
- style={{
452
- padding: '4px 8px', borderRadius: 6, border: 'none',
453
- background: targetPath === part.path ? accentBlue + '20' : bgTertiary,
454
- color: targetPath === part.path ? accentBlue : textSecondary,
455
- cursor: 'pointer', fontSize: 12
504
+ onClick={() => {
505
+ setTargetPath(part.path)
506
+ setCustomPath(part.path)
456
507
  }}
508
+ className={`move-breadcrumb-btn ${targetPath === part.path && !customPath ? 'active' : ''}`}
457
509
  >
458
510
  {part.name}
459
511
  </button>
460
512
  </React.Fragment>
461
513
  ))}
462
514
  </div>
463
- <div style={{ maxHeight: 200, overflow: 'auto', marginBottom: 16 }}>
464
- {allFolders.filter(f => f.path.startsWith(targetPath + (targetPath ? '/' : ''))).length === 0 && targetPath !== '' && (
465
- <p style={{ fontSize: 12, color: textSecondary, textAlign: 'center', padding: 16 }}>该目录下没有子文件夹</p>
466
- )}
467
- {allFolders.filter(f => f.path.startsWith(targetPath + (targetPath ? '/' : ''))).map(folder => (
515
+ <div className="move-folder-list">
516
+ {allFolders.filter(f => {
517
+ if (targetPath === '') {
518
+ return !f.path.includes('/')
519
+ }
520
+ const prefix = targetPath + '/'
521
+ if (!f.path.startsWith(prefix)) return false
522
+ const relativePath = f.path.substring(prefix.length)
523
+ return !relativePath.includes('/')
524
+ }).length === 0 && (
525
+ <p style={{ fontSize: 12, color: 'var(--text-secondary)', textAlign: 'center', padding: 16 }}>该目录下没有子文件夹</p>
526
+ )}
527
+ {allFolders.filter(f => {
528
+ if (targetPath === '') {
529
+ return !f.path.includes('/')
530
+ }
531
+ const prefix = targetPath + '/'
532
+ if (!f.path.startsWith(prefix)) return false
533
+ const relativePath = f.path.substring(prefix.length)
534
+ return !relativePath.includes('/')
535
+ }).map(folder => (
468
536
  <button
469
537
  key={folder.path}
470
538
  onClick={() => setTargetPath(folder.path)}
471
- style={{
472
- display: 'flex', alignItems: 'center', gap: 8, width: '100%', padding: '8px 12px',
473
- borderRadius: 8, border: 'none', background: 'transparent',
474
- cursor: 'pointer', color: textPrimary, fontSize: 13
475
- }}
539
+ className={`move-folder-item ${targetPath === folder.path && !customPath ? 'selected' : ''}`}
476
540
  >
477
- <Folder size={16} color="#6366f1" />
541
+ <Folder size={16} />
478
542
  <span>{folder.name}</span>
479
543
  </button>
480
544
  ))}
481
545
  </div>
482
- <div style={{ display: 'flex', gap: 8 }}>
483
- <button onClick={onClose} style={{ flex: 1, padding: 10, borderRadius: 8, border: `1px solid ${borderColor}`, background: 'transparent', cursor: 'pointer', fontSize: 13 }}>取消</button>
546
+ <div className="modal-actions">
547
+ <button onClick={onClose} className="btn secondary">取消</button>
484
548
  <button
485
- onClick={() => onMove(targetPath)}
486
- disabled={targetPath === currentPath}
487
- style={{
488
- flex: 1, padding: 10, borderRadius: 8, border: 'none',
489
- background: targetPath === currentPath ? bgTertiary : accentBlue,
490
- color: targetPath === currentPath ? textSecondary : '#fff',
491
- cursor: targetPath === currentPath ? 'not-allowed' : 'pointer',
492
- fontSize: 13, fontWeight: 500
493
- }}
549
+ onClick={handleConfirm}
550
+ className="btn primary"
494
551
  >
495
552
  移动
496
553
  </button>
@@ -500,7 +557,7 @@ function MoveModal({ items, allFolders, currentPath, isDarkMode, onMove, onClose
500
557
  )
501
558
  }
502
559
 
503
- // === Main App ===
560
+ // === 主应用 ===
504
561
  export default function App() {
505
562
  const [items, setItems] = useState([])
506
563
  const [trashItems, setTrashItems] = useState([])
@@ -523,9 +580,54 @@ export default function App() {
523
580
  const [isMoveModalOpen, setIsMoveModalOpen] = useState(false)
524
581
  const [confirmModal, setConfirmModal] = useState(null)
525
582
  const [inputModal, setInputModal] = useState(null)
583
+ const [inputLoading, setInputLoading] = useState(false)
526
584
  const [renameTarget, setRenameTarget] = useState(null)
527
585
  const [showWelcome, setShowWelcome] = useState(() => !localStorage.getItem('mostbox_welcomed'))
528
586
  const [showSettings, setShowSettings] = useState(false)
587
+ const [isSidebarOpen, setIsSidebarOpen] = useState(false)
588
+ const [previewText, setPreviewText] = useState('')
589
+ const [previewLoading, setPreviewLoading] = useState(false)
590
+ const [previewMediaLoading, setPreviewMediaLoading] = useState(false)
591
+ const [previewLoaded, setPreviewLoaded] = useState(false)
592
+ const previewMediaRef = useRef(null)
593
+ const previewTextRef = useRef(null)
594
+
595
+ useEffect(() => {
596
+ if (previewItem && (previewItem.subtype === 'image' || previewItem.subtype === 'video')) {
597
+ setPreviewMediaLoading(true)
598
+ setPreviewLoaded(false)
599
+ }
600
+ if (previewItem && previewItem.subtype === 'text') {
601
+ setPreviewText('')
602
+ loadPreviewText(previewItem.cid)
603
+ }
604
+ }, [previewItem?.cid])
605
+
606
+ useEffect(() => {
607
+ const media = previewMediaRef.current
608
+ if (!media) return
609
+
610
+ const handleLoad = () => setPreviewMediaLoading(false)
611
+ const handleError = () => setPreviewMediaLoading(false)
612
+
613
+ if (previewItem?.subtype === 'image') {
614
+ if (media.complete) {
615
+ setPreviewMediaLoading(false)
616
+ } else {
617
+ media.addEventListener('load', handleLoad)
618
+ media.addEventListener('error', handleError)
619
+ }
620
+ } else if (previewItem?.subtype === 'video') {
621
+ media.addEventListener('canplay', handleLoad)
622
+ media.addEventListener('error', handleError)
623
+ }
624
+
625
+ return () => {
626
+ media.removeEventListener('load', handleLoad)
627
+ media.removeEventListener('error', handleError)
628
+ media.removeEventListener('canplay', handleLoad)
629
+ }
630
+ }, [previewItem])
529
631
 
530
632
  const currentPath = currentFolderId || ''
531
633
  const allFolders = getUniqueFolders(items)
@@ -654,6 +756,7 @@ export default function App() {
654
756
  if (isTrash) {
655
757
  await API.permanentDeleteTrashFile(id)
656
758
  } else {
759
+ // 跳过文件夹 ID(以 '__' 前缀标识),只删除文件
657
760
  if (!id.startsWith('__')) await API.deletePublishedFile(id)
658
761
  }
659
762
  }
@@ -694,19 +797,30 @@ export default function App() {
694
797
  defaultValue: currentName,
695
798
  confirmText: '重命名',
696
799
  onConfirm: async (newName) => {
697
- setInputModal(null)
698
800
  if (newName === currentName) return
801
+ setInputLoading(true)
699
802
  try {
700
803
  if (isFolder) {
701
- await API.renameFolder(target.path, newName)
804
+ const lastSlash = target.path.lastIndexOf('/')
805
+ const parentPath = lastSlash !== -1 ? target.path.substring(0, lastSlash) : ''
806
+ const newPath = parentPath ? `${parentPath}/${newName}` : newName
807
+ await API.renameFolder(target.path, newPath)
808
+ addToast('已重命名', 'success')
809
+ refreshFiles()
810
+ handleNavigate(newPath)
702
811
  } else {
703
812
  const { folder } = parseName(target.fileName)
704
813
  const newFileName = folder ? `${folder}/${newName}` : newName
705
814
  await API.moveFile(target.cid, newFileName)
815
+ addToast('已重命名', 'success')
816
+ refreshFiles()
706
817
  }
707
- addToast('已重命名', 'success')
708
- refreshFiles()
709
- } catch { addToast('重命名失败', 'error') }
818
+ setInputModal(null)
819
+ } catch {
820
+ addToast('重命名失败', 'error')
821
+ } finally {
822
+ setInputLoading(false)
823
+ }
710
824
  }
711
825
  })
712
826
  }
@@ -718,7 +832,14 @@ export default function App() {
718
832
  for (const file of Array.from(files)) {
719
833
  const fileName = prefix + file.name
720
834
 
721
- // Create transfer entry for progress tracking
835
+ // 检查完整路径是否已存在
836
+ const nameExists = items.some(item => item.fileName === fileName)
837
+ if (nameExists) {
838
+ addToast(`${file.name} 已存在`, 'warning')
839
+ continue
840
+ }
841
+
842
+ // 创建传输条目用于进度跟踪
722
843
  const transferId = `upload_${Date.now()}_${Math.random().toString(36).slice(2, 8)}`
723
844
  const transfer = {
724
845
  id: transferId,
@@ -730,7 +851,7 @@ export default function App() {
730
851
  newTransfers.push(transfer)
731
852
  setTransfers(prev => [...prev, transfer])
732
853
 
733
- // Open transfer panel if there are new transfers
854
+ // 有新传输时打开传输面板
734
855
  if (newTransfers.length > 0) {
735
856
  setIsTransferPanelOpen(true)
736
857
  }
@@ -738,13 +859,13 @@ export default function App() {
738
859
  try {
739
860
  const result = await API.publishFile(file, fileName)
740
861
  if (result.alreadyExists) {
741
- // Update transfer status
862
+ // 更新传输状态
742
863
  setTransfers(prev => prev.map(t =>
743
864
  t.id === transferId ? { ...t, status: 'completed' } : t
744
865
  ))
745
866
  addToast(`${file.name} 已存在`, 'warning')
746
867
  } else {
747
- // Update transfer status
868
+ // 更新传输状态
748
869
  setTransfers(prev => prev.map(t =>
749
870
  t.id === transferId ? { ...t, progress: 100, status: 'completed' } : t
750
871
  ))
@@ -758,39 +879,28 @@ export default function App() {
758
879
  }
759
880
  }
760
881
 
761
- // Remove completed transfers after a delay
882
+ // 延迟移除已完成的传输
762
883
  setTimeout(() => {
763
- setTransfers(prev => prev.filter(t => t.status === 'uploading'))
884
+ setTransfers(prev => prev.filter(t => t.status !== 'completed' && t.status !== 'error' && t.status !== 'cancelled'))
764
885
  }, 3000)
765
886
 
766
887
  refreshFiles()
767
888
  refreshStorageStats()
768
889
  }
769
890
 
770
- const createNewFolder = () => {
771
- setInputModal({
772
- title: '新建文件夹',
773
- placeholder: '请输入文件夹名称',
774
- confirmText: '创建',
775
- onConfirm: async (folderPath) => {
776
- setInputModal(null)
777
- const exists = items.some(f =>
778
- f.fileName === folderPath ||
779
- f.fileName.startsWith(folderPath + '/')
780
- )
781
- if (exists) {
782
- addToast('文件夹已存在', 'warning')
783
- return
784
- }
785
- try {
786
- const randomContent = Math.random().toString(36).substring(2, 10)
787
- const content = new File([randomContent], 'hello.txt', { type: 'text/plain' })
788
- await API.publishFile(content, `${folderPath}/hello.txt`)
789
- addToast('文件夹已创建', 'success')
790
- refreshFiles()
791
- } catch { addToast('创建失败', 'error') }
792
- }
793
- })
891
+ const loadPreviewText = async (cid) => {
892
+ setPreviewLoading(true)
893
+ try {
894
+ const res = await fetch(`/api/files/${cid}/download`, {
895
+ headers: { 'Range': 'bytes=0-9999' }
896
+ })
897
+ if (!res.ok) throw new Error('加载失败')
898
+ const text = await res.text()
899
+ setPreviewText(text || '(文件为空)')
900
+ } catch {
901
+ setPreviewText('加载失败')
902
+ }
903
+ setPreviewLoading(false)
794
904
  }
795
905
 
796
906
  const handleCopyLink = () => {
@@ -813,7 +923,7 @@ export default function App() {
813
923
  const result = await API.downloadFile(downloadLink)
814
924
  setDownloadLink('')
815
925
  setIsDownloadModalOpen(false)
816
-
926
+
817
927
  if (result.alreadyExists) {
818
928
  addToast(`${result.fileName} 已存在`, 'warning')
819
929
  } else {
@@ -822,7 +932,7 @@ export default function App() {
822
932
  fileName: '下载文件',
823
933
  progress: 0,
824
934
  type: 'download',
825
- status: 'uploading'
935
+ status: 'downloading'
826
936
  }
827
937
  setTransfers(prev => [...prev, transfer])
828
938
  setIsTransferPanelOpen(true)
@@ -834,18 +944,49 @@ export default function App() {
834
944
  setIsDownloading(false)
835
945
  }
836
946
  }
837
-
947
+
838
948
  const handleCancelTransfer = async (transfer) => {
839
- if (transfer.type === 'download' && transfer.status === 'uploading') {
949
+ if (transfer.type === 'download' && transfer.status === 'downloading') {
840
950
  try {
841
951
  await API.cancelDownload(transfer.id)
842
- // The WebSocket will handle the 'download:cancelled' event
952
+ // WebSocket 会处理 'download:cancelled' 事件
843
953
  } catch (err) {
844
954
  addToast('取消失败', 'error')
845
955
  }
846
956
  }
847
957
  }
848
958
 
959
+ const handleSaveAs = async (file) => {
960
+ try {
961
+ const res = await fetch(API.getFileDownloadUrl(file.cid))
962
+ if (!res.ok) throw new Error('获取文件失败')
963
+ const blob = await res.blob()
964
+ if (window.showSaveFilePicker) {
965
+ const handle = await window.showSaveFilePicker({
966
+ suggestedName: file.fileName
967
+ })
968
+ const writable = await handle.createWritable()
969
+ await writable.write(blob)
970
+ await writable.close()
971
+ addToast('文件已保存', 'success')
972
+ } else {
973
+ const url = URL.createObjectURL(blob)
974
+ const a = document.createElement('a')
975
+ a.href = url
976
+ a.download = file.fileName
977
+ document.body.appendChild(a)
978
+ a.click()
979
+ document.body.removeChild(a)
980
+ URL.revokeObjectURL(url)
981
+ addToast('文件已下载', 'success')
982
+ }
983
+ } catch (err) {
984
+ if (err.name !== 'AbortError') {
985
+ addToast('保存失败: ' + err.message, 'error')
986
+ }
987
+ }
988
+ }
989
+
849
990
  const handleNavigate = (path) => {
850
991
  setCurrentFolderId(path || null)
851
992
  setSelectedIds([])
@@ -872,7 +1013,7 @@ export default function App() {
872
1013
  })
873
1014
  }
874
1015
 
875
- // WebSocket
1016
+ // WebSocket 连接
876
1017
  useEffect(() => {
877
1018
  const protocol = location.protocol === 'https:' ? 'wss:' : 'ws:'
878
1019
  const ws = new WebSocket(`${protocol}//${location.host}/ws`)
@@ -892,17 +1033,17 @@ export default function App() {
892
1033
  } else {
893
1034
  addToast(`${data.fileName} 下载完成`, 'success')
894
1035
  }
895
- // Remove completed downloads after delay
1036
+ // 延迟移除已完成的下载
896
1037
  setTimeout(() => {
897
1038
  setTransfers(prev => prev.filter(t => !(t.id === taskId && t.status === 'completed')))
898
1039
  }, 3000)
899
1040
  }
900
1041
  }
901
- // Handle publish/upload progress
1042
+ // 处理发布/上传进度
902
1043
  if (event === 'publish:progress') {
903
1044
  setTransfers(prev => prev.map(t => {
904
1045
  if (data.file && t.fileName === data.file && t.type === 'upload') {
905
- // Calculate percent based on stage
1046
+ // 根据阶段计算百分比
906
1047
  let progress = 50
907
1048
  if (data.stage === 'calculating-cid') progress = 25
908
1049
  else if (data.stage === 'uploading') progress = 75
@@ -912,26 +1053,26 @@ export default function App() {
912
1053
  return t
913
1054
  }))
914
1055
  }
915
- // Handle download progress
1056
+ // 处理下载进度
916
1057
  if (event === 'download:progress') {
917
1058
  setTransfers(prev => prev.map(t =>
918
1059
  t.id === data.taskId ? { ...t, progress: data.percent || 0, loaded: data.loaded, total: data.total } : t
919
1060
  ))
920
1061
  }
921
- // Handle download error
1062
+ // 处理下载错误
922
1063
  if (event === 'download:error') {
923
1064
  setTransfers(prev => prev.map(t =>
924
1065
  t.id === data.taskId ? { ...t, status: 'error' } : t
925
1066
  ))
926
1067
  addToast(`下载失败: ${data.error}`, 'error')
927
1068
  }
928
- // Handle download status (includes filename when known)
1069
+ // 处理下载状态(包含文件名)
929
1070
  if (event === 'download:status') {
930
1071
  setTransfers(prev => prev.map(t =>
931
1072
  t.id === data.taskId ? { ...t, fileName: data.file || t.fileName } : t
932
1073
  ))
933
1074
  }
934
- // Handle download cancelled
1075
+ // 处理下载取消
935
1076
  if (event === 'download:cancelled') {
936
1077
  setTransfers(prev => prev.map(t =>
937
1078
  t.id === data.taskId ? { ...t, status: 'cancelled' } : t
@@ -943,7 +1084,7 @@ export default function App() {
943
1084
  return () => ws.close()
944
1085
  }, [])
945
1086
 
946
- // Init
1087
+ // 初始化
947
1088
  useEffect(() => {
948
1089
  const saved = localStorage.getItem('theme')
949
1090
  if (saved === 'dark' || (!saved && window.matchMedia('(prefers-color-scheme: dark)').matches)) {
@@ -955,182 +1096,171 @@ export default function App() {
955
1096
  API.getStorageStats().then(s => setStorageStats(s)).catch(() => { })
956
1097
  }, [])
957
1098
 
958
- // Theme colors
959
- const bgPrimary = isDarkMode ? '#0f172a' : '#f8fafc'
960
- const bgSecondary = isDarkMode ? '#1e293b' : '#ffffff'
961
- const bgTertiary = isDarkMode ? '#334155' : '#f1f5f9'
962
- const textPrimary = isDarkMode ? '#f8fafc' : '#0f172a'
963
- const textSecondary = isDarkMode ? '#94a3b8' : '#64748b'
964
- const textMuted = isDarkMode ? '#64748b' : '#94a3b8'
965
- const borderColor = isDarkMode ? 'rgba(255,255,255,0.08)' : 'rgba(0,0,0,0.06)'
966
- const accentBlue = isDarkMode ? '#60a5fa' : '#3b82f6'
1099
+ // 同步 data-theme 属性
1100
+ useEffect(() => {
1101
+ document.documentElement.setAttribute('data-theme', isDarkMode ? 'dark' : 'light')
1102
+ }, [isDarkMode])
967
1103
 
1104
+ // 主题颜色
968
1105
  const viewTitle = currentView === 'all' ? '全部内容' : currentView === 'starred' ? '收藏' : '回收站'
969
- const displayFiles = currentView === 'all' ? filteredFiles : currentView === 'starred' ? items.filter(i => i.starred) : []
970
- const displayFolders = currentView === 'starred' ? [] : folders
971
-
972
- // Breadcrumb parts
1106
+ const displayFiles = currentView === 'all'
1107
+ ? filteredFiles
1108
+ : currentView === 'starred'
1109
+ ? items.filter(i => i.starred && parseName(i.fileName).name.toLowerCase().includes(searchQuery.toLowerCase()))
1110
+ : trashItems.filter(i => parseName(i.fileName).name.toLowerCase().includes(searchQuery.toLowerCase()))
1111
+ const displayFolders = currentView === 'starred'
1112
+ ? []
1113
+ : folders.filter(f => f.name.toLowerCase().includes(searchQuery.toLowerCase()))
1114
+
1115
+ // 面包屑部分
973
1116
  const breadcrumbParts = generateBreadcrumbs(currentPath)
974
1117
 
975
1118
  return (
976
- <div style={{ display: 'flex', minHeight: '100vh', background: bgPrimary, color: textPrimary }}>
977
- <style>{`
978
- * { margin: 0; padding: 0; box-sizing: border-box; }
979
- body { font-family: -apple-system, BlinkMacSystemFont, "PingFang SC", "Microsoft YaHei", sans-serif; }
980
- ::-webkit-scrollbar { width: 6px; }
981
- ::-webkit-scrollbar-thumb { background: ${textMuted}; border-radius: 4px; }
982
- @keyframes toastSlideIn { from { transform: translateX(100%); opacity: 0; } to { transform: translateX(0); opacity: 1; } }
983
- `}</style>
984
-
985
- {/* Sidebar */}
986
- <div style={{ width: 200, background: bgTertiary, display: 'flex', flexDirection: 'column', borderRight: `1px solid ${borderColor}`, flexShrink: 0 }}>
987
- <div style={{ padding: '20px 16px', borderBottom: `1px solid ${borderColor}` }}>
988
- <h1 style={{ fontSize: 18, fontWeight: 700, color: accentBlue }}>Most.Box</h1>
1119
+ <div className="app-layout">
1120
+ {/* 侧边栏遮罩 */}
1121
+ <div className={`sidebar-overlay ${isSidebarOpen ? 'visible' : ''}`} onClick={() => setIsSidebarOpen(false)} />
1122
+
1123
+ {/* 侧边栏 */}
1124
+ <div className={`sidebar ${isSidebarOpen ? 'open' : ''}`}>
1125
+ <div className="sidebar-header">
1126
+ <h1>Most.Box</h1>
989
1127
  </div>
990
- <nav style={{ padding: '12px 8px', flex: 1 }}>
1128
+ <nav className="sidebar-nav">
991
1129
  {[{ id: 'all', icon: <Files size={18} />, label: '全部内容' }, { id: 'starred', icon: <Star size={18} />, label: '收藏' }, { id: 'trash', icon: <Trash2 size={18} />, label: '回收站' }].map(item => (
992
1130
  <button
993
1131
  key={item.id}
994
- onClick={() => { setCurrentView(item.id); setCurrentFolderId(null); setSelectedIds([]); setSearchQuery('') }}
995
- style={{
996
- width: '100%', display: 'flex', alignItems: 'center', gap: 10,
997
- padding: '10px 12px', borderRadius: 8, border: 'none', cursor: 'pointer',
998
- marginBottom: 4, background: currentView === item.id ? accentBlue + '20' : 'transparent',
999
- color: currentView === item.id ? accentBlue : textSecondary,
1000
- fontWeight: currentView === item.id ? 600 : 500, fontSize: 13
1001
- }}
1132
+ onClick={() => { setCurrentView(item.id); setCurrentFolderId(null); setSelectedIds([]); setSearchQuery(''); setIsSidebarOpen(false) }}
1133
+ className={`sidebar-nav-btn ${currentView === item.id ? 'active' : ''}`}
1002
1134
  >
1003
1135
  {item.icon}
1004
1136
  <span>{item.label}</span>
1005
1137
  </button>
1006
1138
  ))}
1007
1139
  </nav>
1008
- <div style={{ padding: '12px 16px', borderTop: `1px solid ${borderColor}` }}>
1009
- <div style={{ display: 'flex', alignItems: 'center', gap: 6, marginBottom: 6 }}>
1010
- <HardDrive size={14} color={textSecondary} />
1011
- <span style={{ fontSize: 11, color: textSecondary }}>存储空间</span>
1140
+ <div className="sidebar-footer">
1141
+ <div className="sidebar-footer-label">
1142
+ <HardDrive size={14} />
1143
+ <span>存储空间</span>
1012
1144
  </div>
1013
- <div style={{ height: 4, borderRadius: 2, background: bgSecondary, overflow: 'hidden' }}>
1014
- <div style={{ width: `${storageStats.total > 0 ? (storageStats.used / storageStats.total) * 100 : 0}%`, height: '100%', background: accentBlue, transition: 'width 0.3s' }} />
1145
+ <div className="storage-bar">
1146
+ <div className="storage-bar-fill" style={{ width: `${storageStats.total > 0 ? (storageStats.used / storageStats.total) * 100 : 0}%` }} />
1015
1147
  </div>
1016
- <div style={{ display: 'flex', justifyContent: 'space-between', fontSize: 10, color: textSecondary, marginTop: 4 }}>
1148
+ <div className="storage-info">
1017
1149
  <span>{formatSize(storageStats.used)}</span>
1018
1150
  <span>{storageStats.total > 0 ? formatSize(storageStats.total) : '-'}</span>
1019
1151
  </div>
1020
1152
  </div>
1021
1153
  </div>
1022
1154
 
1023
- {/* Main Content */}
1024
- <div style={{ flex: 1, display: 'flex', flexDirection: 'column', minWidth: 0 }}>
1025
- {/* Header */}
1026
- <header style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center', padding: '16px 24px', borderBottom: `1px solid ${borderColor}`, background: bgSecondary, gap: 16 }}>
1027
- <div style={{ display: 'flex', alignItems: 'center', gap: 12 }}>
1028
- <h2 style={{ fontSize: 16, fontWeight: 600 }}>{viewTitle}</h2>
1029
- <div style={{ display: 'flex', alignItems: 'center', gap: 4, padding: '3px 8px', borderRadius: 12, background: bgTertiary, fontSize: 11, color: textSecondary }}>
1030
- <div style={{ width: 6, height: 6, borderRadius: '50%', background: peerCount > 0 ? '#22c55e' : '#f59e0b' }} />
1155
+ {/* 主内容区 */}
1156
+ <div className="main-content">
1157
+ {/* 头部 */}
1158
+ <header className="app-header">
1159
+ <div className="header-left">
1160
+ <button onClick={() => setIsSidebarOpen(true)} className="icon-btn mobile-menu-btn">
1161
+ <Menu size={18} />
1162
+ </button>
1163
+ <h2 className="header-title">{viewTitle}</h2>
1164
+ <div className="header-badge">
1165
+ <div className={`header-badge-dot ${peerCount > 0 ? 'connected' : ''}`} />
1031
1166
  {peerCount > 0 ? `${peerCount} 节点` : '等待连接'}
1032
1167
  </div>
1033
1168
  </div>
1034
- <div style={{ display: 'flex', alignItems: 'center', gap: 8 }}>
1035
- {currentView !== 'trash' && (
1036
- <div style={{ display: 'flex', alignItems: 'center', gap: 8, padding: '6px 12px', borderRadius: 8, background: bgTertiary, border: `1px solid ${borderColor}` }}>
1037
- <Search size={14} color={textSecondary} />
1038
- <input type="text" value={searchQuery} onChange={(e) => setSearchQuery(e.target.value)} placeholder="搜索..." style={{ background: 'none', border: 'none', outline: 'none', fontSize: 12, color: textPrimary, width: 120 }} />
1039
- {searchQuery && <button onClick={() => setSearchQuery('')} style={{ background: 'none', border: 'none', cursor: 'pointer', color: textSecondary, padding: 0 }}><X size={12} /></button>}
1040
- </div>
1041
- )}
1169
+ <div className="header-right">
1170
+ <div className="search-box">
1171
+ <Search size={14} />
1172
+ <input type="text" value={searchQuery} onChange={(e) => setSearchQuery(e.target.value)} placeholder="搜索..." />
1173
+ {searchQuery && <button onClick={() => setSearchQuery('')}><X size={12} /></button>}
1174
+ </div>
1042
1175
  {currentView === 'trash' && trashItems.length > 0 && (
1043
- <button onClick={handleEmptyTrash} style={{ padding: '6px 12px', borderRadius: 8, border: 'none', background: 'rgba(239,68,68,0.1)', color: '#ef4444', fontSize: 12, fontWeight: 500, cursor: 'pointer' }}>
1176
+ <button onClick={handleEmptyTrash} className="btn small" style={{ background: 'rgba(239,68,68,0.1)', color: '#ef4444' }}>
1044
1177
  清空回收站
1045
1178
  </button>
1046
1179
  )}
1047
- <button onClick={() => setIsTransferPanelOpen(true)} style={{ width: 32, height: 32, borderRadius: 8, border: 'none', background: bgTertiary, cursor: 'pointer', color: textSecondary, position: 'relative' }}>
1180
+ <button onClick={() => setIsTransferPanelOpen(true)} className="icon-btn">
1048
1181
  <ArrowUpDown size={16} />
1049
- {transfers.length > 0 && <span style={{ position: 'absolute', top: -4, right: -4, width: 14, height: 14, borderRadius: '50%', background: '#ef4444', color: '#fff', fontSize: 9, fontWeight: 700, display: 'flex', alignItems: 'center', justifyContent: 'center' }}>{transfers.length}</span>}
1050
- </button>
1051
- <button onClick={createNewFolder} style={{ width: 32, height: 32, borderRadius: 8, border: 'none', background: bgTertiary, cursor: 'pointer', color: accentBlue }}>
1052
- <FolderPlus size={16} />
1182
+ {transfers.length > 0 && <span className="icon-btn-badge">{transfers.length}</span>}
1053
1183
  </button>
1054
- <button onClick={() => setIsDarkMode(!isDarkMode)} style={{ width: 32, height: 32, borderRadius: 8, border: 'none', background: bgTertiary, cursor: 'pointer', color: '#6366f1' }}>
1184
+ <button onClick={() => setIsDarkMode(!isDarkMode)} className="icon-btn theme-toggle">
1055
1185
  {isDarkMode ? <Sun size={16} /> : <Moon size={16} />}
1056
1186
  </button>
1057
- <button onClick={handleShutdown} title="关闭服务" style={{ width: 32, height: 32, borderRadius: 8, border: 'none', background: bgTertiary, cursor: 'pointer', color: '#ef4444' }}>
1058
- <Power size={16} />
1059
- </button>
1060
- <button onClick={() => setShowSettings(true)} style={{ width: 32, height: 32, borderRadius: 8, border: 'none', background: bgTertiary, cursor: 'pointer', color: textSecondary }}>
1187
+ <button onClick={() => setShowSettings(true)} className="icon-btn">
1061
1188
  <Info size={16} />
1062
1189
  </button>
1063
1190
  </div>
1064
1191
  </header>
1065
1192
 
1066
- {/* Upload/Download */}
1193
+ {/* 上传/下载 */}
1067
1194
  {currentView === 'all' && (
1068
- <div style={{ display: 'grid', gridTemplateColumns: '1fr 1fr', gap: 12, padding: '16px 24px', position: 'relative' }}>
1069
- <div onDragOver={(e) => { e.preventDefault(); setIsDraggingOverUpload(true) }} onDragLeave={() => setIsDraggingOverUpload(false)} onDrop={(e) => { e.preventDefault(); setIsDraggingOverUpload(false); processFiles(e.dataTransfer.files) }} style={{ border: `2px dashed ${isDraggingOverUpload ? '#3b82f6' : 'rgba(59,130,246,0.2)'}`, borderRadius: 12, padding: 20, textAlign: 'center', cursor: 'pointer', background: isDraggingOverUpload ? 'rgba(59,130,246,0.05)' : 'transparent', position: 'relative' }}>
1070
- <input type="file" multiple onChange={(e) => processFiles(e.target.files)} style={{ position: 'absolute', inset: 0, width: '100%', height: '100%', opacity: 0, cursor: 'pointer', zIndex: 1 }} />
1071
- <Upload size={20} color={accentBlue} style={{ marginBottom: 8 }} />
1072
- <p style={{ fontSize: 12, color: accentBlue, fontWeight: 500 }}>上传文件</p>
1195
+ <div className="action-grid">
1196
+ <div className={`action-card upload ${isDraggingOverUpload ? 'drag-over' : ''}`} onDragOver={(e) => { e.preventDefault(); setIsDraggingOverUpload(true) }} onDragLeave={() => setIsDraggingOverUpload(false)} onDrop={(e) => { e.preventDefault(); setIsDraggingOverUpload(false); processFiles(e.dataTransfer.files) }}>
1197
+ <input type="file" multiple onChange={(e) => processFiles(e.target.files)} className="action-card-input" />
1198
+ <Upload size={20} style={{ marginBottom: 8 }} />
1199
+ <p>上传文件</p>
1073
1200
  </div>
1074
- <div onClick={() => setIsDownloadModalOpen(true)} style={{ border: '2px dashed rgba(99,102,241,0.2)', borderRadius: 12, padding: 20, textAlign: 'center', cursor: 'pointer' }}>
1075
- <Download size={20} color="#6366f1" style={{ marginBottom: 8 }} />
1076
- <p style={{ fontSize: 12, color: '#6366f1', fontWeight: 500 }}>下载文件</p>
1201
+ <div className="action-card action-card-download" onClick={() => setIsDownloadModalOpen(true)}>
1202
+ <Download size={20} style={{ marginBottom: 8 }} />
1203
+ <p>下载文件</p>
1077
1204
  </div>
1078
1205
  </div>
1079
1206
  )}
1080
1207
 
1081
- {/* Breadcrumb */}
1082
- {currentView === 'all' && currentPath && (
1083
- <div style={{ display: 'flex', alignItems: 'center', gap: 4, padding: '0 24px 12px', fontSize: 12 }}>
1084
- <button onClick={() => handleNavigate('')} style={{ background: 'none', border: 'none', color: textMuted, cursor: 'pointer' }}>全部内容</button>
1085
- {breadcrumbParts.slice(1).map((part, i) => (
1086
- <React.Fragment key={part.path}>
1087
- <ChevronRight size={12} color={textMuted} />
1088
- <button onClick={() => handleNavigate(part.path)} style={{ background: 'none', border: 'none', color: i === breadcrumbParts.length - 2 ? textSecondary : textMuted, cursor: 'pointer' }}>{part.name}</button>
1089
- </React.Fragment>
1090
- ))}
1208
+ {/* 面包屑 */}
1209
+ {currentView === 'all' && (
1210
+ <div className="breadcrumb">
1211
+ {currentPath ? (
1212
+ <>
1213
+ <button onClick={() => handleNavigate('')}>全部内容</button>
1214
+ {breadcrumbParts.slice(1).map((part, i) => (
1215
+ <React.Fragment key={part.path}>
1216
+ <ChevronRight size={12} />
1217
+ <button onClick={() => handleNavigate(part.path)} className={i === breadcrumbParts.length - 2 ? 'current' : ''}>{part.name}</button>
1218
+ {i === breadcrumbParts.length - 2 && (
1219
+ <button onClick={() => openRenameModal(part)} className="breadcrumb-edit-btn">
1220
+ <Edit2 size={12} />
1221
+ </button>
1222
+ )}
1223
+ </React.Fragment>
1224
+ ))}
1225
+ </>
1226
+ ) : null}
1091
1227
  </div>
1092
1228
  )}
1093
1229
 
1094
- {/* Content Grid */}
1095
- <div style={{ flex: 1, padding: '0 24px 24px', overflow: 'auto' }}>
1096
- {/* Trash View */}
1230
+ {/* 内容网格 */}
1231
+ <div className="content-grid">
1232
+ {/* 回收站视图 */}
1097
1233
  {currentView === 'trash' && (
1098
- trashItems.length === 0 ? (
1099
- <div style={{ textAlign: 'center', color: textMuted, padding: 48, fontSize: 13 }}>回收站是空的</div>
1234
+ displayFiles.length === 0 ? (
1235
+ <div className="empty-state">{searchQuery ? '未找到相关文件' : '回收站是空的'}</div>
1100
1236
  ) : (
1101
- <div style={{ display: 'grid', gridTemplateColumns: 'repeat(auto-fill, minmax(140px, 1fr))', gap: 16 }}>
1102
- {trashItems.map(f => (
1237
+ <div className="file-grid">
1238
+ {displayFiles.map(f => (
1103
1239
  <div
1104
1240
  key={f.cid}
1105
1241
  onClick={() => setSelectedIds(prev => prev.includes(f.cid) ? prev.filter(id => id !== f.cid) : [...prev, f.cid])}
1106
1242
  onDoubleClick={() => handleRestore(f.cid)}
1107
- style={{
1108
- display: 'flex', flexDirection: 'column', alignItems: 'center',
1109
- padding: 16, borderRadius: 12,
1110
- background: selectedIds.includes(f.cid) ? accentBlue + '15' : bgSecondary,
1111
- border: `1px solid ${selectedIds.includes(f.cid) ? accentBlue : borderColor}`,
1112
- transition: 'all 0.15s', cursor: 'pointer'
1113
- }}
1243
+ className={`card ${selectedIds.includes(f.cid) ? 'selected' : ''}`}
1114
1244
  >
1115
- <div style={{ ...iconContainerStyle, background: 'linear-gradient(135deg, #94a3b8 0%, #64748b 100%)' }}>
1245
+ <div className="card-icon trash">
1116
1246
  <FileText size={24} color="#fff" />
1117
1247
  </div>
1118
- <p style={textEllipsisStyle}>{parseName(f.fileName).name}</p>
1119
- <p style={{ fontSize: 10, color: textMuted, marginTop: 2 }}>删除于 {formatDate(f.deletedAt)}</p>
1248
+ <p className="card-name">{parseName(f.fileName).name}</p>
1249
+ <p className="card-date">删除于 {formatDate(f.deletedAt)}</p>
1120
1250
  </div>
1121
1251
  ))}
1122
1252
  </div>
1123
1253
  )
1124
1254
  )}
1125
1255
 
1126
- {/* All/Starred View */}
1256
+ {/* 全部/收藏视图 */}
1127
1257
  {currentView !== 'trash' && (
1128
1258
  displayFiles.length === 0 && displayFolders.length === 0 ? (
1129
- <div style={{ textAlign: 'center', color: textMuted, padding: 48, fontSize: 13 }}>
1259
+ <div className="empty-state">
1130
1260
  {searchQuery ? '未找到相关文件' : (currentView === 'starred' ? '暂无收藏' : '暂无文件')}
1131
1261
  </div>
1132
1262
  ) : (
1133
- <div style={{ display: 'grid', gridTemplateColumns: 'repeat(auto-fill, minmax(140px, 1fr))', gap: 16 }}>
1263
+ <div className="file-grid">
1134
1264
  {displayFolders.map(folder => (
1135
1265
  <FolderCard key={folder.path} folder={folder} isDarkMode={isDarkMode} onClick={() => handleNavigate(folder.path)} />
1136
1266
  ))}
@@ -1145,30 +1275,35 @@ export default function App() {
1145
1275
 
1146
1276
 
1147
1277
 
1148
- {/* Confirm Modal */}
1278
+ {/* 确认弹窗 */}
1149
1279
  {confirmModal && (
1150
1280
  <ConfirmModal
1151
1281
  title={confirmModal.title}
1152
1282
  message={confirmModal.message}
1153
1283
  confirmText={confirmModal.confirmText}
1154
1284
  danger={confirmModal.danger}
1285
+ isDarkMode={isDarkMode}
1286
+ closeOnOverlayClick={confirmModal.danger}
1155
1287
  onConfirm={confirmModal.onConfirm}
1156
1288
  onClose={() => setConfirmModal(null)}
1157
1289
  />
1158
1290
  )}
1159
1291
 
1160
- {/* Input Modal */}
1292
+ {/* 输入弹窗 */}
1161
1293
  {inputModal && (
1162
1294
  <InputModal
1163
1295
  title={inputModal.title}
1164
1296
  placeholder={inputModal.placeholder}
1297
+ defaultValue={inputModal.defaultValue}
1165
1298
  confirmText={inputModal.confirmText}
1299
+ isDarkMode={isDarkMode}
1300
+ isLoading={inputLoading}
1166
1301
  onConfirm={inputModal.onConfirm}
1167
1302
  onClose={() => setInputModal(null)}
1168
1303
  />
1169
1304
  )}
1170
1305
 
1171
- {/* Move Modal */}
1306
+ {/* 移动弹窗 */}
1172
1307
  {isMoveModalOpen && (
1173
1308
  <MoveModal
1174
1309
  items={selectedIds.map(id => items.find(i => i.cid === id)).filter(Boolean)}
@@ -1180,17 +1315,17 @@ export default function App() {
1180
1315
  />
1181
1316
  )}
1182
1317
 
1183
- {/* Share Modal */}
1318
+ {/* 分享弹窗 */}
1184
1319
  {shareItem && (
1185
1320
  <ModalOverlay onClose={() => setShareItem(null)}>
1186
- <div style={{ width: 420, padding: 24, borderRadius: 16, background: bgSecondary, border: `1px solid ${borderColor}` }} onClick={e => e.stopPropagation()}>
1187
- <div style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center', marginBottom: 16 }}>
1188
- <h3 style={{ fontSize: 16, fontWeight: 600 }}>分享链接</h3>
1189
- <button onClick={() => setShareItem(null)} style={{ background: 'none', border: 'none', cursor: 'pointer', color: textMuted }}><X size={18} /></button>
1321
+ <div className="share-modal" onClick={e => e.stopPropagation()}>
1322
+ <div className="modal-header">
1323
+ <h3>分享链接</h3>
1324
+ <button onClick={() => setShareItem(null)} className="modal-close-btn"><X size={18} /></button>
1190
1325
  </div>
1191
- <div style={{ display: 'flex', gap: 10, marginBottom: 16 }}>
1192
- <div style={{ flex: 1, padding: '12px 14px', borderRadius: 10, background: bgTertiary, fontSize: 13, fontFamily: 'monospace', color: textPrimary, wordBreak: 'break-all' }}>most://{shareItem.cid}</div>
1193
- <button onClick={handleCopyLink} style={{ width: 44, borderRadius: 10, border: 'none', background: copied ? '#22c55e' : accentBlue, color: '#fff', cursor: 'pointer' }}>
1326
+ <div className="share-link-box">
1327
+ <div className="share-link-text">most://{shareItem.cid}</div>
1328
+ <button onClick={handleCopyLink} className={`share-copy-btn ${copied ? 'copied' : ''}`}>
1194
1329
  {copied ? <Check size={18} /> : <Copy size={18} />}
1195
1330
  </button>
1196
1331
  </div>
@@ -1198,51 +1333,113 @@ export default function App() {
1198
1333
  </ModalOverlay>
1199
1334
  )}
1200
1335
 
1201
- {/* Download Modal */}
1336
+ {/* 下载弹窗 */}
1202
1337
  {isDownloadModalOpen && (
1203
1338
  <ModalOverlay onClose={() => setIsDownloadModalOpen(false)}>
1204
- <div style={{ width: 400, padding: 24, borderRadius: 16, background: bgSecondary, border: `1px solid ${borderColor}` }} onClick={e => e.stopPropagation()}>
1205
- <div style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center', marginBottom: 16 }}>
1206
- <h3 style={{ fontSize: 16, fontWeight: 600 }}>下载文件</h3>
1207
- <button onClick={() => setIsDownloadModalOpen(false)} style={{ background: 'none', border: 'none', cursor: 'pointer', color: textMuted }}><X size={18} /></button>
1339
+ <div className="download-modal" onClick={e => e.stopPropagation()}>
1340
+ <div className="modal-header">
1341
+ <h3>下载文件</h3>
1342
+ <button onClick={() => setIsDownloadModalOpen(false)} className="modal-close-btn"><X size={18} /></button>
1208
1343
  </div>
1209
- <input type="text" value={downloadLink} onChange={(e) => setDownloadLink(e.target.value)} placeholder="most://..." onKeyDown={(e) => e.key === 'Enter' && handleDownloadSharedFile()} style={{ width: '100%', padding: '12px 14px', borderRadius: 10, border: `1.5px solid ${borderColor}`, fontSize: 13, fontFamily: 'monospace', outline: 'none', background: bgTertiary, color: textPrimary, marginBottom: 16 }} />
1210
- <button onClick={handleDownloadSharedFile} disabled={!downloadLink.trim() || isDownloading} style={{ width: '100%', padding: 12, borderRadius: 10, border: 'none', background: (downloadLink.trim() && !isDownloading) ? '#6366f1' : bgTertiary, color: (downloadLink.trim() && !isDownloading) ? '#fff' : textMuted, fontSize: 13, fontWeight: 600, cursor: (downloadLink.trim() && !isDownloading) ? 'pointer' : 'not-allowed' }}>
1344
+ <input type="text" value={downloadLink} onChange={(e) => setDownloadLink(e.target.value)} placeholder="most://..." onKeyDown={(e) => e.key === 'Enter' && handleDownloadSharedFile()} className="download-input" />
1345
+ <button onClick={handleDownloadSharedFile} disabled={!downloadLink.trim() || isDownloading} className="download-btn">
1211
1346
  {isDownloading ? '下载中...' : '开始下载'}
1212
1347
  </button>
1213
1348
  </div>
1214
1349
  </ModalOverlay>
1215
1350
  )}
1216
1351
 
1217
- {/* Preview Modal */}
1352
+ {/* 预览弹窗 */}
1218
1353
  {previewItem && (
1219
- <div style={{ position: 'fixed', inset: 0, zIndex: 200, background: 'rgba(0,0,0,0.9)', display: 'flex', alignItems: 'center', justifyContent: 'center', padding: 24 }} onClick={() => setPreviewItem(null)}>
1220
- <button onClick={() => setPreviewItem(null)} style={{ position: 'absolute', top: 20, right: 20, width: 36, height: 36, borderRadius: 10, border: 'none', background: 'rgba(255,255,255,0.1)', cursor: 'pointer', color: '#fff' }}><X size={20} /></button>
1354
+ <div className="preview-overlay" onClick={() => { setPreviewItem(null); setPreviewText(''); setPreviewMediaLoading(false) }}>
1355
+ <button className="preview-close"><X size={20} /></button>
1221
1356
  <div onClick={e => e.stopPropagation()}>
1222
- {previewItem.subtype === 'image' && <img src={API.getFileDownloadUrl(previewItem.cid)} alt="" style={{ maxWidth: '100%', maxHeight: '80vh', borderRadius: 12 }} />}
1223
- {previewItem.subtype === 'video' && <video src={API.getFileDownloadUrl(previewItem.cid)} controls style={{ maxWidth: '100%', maxHeight: '80vh', borderRadius: 12 }} />}
1224
- {previewItem.subtype === 'audio' && <div style={{ background: 'rgba(255,255,255,0.05)', borderRadius: 16, padding: 32 }}><Music size={48} color="#fff" style={{ marginBottom: 12 }} /><audio src={API.getFileDownloadUrl(previewItem.cid)} controls /></div>}
1357
+ {previewItem.subtype === 'image' && (
1358
+ <div className="preview-media-wrapper">
1359
+ <img ref={previewMediaRef} src={API.getFileDownloadUrl(previewItem.cid)} alt="" />
1360
+ </div>
1361
+ )}
1362
+ {previewItem.subtype === 'video' && (
1363
+ <div className="preview-media-wrapper">
1364
+ <video ref={previewMediaRef} src={API.getFileDownloadUrl(previewItem.cid)} controls />
1365
+ </div>
1366
+ )}
1367
+ {previewItem.subtype === 'audio' && (
1368
+ <div className="preview-audio">
1369
+ <div className="preview-audio-icon">
1370
+ <Music size={36} color="var(--accent-blue)" />
1371
+ </div>
1372
+ <p className="preview-audio-filename">{previewItem.fileName}</p>
1373
+ <audio className="preview-audio-player" src={API.getFileDownloadUrl(previewItem.cid)} controls />
1374
+ </div>
1375
+ )}
1376
+ {previewItem.subtype === 'file' && (
1377
+ <div className="preview-unsupported">
1378
+ <FileText size={48} color="#fff" style={{ marginBottom: 12, opacity: 0.5 }} />
1379
+ <p>{previewItem.fileName}</p>
1380
+ <p style={{ fontSize: 12, marginTop: 8, opacity: 0.7 }}>无法预览</p>
1381
+ </div>
1382
+ )}
1383
+ {previewItem.subtype === 'text' && (
1384
+ <div className="preview-text-container">
1385
+ <div className="preview-text-header">
1386
+ <span>{previewItem.fileName}</span>
1387
+ </div>
1388
+ {previewLoading ? (
1389
+ <div className="preview-text-loading">
1390
+ <Loader size={24} className="preview-text-spinner" />
1391
+ <p>正在加载文本预览...</p>
1392
+ <p className="preview-text-loading-hint">如果是首次预览,可能需要等待 P2P 网络同步</p>
1393
+ </div>
1394
+ ) : (
1395
+ <pre className="preview-text">{previewText || '(文件为空)'}</pre>
1396
+ )}
1397
+ </div>
1398
+ )}
1225
1399
  </div>
1226
1400
  </div>
1227
1401
  )}
1228
1402
 
1229
- {/* Batch Actions Bar */}
1403
+ {/* 批量操作栏 */}
1230
1404
  {selectedIds.length > 0 && (
1231
- <div style={{ position: 'fixed', bottom: 24, left: '50%', transform: 'translateX(-50%)', display: 'flex', alignItems: 'center', gap: 8, padding: '10px 16px', borderRadius: 12, background: bgSecondary, boxShadow: '0 4px 20px rgba(0,0,0,0.15)', border: `1px solid ${borderColor}`, zIndex: 100 }}>
1232
- <span style={{ fontSize: 12, color: textSecondary }}>已选 {selectedIds.length} 项</span>
1233
- <button onClick={() => setSelectedIds([])} style={{ background: 'none', border: 'none', cursor: 'pointer', color: textMuted, padding: 4 }}><X size={16} /></button>
1234
- <div style={{ width: 1, height: 20, background: borderColor }} />
1405
+ <div className="batch-bar">
1406
+ <span className="batch-info">已选 {selectedIds.length} 项</span>
1407
+ <button onClick={() => setSelectedIds([])} className="batch-dismiss"><X size={16} /></button>
1408
+ <div className="batch-divider" />
1235
1409
  {currentView === 'trash' ? (
1236
1410
  <>
1237
- <button onClick={() => selectedIds.forEach(cid => handleRestore(cid))} style={{ display: 'flex', alignItems: 'center', gap: 4, padding: '6px 12px', borderRadius: 8, border: 'none', fontSize: 12, cursor: 'pointer' }}>
1411
+ <button onClick={async () => {
1412
+ await Promise.all(selectedIds.map(cid => API.restoreTrashFile(cid)))
1413
+ setSelectedIds([])
1414
+ addToast('已恢复', 'success')
1415
+ refreshFiles()
1416
+ refreshTrash()
1417
+ refreshStorageStats()
1418
+ }} className="btn small">
1238
1419
  恢复
1239
1420
  </button>
1240
- <button onClick={handleBatchDelete} style={{ display: 'flex', alignItems: 'center', gap: 4, padding: '6px 12px', borderRadius: 8, border: 'none', background: '#ef4444', color: '#fff', fontSize: 12, cursor: 'pointer' }}>
1421
+ <button onClick={handleBatchDelete} className="btn small danger">
1241
1422
  永久删除
1242
1423
  </button>
1243
1424
  </>
1244
1425
  ) : (
1245
1426
  <>
1427
+ {selectedIds.length === 1 && (() => {
1428
+ const file = items.find(i => i.cid === selectedIds[0])
1429
+ return file && getFileSubtype(file.fileName) !== 'file'
1430
+ })() && (
1431
+ <button onClick={() => {
1432
+ const file = items.find(i => i.cid === selectedIds[0])
1433
+ if (file) {
1434
+ const subtype = getFileSubtype(file.fileName)
1435
+ setPreviewItem({ ...file, subtype })
1436
+ setPreviewText('')
1437
+ if (subtype === 'text') loadPreviewText(file.cid)
1438
+ }
1439
+ }} className="btn small">
1440
+ 预览
1441
+ </button>
1442
+ )}
1246
1443
  <button onClick={() => {
1247
1444
  const hasUnstarred = selectedIds.some(id => {
1248
1445
  const item = items.find(i => i.cid === id)
@@ -1254,65 +1451,72 @@ export default function App() {
1254
1451
  handleToggleStar(id)
1255
1452
  }
1256
1453
  })
1257
- }} style={{ display: 'flex', alignItems: 'center', gap: 4, padding: '6px 12px', borderRadius: 8, border: 'none', background: '#f59e0b', color: '#fff', fontSize: 12, cursor: 'pointer' }}>
1454
+ }} className="btn small" style={{ background: '#f59e0b', color: '#fff' }}>
1258
1455
  收藏
1259
1456
  </button>
1260
- <button onClick={() => {
1261
- const firstSelected = items.find(i => i.cid === selectedIds[0])
1262
- if (firstSelected) openRenameModal(firstSelected)
1263
- }} style={{ display: 'flex', alignItems: 'center', gap: 4, padding: '6px 12px', borderRadius: 8, border: 'none', background: bgTertiary, color: textPrimary, fontSize: 12, cursor: 'pointer' }}>
1264
- 重命名
1265
- </button>
1266
- <button onClick={() => setIsMoveModalOpen(true)} style={{ display: 'flex', alignItems: 'center', gap: 4, padding: '6px 12px', borderRadius: 8, border: 'none', background: accentBlue, color: '#fff', fontSize: 12, cursor: 'pointer' }}>
1457
+ {selectedIds.length === 1 && (
1458
+ <button onClick={() => {
1459
+ const firstSelected = items.find(i => i.cid === selectedIds[0])
1460
+ if (firstSelected) openRenameModal(firstSelected)
1461
+ }} className="btn small">
1462
+ 重命名
1463
+ </button>
1464
+ )}
1465
+ <button onClick={() => setIsMoveModalOpen(true)} className="btn small" style={{ background: 'var(--accent-blue)', color: '#fff' }}>
1267
1466
  移动
1268
1467
  </button>
1269
- <button onClick={handleBatchDelete} style={{ padding: '6px 12px', borderRadius: 8, border: 'none', background: '#ef4444', color: '#fff', fontSize: 12, cursor: 'pointer' }}>删除</button>
1270
- <button onClick={() => setShareItem(items.find(i => i.cid === selectedIds[0]))} style={{ padding: '6px 12px', borderRadius: 8, border: 'none', background: bgTertiary, color: textPrimary, fontSize: 12, cursor: 'pointer' }}>分享</button>
1468
+ <button onClick={handleBatchDelete} className="btn small danger">删除</button>
1469
+ {selectedIds.length === 1 && (
1470
+ <button onClick={() => setShareItem(items.find(i => i.cid === selectedIds[0]))} className="btn small">分享</button>
1471
+ )}
1472
+ {selectedIds.length === 1 && (
1473
+ <button onClick={() => {
1474
+ const file = items.find(i => i.cid === selectedIds[0])
1475
+ if (file) handleSaveAs(file)
1476
+ }} className="btn small">另存为</button>
1477
+ )}
1271
1478
  </>
1272
1479
  )}
1273
1480
  </div>
1274
1481
  )}
1275
1482
 
1276
- {/* Transfer Panel */}
1483
+ {/* 传输面板 */}
1277
1484
  {isTransferPanelOpen && (
1278
- <ModalOverlay onClose={() => setIsTransferPanelOpen(false)}>
1279
- <div style={{ width: 380, maxHeight: '70vh', padding: 24, borderRadius: 16, background: bgSecondary, border: `1px solid ${borderColor}`, overflow: 'auto' }} onClick={e => e.stopPropagation()}>
1280
- <div style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center', marginBottom: 16 }}>
1281
- <h3 style={{ fontSize: 16, fontWeight: 600 }}>传输</h3>
1282
- <button onClick={() => setIsTransferPanelOpen(false)} style={{ background: 'none', border: 'none', cursor: 'pointer', color: textMuted }}><X size={18} /></button>
1485
+ <ModalOverlay onClose={() => setIsTransferPanelOpen(false)} closeOnOverlayClick={true}>
1486
+ <div className="transfer-modal" onClick={e => e.stopPropagation()}>
1487
+ <div className="modal-header">
1488
+ <h3>传输</h3>
1489
+ <button onClick={() => setIsTransferPanelOpen(false)} className="modal-close-btn"><X size={18} /></button>
1283
1490
  </div>
1284
1491
  {transfers.length === 0 ? (
1285
- <div style={{ textAlign: 'center', color: textMuted, padding: 24, fontSize: 13 }}>
1492
+ <div style={{ textAlign: 'center', color: 'var(--text-muted)', padding: 24, fontSize: 13 }}>
1286
1493
  暂无传输
1287
1494
  </div>
1288
1495
  ) : (
1289
1496
  transfers.map(t => (
1290
- <div key={t.id} style={{ padding: '10px 0', borderBottom: `1px solid ${borderColor}` }}>
1291
- <div style={{ display: 'flex', alignItems: 'center', gap: 8, marginBottom: 4 }}>
1292
- {t.type === 'upload' ? <Upload size={14} color={accentBlue} /> : <Download size={14} color="#6366f1" />}
1293
- <span style={{ fontSize: 12, fontWeight: 500, flex: 1, overflow: 'hidden', textOverflow: 'ellipsis', whiteSpace: 'nowrap' }}>{t.fileName}</span>
1294
- {t.status === 'uploading' && t.type === 'download' && (
1295
- <button onClick={() => handleCancelTransfer(t)} style={{ background: 'none', border: 'none', cursor: 'pointer', color: '#ef4444', padding: 2 }}>
1497
+ <div key={t.id} className="transfer-item">
1498
+ <div className="transfer-item-header">
1499
+ {t.type === 'upload' ? <Upload size={14} /> : <Download size={14} />}
1500
+ <span className="transfer-item-name">{t.fileName}</span>
1501
+ {t.status === 'downloading' && t.type === 'download' && (
1502
+ <button onClick={() => handleCancelTransfer(t)} className="transfer-item-cancel">
1296
1503
  <X size={14} />
1297
1504
  </button>
1298
1505
  )}
1299
1506
  </div>
1300
- <div style={{ display: 'flex', alignItems: 'center', gap: 8 }}>
1301
- <div style={{ flex: 1, height: 4, borderRadius: 2, background: bgTertiary }}>
1302
- <div style={{
1303
- width: `${t.progress}%`,
1304
- height: '100%',
1305
- borderRadius: 2,
1306
- background: t.status === 'error' ? '#ef4444' : t.status === 'cancelled' ? '#f59e0b' : t.type === 'upload' ? accentBlue : '#6366f1',
1307
- transition: 'width 0.2s'
1308
- }} />
1507
+ <div className="transfer-progress-row">
1508
+ <div className="transfer-progress-bar">
1509
+ <div
1510
+ className={`transfer-progress-fill ${t.type === 'download' ? 'download' : ''} ${t.status === 'error' ? 'error' : ''} ${t.status === 'cancelled' ? 'cancelled' : ''}`}
1511
+ style={{ width: `${t.progress}%` }}
1512
+ />
1309
1513
  </div>
1310
- <span style={{ fontSize: 10, color: textMuted, minWidth: 32, textAlign: 'right' }}>
1311
- {t.status === 'completed' ? '完成' :
1312
- t.status === 'error' ? '失败' :
1313
- t.status === 'cancelled' ? '已取消' :
1314
- t.loaded && t.total ? `${formatSize(t.loaded)}/${formatSize(t.total)}` :
1315
- `${t.progress}%`}
1514
+ <span className="transfer-progress-text">
1515
+ {t.status === 'completed' ? '完成' :
1516
+ t.status === 'error' ? '失败' :
1517
+ t.status === 'cancelled' ? '已取消' :
1518
+ t.loaded && t.total ? `${formatSize(t.loaded)}/${formatSize(t.total)}` :
1519
+ `${t.progress}%`}
1316
1520
  </span>
1317
1521
  </div>
1318
1522
  </div>
@@ -1322,14 +1526,18 @@ export default function App() {
1322
1526
  </ModalOverlay>
1323
1527
  )}
1324
1528
 
1325
- {/* Toasts */}
1529
+ {/* 通知列表 */}
1326
1530
  {toasts.map((t, i) => <Toast key={t.id} message={t.message} type={t.type} onDone={() => removeToast(t.id)} index={i} />)}
1327
1531
 
1328
- {/* Welcome Guide */}
1329
- {showWelcome && <WelcomeGuide onClose={handleCloseWelcome} />}
1532
+ {/* 引导页 */}
1533
+ {showWelcome && <WelcomeGuide onClose={handleCloseWelcome} onShutdown={() => {
1534
+ fetch('/api/shutdown', { method: 'POST' })
1535
+ addToast('服务已关闭,请重新启动应用', 'info')
1536
+ handleCloseWelcome()
1537
+ }} />}
1330
1538
 
1331
- {/* Settings Modal */}
1332
- {showSettings && <SettingsModal onClose={() => setShowSettings(false)} addToast={addToast} />}
1539
+ {/* 设置弹窗 */}
1540
+ {showSettings && <SettingsModal onClose={() => setShowSettings(false)} addToast={addToast} isDarkMode={isDarkMode} handleShutdown={handleShutdown} />}
1333
1541
  </div>
1334
1542
  )
1335
1543
  }