NathMen12 commited on
Commit
7ac5344
·
verified ·
1 Parent(s): 80df7bc

Update public/app.js

Browse files
Files changed (1) hide show
  1. public/app.js +552 -392
public/app.js CHANGED
@@ -1,420 +1,580 @@
1
- <!DOCTYPE html>
2
- <html lang="fr">
3
- <head>
4
- <meta charset="UTF-8">
5
- <meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=no">
6
- <title>PixelDrive</title>
7
- <link rel="manifest" href="/manifest.json">
8
- <meta name="theme-color" content="#4f46e5">
9
- <!-- ✅ OPEN GRAPH : bannière visible dans WhatsApp/Messages/Discord, PAS sur le site -->
10
- <meta property="og:type" content="website">
11
- <meta property="og:site_name" content="PixelDrive">
12
- <meta property="og:title" content="PixelDrive — Stockage cloud chiffré Zero-Knowledge">
13
- <meta property="og:description" content="Importez, chiffrez (AES-256-GCM) et partagez vos fichiers en toute confidentialité.">
14
- <meta property="og:image" content="https://nathmen12-pixel-drive.hf.space/banner.png">
15
- <meta property="og:url" content="https://nathmen12-pixel-drive.hf.space/">
16
- <meta name="twitter:card" content="summary_large_image">
17
- <meta name="twitter:title" content="PixelDrive — Stockage cloud chiffré">
18
- <meta name="twitter:description" content="Partage sécurisé de fichiers chiffrés.">
19
- <meta name="twitter:image" content="https://nathmen12-pixel-drive.hf.space/banner.png">
20
- <link rel="icon" type="image/png" href="/banner.png">
21
- <style>
22
- :root { --bg-dark:#0f0f13; --bg-panel:#18181b; --bg-hover:#27272a; --primary:#6366f1; --primary-hover:#4f46e5; --text-main:#f4f4f5; --text-muted:#a1a1aa; --border:#27272a; --danger:#ef4444; --success:#22c55e; --radius:8px; --font:'Inter',-apple-system,BlinkMacSystemFont,"Segoe UI",Roboto,sans-serif; --sidebar-width:260px; }
23
- * { box-sizing:border-box; margin:0; padding:0; outline:none; -webkit-tap-highlight-color:transparent; }
24
- body { font-family:var(--font); background:var(--bg-dark); color:var(--text-main); height:100dvh; overflow:hidden; display:flex; flex-direction:column; }
25
- .icon { width:20px; height:20px; stroke:currentColor; stroke-width:2; fill:none; stroke-linecap:round; stroke-linejoin:round; flex-shrink:0; }
26
- .icon-sm { width:16px; height:16px; }
27
- .icon-lg { width:48px; height:48px; stroke-width:1.5; }
28
 
29
- #auth-screen { position:fixed; inset:0; background:var(--bg-dark); z-index:1000; display:flex; align-items:center; justify-content:center; padding:1rem; }
30
- .auth-card { background:var(--bg-panel); padding:2rem; border-radius:var(--radius); width:100%; max-width:400px; border:1px solid var(--border); }
31
- .auth-header { text-align:center; margin-bottom:2rem; }
32
- .auth-header h1 { font-size:1.5rem; font-weight:700; color:var(--primary); display:flex; align-items:center; justify-content:center; gap:0.5rem; }
33
- .form-group { margin-bottom:1rem; }
34
- .form-group label { display:block; font-size:0.875rem; color:var(--text-muted); margin-bottom:0.5rem; }
35
- .form-input { width:100%; padding:0.875rem; background:var(--bg-dark); border:1px solid var(--border); border-radius:var(--radius); color:var(--text-main); font-size:1rem; }
36
- .form-input:focus { border-color:var(--primary); }
37
- .btn { width:100%; padding:0.875rem; background:var(--primary); color:white; border:none; border-radius:var(--radius); font-weight:600; cursor:pointer; display:flex; align-items:center; justify-content:center; gap:0.5rem; font-size:1rem; }
38
- .btn-secondary { background:transparent; border:1px solid var(--border); color:var(--text-muted); }
39
- .auth-switch { margin-top:1.5rem; text-align:center; font-size:0.875rem; color:var(--text-muted); }
40
- .auth-switch a { color:var(--primary); cursor:pointer; font-weight:500; }
41
 
42
- #app-screen { display:none; height:100%; flex-direction:column; }
43
- header { height:60px; background:var(--bg-panel); border-bottom:1px solid var(--border); display:flex; align-items:center; justify-content:space-between; padding:0 1rem; z-index:100; flex-shrink:0; }
44
- .header-left { display:flex; align-items:center; gap:0.75rem; }
45
- .menu-btn { display:none; background:transparent; border:none; color:var(--text-main); cursor:pointer; padding:0.5rem; border-radius:var(--radius); }
46
- .logo { font-weight:700; font-size:1.25rem; color:var(--primary); display:flex; align-items:center; gap:0.5rem; }
47
- .user-menu { display:flex; align-items:center; gap:0.5rem; }
48
- .user-badge { font-size:0.875rem; color:var(--text-muted); }
49
- .main-container { display:flex; flex:1; overflow:hidden; }
50
- aside { width:var(--sidebar-width); background:var(--bg-panel); border-right:1px solid var(--border); display:flex; flex-direction:column; padding:1rem; flex-shrink:0; transition:transform 0.3s ease; }
51
- .nav-item { display:flex; align-items:center; gap:0.75rem; padding:0.875rem; border-radius:var(--radius); color:var(--text-muted); cursor:pointer; margin-bottom:0.25rem; font-weight:500; }
52
- .nav-item.active { background:var(--primary); color:white; }
53
- .nav-section { font-size:0.75rem; text-transform:uppercase; letter-spacing:0.05em; color:var(--text-muted); margin:1.5rem 0 0.5rem 0.75rem; font-weight:600; }
 
 
 
 
 
 
54
 
55
- /* FOOTER SIDEBAR : profil + settings */
56
- .sidebar-footer { margin-top:auto; padding-top:1rem; border-top:1px solid var(--border); display:flex; align-items:center; gap:0.75rem; }
57
- .avatar-btn { width:40px; height:40px; border-radius:50%; overflow:hidden; background:var(--primary); border:none; cursor:pointer; display:flex; align-items:center; justify-content:center; flex-shrink:0; }
58
- .avatar-btn img { width:100%; height:100%; object-fit:cover; }
59
- .avatar-initial { color:white; font-weight:700; font-size:1rem; display:flex; align-items:center; justify-content:center; width:100%; height:100%; }
60
- .avatar-initial.big { font-size:1.5rem; }
61
- .footer-info { flex:1; min-width:0; display:flex; flex-direction:column; }
62
- .footer-username { font-size:0.875rem; font-weight:600; white-space:nowrap; overflow:hidden; text-overflow:ellipsis; }
63
- .footer-sub { font-size:0.7rem; color:var(--text-muted); }
64
- .avatar-preview { width:72px; height:72px; border-radius:50%; overflow:hidden; background:var(--primary); display:flex; align-items:center; justify-content:center; flex-shrink:0; }
65
- .avatar-preview img { width:100%; height:100%; object-fit:cover; }
66
- .settings-avatar-row { display:flex; gap:1rem; align-items:center; margin-bottom:1.25rem; }
67
- .settings-section { margin-bottom:1.25rem; }
68
- .owner-badge img { width:14px; height:14px; border-radius:50%; object-fit:cover; vertical-align:-2px; margin-right:4px; }
69
 
70
- main { flex:1; display:flex; flex-direction:column; min-width:0; }
71
- .toolbar { padding:0.75rem 1rem; display:flex; align-items:center; justify-content:space-between; border-bottom:1px solid var(--border); background:var(--bg-panel); gap:0.5rem; flex-shrink:0; }
72
- .breadcrumb { display:flex; align-items:center; gap:0.5rem; font-size:1rem; font-weight:600; flex:1; min-width:0; overflow:hidden; }
73
- .breadcrumb span { color:var(--text-muted); cursor:pointer; white-space:nowrap; overflow:hidden; text-overflow:ellipsis; }
74
- .breadcrumb span:last-child { color:var(--text-main); }
75
- .breadcrumb .sep { color:var(--border); flex-shrink:0; }
76
- .actions { display:flex; gap:0.5rem; align-items:center; flex-shrink:0; }
77
- .btn-icon { width:40px; height:40px; background:transparent; border:1px solid var(--border); border-radius:var(--radius); color:var(--text-muted); cursor:pointer; display:flex; align-items:center; justify-content:center; }
78
- .btn-primary { background:var(--primary); color:white; border:none; padding:0 1rem; height:40px; border-radius:var(--radius); font-weight:500; cursor:pointer; display:flex; align-items:center; gap:0.5rem; }
79
- .sort-select { height:40px; background:var(--bg-dark); border:1px solid var(--border); border-radius:var(--radius); color:var(--text-muted); font-size:0.8rem; padding:0 0.5rem; }
80
- .file-area { flex:1; overflow-y:auto; padding:1rem; -webkit-overflow-scrolling:touch; }
81
- .empty-state { text-align:center; padding:4rem 1rem; color:var(--text-muted); }
82
- .empty-state .icon { margin-bottom:1rem; opacity:0.5; }
83
- .section-title { margin:1.25rem 0 0.75rem; font-size:0.75rem; text-transform:uppercase; letter-spacing:0.05em; color:var(--text-muted); font-weight:600; }
84
- .section-title:first-child { margin-top:0; }
85
 
86
- .grid { display:grid; grid-template-columns:repeat(auto-fill,minmax(150px,1fr)); gap:0.75rem; }
87
- .card { background:var(--bg-panel); border:1px solid var(--border); border-radius:var(--radius); overflow:hidden; cursor:pointer; display:flex; flex-direction:column; }
88
- .card:active { transform:scale(0.97); border-color:var(--primary); }
89
- .card-thumb { aspect-ratio:1/1; background:#000; display:flex; align-items:center; justify-content:center; overflow:hidden; position:relative; }
90
- .card-thumb img { width:100%; height:100%; object-fit:cover; opacity:0.9; }
91
- .card-thumb .icon-placeholder { width:36px; height:36px; color:var(--text-muted); opacity:0.5; }
92
- .card-thumb.folder-thumb { background:var(--bg-hover); }
93
- .card-thumb.folder-thumb .icon { width:42px; height:42px; color:var(--primary); }
94
- .owner-badge { position:absolute; top:6px; left:6px; background:rgba(0,0,0,0.65); color:var(--primary); font-size:0.65rem; padding:2px 7px; border-radius:4px; font-weight:600; max-width:90%; white-space:nowrap; overflow:hidden; text-overflow:ellipsis; }
95
- .card-info { padding:0.6rem 0.7rem; border-top:1px solid var(--border); }
96
- .card-name { font-weight:500; font-size:0.85rem; white-space:nowrap; overflow:hidden; text-overflow:ellipsis; margin-bottom:0.2rem; }
97
- .card-meta { display:flex; justify-content:space-between; gap:0.5rem; font-size:0.7rem; color:var(--text-muted); }
98
- .card-meta span { white-space:nowrap; overflow:hidden; text-overflow:ellipsis; min-width:0; }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
99
 
100
- .modal-overlay { position:fixed; inset:0; background:rgba(0,0,0,0.85); backdrop-filter:blur(4px); display:none; align-items:center; justify-content:center; z-index:2000; padding:1rem; }
101
- .modal { background:var(--bg-panel); border:1px solid var(--border); border-radius:var(--radius); width:100%; max-width:500px; padding:1.5rem; max-height:90vh; overflow-y:auto; }
102
- .modal-header { display:flex; justify-content:space-between; align-items:center; margin-bottom:1.5rem; }
103
- .modal-title { font-size:1.25rem; font-weight:600; display:flex; align-items:center; gap:0.5rem; }
104
- .close-modal { background:none; border:none; color:var(--text-muted); cursor:pointer; padding:0.5rem; }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
105
 
106
- #preview-modal .modal { max-width:100vw; max-height:100vh; background:var(--bg-dark); border:none; border-radius:0; padding:0; display:flex; flex-direction:column; width:100%; height:100%; }
107
- #preview-container { flex:1; display:flex; align-items:center; justify-content:center; overflow:hidden; padding:1rem; }
108
- #preview-content { max-width:100%; max-height:100%; object-fit:contain; }
109
- .preview-controls { padding:1rem; border-top:1px solid var(--border); display:flex; gap:1rem; background:var(--bg-panel); }
110
- .preview-controls .btn { flex:1; }
 
 
111
 
112
- .tos-frame { width:100%; height:45vh; border:1px solid var(--border); border-radius:var(--radius); background:var(--bg-dark); margin-bottom:1.5rem; }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
113
 
114
- .dialog-message { font-size:0.9rem; color:var(--text-muted); margin-bottom:1rem; line-height:1.5; }
115
- .dialog-actions { display:flex; gap:1rem; margin-top:1.5rem; }
116
- .dialog-actions .btn { flex:1; }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
117
 
118
- .share-section { margin-bottom:1.25rem; }
119
- .share-label { font-size:0.9rem; font-weight:600; display:flex; align-items:center; gap:0.4rem; margin-bottom:0.25rem; }
120
- .share-hint { font-size:0.75rem; color:var(--text-muted); margin-bottom:0.5rem; }
121
- .share-input-group { display:flex; gap:0.5rem; }
122
- .share-input { flex:1; font-family:monospace; font-size:0.8rem; padding:0.6rem 0.75rem; background:var(--bg-dark); }
123
- .share-copy-btn { width:44px; height:44px; background:var(--primary); color:white; border:none; flex-shrink:0; }
124
- .share-copy-btn.copied { background:var(--success); }
125
- .share-footer { font-size:0.75rem; color:var(--text-muted); text-align:center; padding-top:0.75rem; border-top:1px solid var(--border); }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
126
 
127
- .share-list { display:flex; flex-direction:column; gap:0.75rem; }
128
- .share-item { background:var(--bg-panel); border:1px solid var(--border); border-radius:var(--radius); padding:1rem; }
129
- .share-item-header { display:flex; justify-content:space-between; align-items:center; margin-bottom:0.75rem; padding-bottom:0.75rem; border-bottom:1px solid var(--border); gap:0.5rem; }
130
- .share-item-info { display:flex; align-items:center; gap:0.5rem; flex:1; min-width:0; }
131
- .share-item-name { font-weight:500; white-space:nowrap; overflow:hidden; text-overflow:ellipsis; }
132
- .share-item-sub { font-size:0.75rem; color:var(--text-muted); }
133
- .share-item-links { display:flex; flex-direction:column; gap:0.5rem; }
134
- .share-link-row { display:flex; align-items:center; gap:0.5rem; }
135
- .share-link-label { font-size:0.75rem; color:var(--text-muted); width:70px; flex-shrink:0; }
136
- .share-link-input { flex:1; font-family:monospace; font-size:0.75rem; padding:0.5rem 0.75rem; background:var(--bg-dark); }
137
- .share-item-footer { margin-top:0.75rem; padding-top:0.75rem; border-top:1px solid var(--border); font-size:0.75rem; color:var(--text-muted); text-align:center; }
 
 
 
 
 
 
138
 
139
- #upload-overlay { position:fixed; bottom:0; left:0; right:0; background:var(--bg-panel); border-top:1px solid var(--border); padding:1rem; display:none; z-index:3000; padding-bottom:calc(1rem + env(safe-area-inset-bottom)); }
140
- .progress-bar-bg { height:4px; background:var(--bg-dark); border-radius:2px; margin-top:0.75rem; overflow:hidden; }
141
- .progress-bar-fill { height:100%; background:var(--primary); width:0%; transition:width 0.3s; }
142
- .upload-status { font-size:0.875rem; color:var(--text-muted); margin-top:0.75rem; display:flex; justify-content:space-between; gap:1rem; }
143
- .upload-status span:first-child { overflow:hidden; text-overflow:ellipsis; white-space:nowrap; }
144
- .upload-speed { color:var(--success); font-weight:600; }
 
 
 
 
 
 
 
 
 
 
145
 
146
- #toast-container { position:fixed; top:1rem; right:1rem; left:1rem; z-index:4000; display:flex; flex-direction:column; gap:0.5rem; pointer-events:none; }
147
- .toast { background:var(--bg-panel); border-left:4px solid var(--primary); padding:1rem; border-radius:4px; animation:slideIn 0.3s ease-out; pointer-events:auto; }
148
- .toast.error { border-left-color:var(--danger); }
149
- .toast.success { border-left-color:var(--success); }
150
- @keyframes slideIn { from { transform:translateY(-20px); opacity:0; } to { transform:translateY(0); opacity:1; } }
 
 
 
 
151
 
152
- #context-menu { position:fixed; background:var(--bg-panel); border:1px solid var(--border); border-radius:var(--radius); padding:0.5rem 0; min-width:220px; box-shadow:0 10px 25px rgba(0,0,0,0.5); display:none; z-index:5000; }
153
- .ctx-item { padding:0.75rem 1rem; font-size:0.875rem; cursor:pointer; display:flex; align-items:center; gap:0.75rem; }
154
- .ctx-sep { height:1px; background:var(--border); margin:0.25rem 0; }
 
 
 
 
 
 
155
 
156
- .sidebar-overlay { display:none; position:fixed; inset:0; background:rgba(0,0,0,0.5); z-index:90; }
157
- .sidebar-overlay.active { display:block; }
 
 
 
 
 
 
 
 
 
 
 
158
 
159
- /* THÈME CLAIR */
160
- body.light { --bg-dark:#f5f5f6; --bg-panel:#ffffff; --bg-hover:#ececee; --text-main:#17171b; --text-muted:#5b5b66; --border:#d8d8de; }
161
- body.light .card-thumb { background:#e8e8ec; }
162
- body.light .card-thumb.folder-thumb { background:#e0e0e6; }
163
- body.light .form-input, body.light .share-input, body.light .share-link-input, body.light .sort-select { background:#f0f0f2; }
 
164
 
165
- @media (max-width:768px) {
166
- .menu-btn { display:flex; }
167
- .user-badge { display:none; }
168
- aside { position:fixed; left:0; top:60px; bottom:0; z-index:95; transform:translateX(-100%); }
169
- aside.open { transform:translateX(0); }
170
- .btn-primary .text { display:none; }
171
- .btn-primary { padding:0; width:40px; justify-content:center; }
172
- .sort-select { display:none; }
173
- #context-menu { bottom:0 !important; top:auto !important; left:0 !important; right:0; width:100%; min-width:100%; border-radius:16px 16px 0 0; padding-bottom:env(safe-area-inset-bottom); }
174
- .ctx-item { padding:1rem 1.5rem; font-size:1rem; }
175
- }
176
- @media (max-width:480px) {
177
- .grid { grid-template-columns:repeat(2,1fr); }
178
- .file-area { padding:0.75rem; }
179
- .auth-card { padding:1.5rem; }
180
- .modal { padding:1.25rem; }
181
- }
182
- @media (min-width:769px) { .btn-primary .text { display:inline; } }
183
- </style>
184
- </head>
185
- <body>
186
- <svg style="display:none;">
187
- <symbol id="icon-drive" viewBox="0 0 24 24"><path d="M22 19a2 2 0 0 1-2 2H4a2 2 0 0 1-2-2V5a2 2 0 0 1 2-2h5l2 3h9a2 2 0 0 1 2 2z"></path></symbol>
188
- <symbol id="icon-folder" viewBox="0 0 24 24"><path d="M22 19a2 2 0 0 1-2 2H4a2 2 0 0 1-2-2V5a2 2 0 0 1 2-2h5l2 3h9a2 2 0 0 1 2 2z"></path></symbol>
189
- <symbol id="icon-file" viewBox="0 0 24 24"><path d="M13 2H6a2 2 0 0 0-2 2v16a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V9z"></path><polyline points="13 2 13 9 20 9"></polyline></symbol>
190
- <symbol id="icon-upload" viewBox="0 0 24 24"><path d="M21 15v4a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2v-4"></path><polyline points="17 8 12 3 7 8"></polyline><line x1="12" y1="3" x2="12" y2="15"></line></symbol>
191
- <symbol id="icon-share" viewBox="0 0 24 24"><circle cx="18" cy="5" r="3"></circle><circle cx="6" cy="12" r="3"></circle><circle cx="18" cy="19" r="3"></circle><line x1="8.59" y1="13.51" x2="15.42" y2="17.49"></line><line x1="15.41" y1="6.51" x2="8.59" y2="10.49"></line></symbol>
192
- <symbol id="icon-clock" viewBox="0 0 24 24"><circle cx="12" cy="12" r="10"></circle><polyline points="12 6 12 12 16 14"></polyline></symbol>
193
- <symbol id="icon-trash" viewBox="0 0 24 24"><polyline points="3 6 5 6 21 6"></polyline><path d="M19 6v14a2 2 0 0 1-2 2H7a2 2 0 0 1-2-2V6m3 0V4a2 2 0 0 1 2-2h4a2 2 0 0 1 2 2v2"></path></symbol>
194
- <symbol id="icon-download" viewBox="0 0 24 24"><path d="M21 15v4a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2v-4"></path><polyline points="7 10 12 15 17 10"></polyline><line x1="12" y1="15" x2="12" y2="3"></line></symbol>
195
- <symbol id="icon-eye" viewBox="0 0 24 24"><path d="M1 12s4-8 11-8 11 8 11 8-4 8-11 8-11-8-11-8z"></path><circle cx="12" cy="12" r="3"></circle></symbol>
196
- <symbol id="icon-plus" viewBox="0 0 24 24"><line x1="12" y1="5" x2="12" y2="19"></line><line x1="5" y1="12" x2="19" y2="12"></line></symbol>
197
- <symbol id="icon-log-out" viewBox="0 0 24 24"><path d="M9 21H5a2 2 0 0 1-2-2V5a2 2 0 0 1 2-2h4"></path><polyline points="16 17 21 12 16 7"></polyline><line x1="21" y1="12" x2="9" y2="12"></line></symbol>
198
- <symbol id="icon-refresh" viewBox="0 0 24 24"><polyline points="23 4 23 10 17 10"></polyline><polyline points="1 20 1 14 7 14"></polyline><path d="M3.51 9a9 9 0 0 1 14.85-3.36L23 10M1 14l4.64 4.36A9 9 0 0 0 20.49 15"></path></symbol>
199
- <symbol id="icon-x" viewBox="0 0 24 24"><line x1="18" y1="6" x2="6" y2="18"></line><line x1="6" y1="6" x2="18" y2="18"></line></symbol>
200
- <symbol id="icon-link" viewBox="0 0 24 24"><path d="M10 13a5 5 0 0 0 7.54.54l3-3a5 5 0 0 0-7.07-7.07l-1.72 1.71"></path><path d="M14 11a5 5 0 0 0-7.54-.54l-3 3a5 5 0 0 0 7.07 7.07l1.71-1.71"></path></symbol>
201
- <symbol id="icon-user-plus" viewBox="0 0 24 24"><path d="M16 21v-2a4 4 0 0 0-4-4H5a4 4 0 0 0-4 4v2"></path><circle cx="8.5" cy="7" r="4"></circle><line x1="20" y1="8" x2="20" y2="14"></line><line x1="23" y1="11" x2="17" y2="11"></line></symbol>
202
- <symbol id="icon-menu" viewBox="0 0 24 24"><line x1="3" y1="12" x2="21" y2="12"></line><line x1="3" y1="6" x2="21" y2="6"></line><line x1="3" y1="18" x2="21" y2="18"></line></symbol>
203
- <symbol id="icon-settings" viewBox="0 0 24 24"><circle cx="12" cy="12" r="3"></circle><path d="M19.4 15a1.65 1.65 0 0 0 .33 1.82l.06.06a2 2 0 1 1-2.83 2.83l-.06-.06a1.65 1.65 0 0 0-1.82-.33 1.65 1.65 0 0 0-1 1.51V21a2 2 0 1 1-4 0v-.09a1.65 1.65 0 0 0-1-1.51 1.65 1.65 0 0 0-1.82.33l-.06.06a2 2 0 1 1-2.83-2.83l.06-.06a1.65 1.65 0 0 0 .33-1.82 1.65 1.65 0 0 0-1.51-1H3a2 2 0 1 1 0-4h.09a1.65 1.65 0 0 0 1.51-1 1.65 1.65 0 0 0-.33-1.82l-.06-.06a2 2 0 1 1 2.83-2.83l.06.06a1.65 1.65 0 0 0 1.82.33h.01a1.65 1.65 0 0 0 1-1.51V3a2 2 0 1 1 4 0v.09a1.65 1.65 0 0 0 1 1.51h.01a1.65 1.65 0 0 0 1.82-.33l.06-.06a2 2 0 1 1 2.83 2.83l-.06.06a1.65 1.65 0 0 0-.33 1.82v.01a1.65 1.65 0 0 0 1.51 1H21a2 2 0 1 1 0 4h-.09a1.65 1.65 0 0 0-1.51 1z"></path></symbol>
204
- </svg>
205
 
206
- <div id="auth-screen">
207
- <div class="auth-card">
208
- <div class="auth-header"><h1><svg class="icon"><use href="#icon-drive"></use></svg> PixelDrive</h1></div>
209
- <div id="login-form">
210
- <div class="form-group"><label data-i18n="username">Utilisateur</label><input type="text" id="login-user" class="form-input" autocomplete="username"></div>
211
- <div class="form-group"><label data-i18n="password">Mot de passe</label><input type="password" id="login-pass" class="form-input" autocomplete="current-password"></div>
212
- <button class="btn" onclick="app.login()"><span data-i18n="login_btn">Se connecter</span></button>
213
- <div class="auth-switch" id="switch-login"></div>
214
- </div>
215
- <div id="register-form" style="display:none;">
216
- <div class="form-group"><label data-i18n="username">Utilisateur</label><input type="text" id="reg-user" class="form-input"></div>
217
- <div class="form-group"><label data-i18n="password">Mot de passe</label><input type="password" id="reg-pass" class="form-input"></div>
218
- <div class="form-group"><label data-i18n="confirm_pass">Confirmer</label><input type="password" id="reg-pass2" class="form-input"></div>
219
- <div class="form-group" style="display:flex;align-items:center;gap:0.75rem;">
220
- <input type="checkbox" id="reg-tos" style="width:20px;height:20px;accent-color:var(--primary);">
221
- <label for="reg-tos" style="margin:0;font-size:0.875rem;"><span data-i18n="tos_accept">J'accepte les CGU</span> · <a href="#" onclick="app.showTos(false); return false;">CGU</a></label>
222
- </div>
223
- <button class="btn" onclick="app.register()"><span data-i18n="register_btn">Créer compte</span></button>
224
- <div class="auth-switch" id="switch-register"></div>
225
- </div>
226
- </div>
227
- </div>
 
 
 
 
228
 
229
- <div id="app-screen">
230
- <header>
231
- <div class="header-left">
232
- <button class="menu-btn" onclick="app.toggleSidebar()"><svg class="icon"><use href="#icon-menu"></use></svg></button>
233
- <div class="logo"><svg class="icon"><use href="#icon-drive"></use></svg> PixelDrive</div>
234
- </div>
235
- <div class="user-menu">
236
- <span class="user-badge" id="user-display">User</span>
237
- <button class="btn-icon" onclick="app.logout()"><svg class="icon"><use href="#icon-log-out"></use></svg></button>
238
- </div>
239
- </header>
240
- <div class="sidebar-overlay" onclick="app.toggleSidebar()"></div>
241
- <div class="main-container">
242
- <aside id="sidebar">
243
- <div class="nav-section" data-i18n="nav_section">Navigation</div>
244
- <div class="nav-item active" onclick="app.navTo('files')" id="nav-files"><svg class="icon"><use href="#icon-folder"></use></svg> <span data-i18n="nav_files">Mes Fichiers</span></div>
245
- <div class="nav-item" onclick="app.navTo('shared')" id="nav-shared"><svg class="icon"><use href="#icon-share"></use></svg> <span data-i18n="nav_shared">Partagés</span></div>
246
- <div class="nav-item" onclick="app.navTo('recent')" id="nav-recent"><svg class="icon"><use href="#icon-clock"></use></svg> <span data-i18n="nav_recent">Récents</span></div>
247
- <!-- ✅ FOOTER : avatar + profil + settings -->
248
- <div class="sidebar-footer">
249
- <button class="avatar-btn" onclick="app.openSettings()" aria-label="Profil">
250
- <img id="avatar-img" alt="" style="display:none;">
251
- <span id="avatar-initial" class="avatar-initial">?</span>
252
- </button>
253
- <div class="footer-info">
254
- <span class="footer-username" id="footer-username">User</span>
255
- <span class="footer-sub" data-i18n="settings">Paramètres</span>
256
- </div>
257
- <button class="btn-icon" onclick="app.openSettings()" title="Paramètres" data-i18n-title="settings">
258
- <svg class="icon"><use href="#icon-settings"></use></svg>
259
- </button>
260
- </div>
261
- </aside>
262
- <main>
263
- <div class="toolbar">
264
- <div class="breadcrumb" id="breadcrumb"><span onclick="app.goRoot()">Mes Fichiers</span></div>
265
- <div class="actions" id="toolbar-actions">
266
- <button class="btn-primary" onclick="document.getElementById('file-input').click()"><svg class="icon icon-sm"><use href="#icon-upload"></use></svg> <span class="text" data-i18n="upload">Uploader</span></button>
267
- <input type="file" id="file-input" style="display:none;" multiple onchange="app.handleUpload(this)">
268
- <button class="btn-icon" onclick="app.createFolder()" title="Nouveau Dossier" data-i18n-title="new_folder"><svg class="icon"><use href="#icon-plus"></use></svg></button>
269
- <select id="sort-select" class="sort-select" onchange="app.setSort(this.value)">
270
- <option value="name-asc" data-i18n="sort_name_asc">Nom (A-Z)</option>
271
- <option value="name-desc" data-i18n="sort_name_desc">Nom (Z-A)</option>
272
- <option value="size-desc" data-i18n="sort_size">Taille</option>
273
- <option value="date-desc" data-i18n="sort_date">Date</option>
274
- </select>
275
- <button class="btn-icon" onclick="app.refresh()" title="Rafraîchir" data-i18n-title="refresh"><svg class="icon"><use href="#icon-refresh"></use></svg></button>
276
- </div>
277
- </div>
278
- <div class="file-area" id="file-area"></div>
279
- </main>
280
- </div>
281
- </div>
282
 
283
- <div id="upload-overlay">
284
- <div style="display:flex;justify-content:space-between;align-items:center;gap:1rem;">
285
- <strong><span data-i18n="transfer">Transfert</span> <span id="upload-count"></span></strong>
286
- <button class="btn-icon" style="width:32px;height:32px;" onclick="app.cancelUpload()"><svg class="icon icon-sm"><use href="#icon-x"></use></svg></button>
287
- </div>
288
- <div class="progress-bar-bg"><div class="progress-bar-fill" id="upload-progress"></div></div>
289
- <div class="upload-status">
290
- <span id="upload-filename"></span>
291
- <span><span id="upload-speed" class="upload-speed"></span> <span id="upload-percent" style="font-weight:600;">0%</span></span>
292
- </div>
293
- </div>
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
294
 
295
- <div class="modal-overlay" id="preview-modal" onclick="if(event.target === this) app.closePreview()">
296
- <div class="modal">
297
- <div id="preview-container"></div>
298
- <div class="preview-controls">
299
- <button class="btn btn-secondary" onclick="app.closePreview()"><span data-i18n="close">Fermer</span></button>
300
- <button class="btn btn-primary" id="preview-download-btn"><svg class="icon icon-sm"><use href="#icon-download"></use></svg> <span data-i18n="download">Télécharger</span></button>
301
- </div>
302
- </div>
303
- </div>
 
 
 
 
 
 
 
304
 
305
- <div class="modal-overlay" id="share-modal" onclick="if(event.target === this) app.closeShare()">
306
- <div class="modal">
307
- <div class="modal-header">
308
- <div class="modal-title"><svg class="icon"><use href="#icon-share"></use></svg> <span data-i18n="share_title">Partager le fichier</span></div>
309
- <button class="close-modal" onclick="app.closeShare()"><svg class="icon"><use href="#icon-x"></use></svg></button>
310
- </div>
311
- <div class="share-section">
312
- <label class="share-label"><svg class="icon icon-sm"><use href="#icon-eye"></use></svg> <span data-i18n="share_preview">Lien de prévisualisation</span></label>
313
- <p class="share-hint" data-i18n="share_preview_hint">Page web avec lecteur intégré</p>
314
- <div class="share-input-group">
315
- <input type="text" id="share-preview-url" readonly class="form-input share-input" value="Génération...">
316
- <button class="btn-icon share-copy-btn" data-type="preview"><svg class="icon"><use href="#icon-link"></use></svg></button>
317
- </div>
318
- </div>
319
- <div class="share-section">
320
- <label class="share-label"><svg class="icon icon-sm"><use href="#icon-download"></use></svg> <span data-i18n="share_dl">Lien de téléchargement direct</span></label>
321
- <p class="share-hint" data-i18n="share_dl_hint">Télécharge le fichier immédiatement</p>
322
- <div class="share-input-group">
323
- <input type="text" id="share-download-url" readonly class="form-input share-input" value="Génération...">
324
- <button class="btn-icon share-copy-btn" data-type="download"><svg class="icon"><use href="#icon-link"></use></svg></button>
325
- </div>
326
- </div>
327
- <div class="share-section">
328
- <label class="share-label"><svg class="icon icon-sm"><use href="#icon-link"></use></svg> <span data-i18n="share_embed">Code d'intégration HTML</span></label>
329
- <p class="share-hint" data-i18n="share_embed_hint">À coller sur un site web ou un forum</p>
330
- <div class="share-input-group">
331
- <input type="text" id="share-embed-code" readonly class="form-input share-input" value="Génération...">
332
- <button class="btn-icon share-copy-btn" data-type="embed"><svg class="icon"><use href="#icon-link"></use></svg></button>
333
- </div>
334
- </div>
335
- <div class="share-footer" data-i18n="share_expiry">Ces liens expirent dans 7 jours</div>
336
- </div>
337
- </div>
 
 
 
 
 
 
 
 
338
 
339
- <div class="modal-overlay" id="tos-modal">
340
- <div class="modal">
341
- <div class="modal-header">
342
- <div class="modal-title" data-i18n="tos_title">Conditions Générales d'Utilisation</div>
343
- <button class="close-modal" onclick="app.hideTos()"><svg class="icon"><use href="#icon-x"></use></svg></button>
344
- </div>
345
- <iframe id="tos-frame" src="/tos.html" class="tos-frame" title="CGU PixelDrive"></iframe>
346
- <div style="display:flex;gap:1rem;">
347
- <button class="btn btn-secondary" style="flex:1;" onclick="app.hideTos()"><span data-i18n="cancel">Annuler</span></button>
348
- <button class="btn btn-primary" id="tos-accept-btn" style="display:none;flex:1;" onclick="app.acceptTos()"><span data-i18n="tos_accept_btn">J'accepte</span></button>
349
- </div>
350
- </div>
351
- </div>
352
 
353
- <div class="modal-overlay" id="dialog-modal">
354
- <div class="modal" style="max-width:420px;">
355
- <div class="modal-header"><div class="modal-title" id="dialog-title"></div></div>
356
- <div class="dialog-message" id="dialog-message"></div>
357
- <div id="dialog-input-wrap" style="display:none;">
358
- <input type="text" id="dialog-input" class="form-input">
359
- </div>
360
- <div class="dialog-actions">
361
- <button class="btn btn-secondary" id="dialog-cancel" onclick="app._dialogClose(false)"><span data-i18n="cancel">Annuler</span></button>
362
- <button class="btn" id="dialog-ok" onclick="app._dialogClose(true)">Confirmer</button>
363
- </div>
364
- </div>
365
- </div>
366
 
367
- <!-- ✅ MODALE SETTINGS -->
368
- <div class="modal-overlay" id="settings-modal" onclick="if(event.target === this) app.closeSettings()">
369
- <div class="modal" style="max-width:460px;">
370
- <div class="modal-header">
371
- <div class="modal-title"><svg class="icon"><use href="#icon-settings"></use></svg> <span data-i18n="settings">Paramètres</span></div>
372
- <button class="close-modal" onclick="app.closeSettings()"><svg class="icon"><use href="#icon-x"></use></svg></button>
373
- </div>
374
- <div class="settings-section">
375
- <label class="share-label" data-i18n="profile">Profil</label>
376
- <div class="settings-avatar-row">
377
- <div class="avatar-preview"><img id="settings-avatar-img" alt="" style="display:none;"><span id="settings-avatar-initial" class="avatar-initial big">?</span></div>
378
- <div style="flex:1;display:flex;flex-direction:column;gap:0.5rem;">
379
- <button class="btn" style="padding:0.6rem;" onclick="document.getElementById('avatar-input').click()"><span data-i18n="change_avatar">Changer l'avatar</span></button>
380
- <input type="file" id="avatar-input" accept="image/*" style="display:none;" onchange="app.uploadAvatar(this)">
381
- <button class="btn btn-secondary" style="padding:0.6rem;" onclick="app.removeAvatar()"><span data-i18n="remove_avatar">Retirer</span></button>
382
- </div>
383
- </div>
384
- </div>
385
- <div class="settings-section">
386
- <label class="share-label" data-i18n="language">Langue</label>
387
- <select id="set-lang" class="form-input" onchange="app.setLang(this.value)"><option value="fr">Français</option><option value="en">English</option></select>
388
- </div>
389
- <div class="settings-section">
390
- <label class="share-label" data-i18n="theme">Thème</label>
391
- <select id="set-theme" class="form-input" onchange="app.setTheme(this.value)"><option value="dark" data-i18n="theme_dark">Sombre</option><option value="light" data-i18n="theme_light">Clair</option></select>
392
- </div>
393
- <div class="settings-section">
394
- <label class="share-label" data-i18n="default_sort">Tri par défaut</label>
395
- <select id="set-sort" class="form-input" onchange="app.setDefSort(this.value)">
396
- <option value="name-asc" data-i18n="sort_name_asc">Nom (A-Z)</option>
397
- <option value="name-desc" data-i18n="sort_name_desc">Nom (Z-A)</option>
398
- <option value="size-desc" data-i18n="sort_size">Taille</option>
399
- <option value="date-desc" data-i18n="sort_date">Date</option>
400
- </select>
401
- </div>
402
- </div>
403
- </div>
404
 
405
- <div id="context-menu">
406
- <div class="ctx-item" id="ctx-preview"><svg class="icon icon-sm"><use href="#icon-eye"></use></svg> <span data-i18n="ctx_preview">Aperçu</span></div>
407
- <div class="ctx-item" id="ctx-download"><svg class="icon icon-sm"><use href="#icon-download"></use></svg> <span data-i18n="ctx_download">Télécharger</span></div>
408
- <div class="ctx-item" id="ctx-share"><svg class="icon icon-sm"><use href="#icon-link"></use></svg> <span data-i18n="ctx_link">Lien public</span></div>
409
- <div class="ctx-item" id="ctx-share-user"><svg class="icon icon-sm"><use href="#icon-user-plus"></use></svg> <span data-i18n="ctx_share_user">Partager à un utilisateur</span></div>
410
- <div class="ctx-sep"></div>
411
- <div class="ctx-item" id="ctx-remove" style="color:var(--danger);"><svg class="icon icon-sm"><use href="#icon-x"></use></svg> <span data-i18n="ctx_remove">Retirer de ma liste</span></div>
412
- <div class="ctx-item" id="ctx-delete" style="color:var(--danger);"><svg class="icon icon-sm"><use href="#icon-trash"></use></svg> <span data-i18n="ctx_delete">Supprimer</span></div>
413
- </div>
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
414
 
415
- <div id="toast-container"></div>
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
416
 
417
- <script src="/i18n.js"></script>
418
- <script src="/app.js"></script>
419
- </body>
420
- </html>
 
1
+ // ==========================================
2
+ // PIXELDRIVE v4.1 - APP.JS (i18n + avatars + settings)
3
+ // ==========================================
4
+ const app = {
5
+ user: null, currentFolder: null, currentSection: 'files', sortMode: 'name-asc',
6
+ files: [], folders: [], uploadController: null, _uploadCancelled: false,
7
+ isMobile: window.innerWidth <= 768, suppressClick: false,
8
+ _dialogResolver: null, _dialogOpts: null,
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
9
 
10
+ async init() {
11
+ // Préférences locales (thème, tri) + i18n
12
+ document.body.classList.toggle('light', (localStorage.getItem('pd_theme') || 'dark') === 'light');
13
+ this.sortMode = localStorage.getItem('pd_sort') || 'name-asc';
14
+ const ss = document.getElementById('sort-select'); if (ss) ss.value = this.sortMode;
15
+ applyI18n();
16
+ this.applyDynamicI18n();
 
 
 
 
 
17
 
18
+ try {
19
+ const res = await fetch('/api/me');
20
+ if (res.ok) {
21
+ this.user = await res.json();
22
+ this.showApp();
23
+ if (!this.user.tosAccepted) this.showTos(true);
24
+ this.loadContent();
25
+ }
26
+ } catch (e) { console.error(e); }
27
+ document.addEventListener('click', (e) => { if (!document.getElementById('context-menu').contains(e.target)) this.closeMenu(); });
28
+ document.getElementById('file-area').addEventListener('contextmenu', (e) => e.preventDefault());
29
+ window.addEventListener('resize', () => {
30
+ this.isMobile = window.innerWidth <= 768;
31
+ if (!this.isMobile) { document.getElementById('sidebar').classList.remove('open'); document.querySelector('.sidebar-overlay').classList.remove('active'); }
32
+ });
33
+ document.getElementById('login-pass').addEventListener('keydown', (e) => { if (e.key === 'Enter') this.login(); });
34
+ document.getElementById('reg-pass2').addEventListener('keydown', (e) => { if (e.key === 'Enter') this.register(); });
35
+ },
36
 
37
+ // ---------- UTILS ----------
38
+ esc(s) { return String(s).replace(/&/g,'&amp;').replace(/</g,'&lt;').replace(/>/g,'&gt;').replace(/"/g,'&quot;'); },
39
+ formatSize(b) { if (b === null || b === undefined) return '—'; if (b < 1024) return b + ' B'; if (b < 1048576) return (b/1024).toFixed(0) + ' KB'; if (b < 1073741824) return (b/1048576).toFixed(1) + ' MB'; return (b/1073741824).toFixed(2) + ' GB'; },
40
+ statusLabel(s) { return ({pending:t('st_pending'), uploading:t('st_uploading'), processing:t('st_processing'), ready:t('st_ready'), error:t('st_error')})[s] || s; },
41
+ formatDate(ts) { return ts ? new Date(ts).toLocaleString('fr-FR', {day:'2-digit',month:'2-digit',year:'numeric',hour:'2-digit',minute:'2-digit'}) : ''; },
42
+ toast(msg, type) {
43
+ const el = document.createElement('div');
44
+ el.className = 'toast ' + (type || 'info');
45
+ el.textContent = msg;
46
+ document.getElementById('toast-container').appendChild(el);
47
+ setTimeout(() => { el.style.opacity = '0'; el.style.transition = 'opacity 0.3s'; setTimeout(() => el.remove(), 300); }, 3500);
48
+ },
 
 
49
 
50
+ // Lignes composites (texte + lien) traduites
51
+ _switchHTML(key, onclick) {
52
+ const s = t(key);
53
+ const i = s.indexOf('?');
54
+ if (i < 0) return this.esc(s);
55
+ return this.esc(s.slice(0, i + 1)) + ' <a onclick="' + onclick + '">' + this.esc(s.slice(i + 1).trim()) + '</a>';
56
+ },
57
+ applyDynamicI18n() {
58
+ const sw1 = document.getElementById('switch-login');
59
+ if (sw1) sw1.innerHTML = this._switchHTML('no_account', "app.toggleAuth('register')");
60
+ const sw2 = document.getElementById('switch-register');
61
+ if (sw2) sw2.innerHTML = this._switchHTML('have_account', "app.toggleAuth('login')");
62
+ },
 
 
63
 
64
+ // DIALOGUES CUSTOM
65
+ dialog(opts) {
66
+ return new Promise(resolve => {
67
+ this._dialogResolver = resolve;
68
+ this._dialogOpts = opts;
69
+ document.getElementById('dialog-title').textContent = opts.title || '';
70
+ const msg = document.getElementById('dialog-message');
71
+ msg.textContent = opts.message || '';
72
+ msg.style.display = opts.message ? 'block' : 'none';
73
+ const wrap = document.getElementById('dialog-input-wrap');
74
+ const input = document.getElementById('dialog-input');
75
+ if (opts.type === 'prompt') {
76
+ wrap.style.display = 'block';
77
+ input.value = opts.value || '';
78
+ input.placeholder = opts.placeholder || '';
79
+ setTimeout(() => input.focus(), 60);
80
+ } else wrap.style.display = 'none';
81
+ input.onkeydown = (e) => { if (e.key === 'Enter') this._dialogClose(true); };
82
+ const ok = document.getElementById('dialog-ok');
83
+ ok.textContent = opts.confirmText || 'Confirmer';
84
+ ok.style.background = opts.danger ? 'var(--danger)' : 'var(--primary)';
85
+ document.getElementById('dialog-modal').style.display = 'flex';
86
+ });
87
+ },
88
+ _dialogClose(okClicked) {
89
+ document.getElementById('dialog-modal').style.display = 'none';
90
+ const opts = this._dialogOpts || {};
91
+ const value = document.getElementById('dialog-input').value.trim();
92
+ const r = this._dialogResolver;
93
+ this._dialogResolver = null;
94
+ if (r) r(okClicked ? (opts.type === 'prompt' ? (value || null) : true) : null);
95
+ },
96
+ confirm(opts) { return this.dialog(Object.assign({ type: 'confirm' }, opts)); },
97
+ prompt(opts) { return this.dialog(Object.assign({ type: 'prompt' }, opts)); },
98
 
99
+ // ---------- AUTH ----------
100
+ toggleAuth(m) { document.getElementById('login-form').style.display = m === 'login' ? 'block' : 'none'; document.getElementById('register-form').style.display = m === 'register' ? 'block' : 'none'; },
101
+ async login() {
102
+ const u = document.getElementById('login-user').value.trim(), p = document.getElementById('login-pass').value;
103
+ if (!u || !p) return this.toast('Champs requis', 'error');
104
+ try {
105
+ const res = await fetch('/api/login', {method:'POST', headers:{'Content-Type':'application/json'}, body: JSON.stringify({u, p})});
106
+ const d = await res.json();
107
+ if (d.ok) { this.user = d.user; this.showApp(); if (!this.user.tosAccepted) this.showTos(true); this.navTo('files'); }
108
+ else this.toast(d.error || 'Erreur', 'error');
109
+ } catch (e) { this.toast('Erreur réseau', 'error'); }
110
+ },
111
+ async register() {
112
+ const u = document.getElementById('reg-user').value.trim(), p = document.getElementById('reg-pass').value, p2 = document.getElementById('reg-pass2').value, tos = document.getElementById('reg-tos').checked;
113
+ if (!u || !p) return this.toast('Champs requis', 'error');
114
+ if (p !== p2) return this.toast('Mots de passe différents', 'error');
115
+ if (!tos) return this.toast('Acceptez les CGU', 'error');
116
+ try {
117
+ const res = await fetch('/api/register', {method:'POST', headers:{'Content-Type':'application/json'}, body: JSON.stringify({u, p, t:true})});
118
+ const d = await res.json();
119
+ if (d.ok) { this.toast('Compte créé !', 'success'); this.toggleAuth('login'); } else this.toast(d.error, 'error');
120
+ } catch (e) { this.toast('Erreur', 'error'); }
121
+ },
122
+ async logout() {
123
+ const ok = await this.confirm({ title: t('dlg_logout_title'), message: t('dlg_logout_msg'), confirmText: t('dlg_logout_title'), danger: true });
124
+ if (!ok) return;
125
+ await fetch('/api/logout', {method:'POST'});
126
+ location.reload();
127
+ },
128
+ showApp() {
129
+ document.getElementById('auth-screen').style.display = 'none';
130
+ document.getElementById('app-screen').style.display = 'flex';
131
+ document.getElementById('user-display').textContent = this.user.username;
132
+ document.getElementById('footer-username').textContent = this.user.username;
133
+ this.loadAvatar();
134
+ },
135
 
136
+ // ---------- CGU ----------
137
+ showTos(force) {
138
+ document.getElementById('tos-modal').style.display = 'flex';
139
+ document.getElementById('tos-accept-btn').style.display = force ? 'flex' : 'none';
140
+ },
141
+ hideTos() { document.getElementById('tos-modal').style.display = 'none'; },
142
+ async acceptTos() { await fetch('/api/tos/accept', {method:'POST'}); this.user.tosAccepted = true; this.hideTos(); this.toast('CGU ✓', 'success'); },
143
 
144
+ // AVATARS
145
+ loadAvatar() {
146
+ if (!this.user) return;
147
+ const url = '/api/users/' + this.user.id + '/avatar?' + Date.now();
148
+ const letter = (this.user.username || '?')[0].toUpperCase();
149
+ const imgs = [document.getElementById('avatar-img'), document.getElementById('settings-avatar-img')];
150
+ const inits = [document.getElementById('avatar-initial'), document.getElementById('settings-avatar-initial')];
151
+ imgs.forEach((img, i) => {
152
+ if (!img) return;
153
+ img.onerror = () => { img.style.display = 'none'; if (inits[i]) { inits[i].style.display = 'flex'; inits[i].textContent = letter; } };
154
+ img.onload = () => { img.style.display = 'block'; if (inits[i]) inits[i].style.display = 'none'; };
155
+ img.src = url;
156
+ });
157
+ },
158
+ async uploadAvatar(input) {
159
+ const f = input.files[0]; input.value = '';
160
+ if (!f) return;
161
+ const reader = new FileReader();
162
+ reader.onload = async () => {
163
+ const res = await fetch('/api/avatar', { method:'POST', headers:{'Content-Type':'application/json'}, body: JSON.stringify({ data: reader.result }) });
164
+ const d = await res.json();
165
+ if (d.ok) { this.toast(t('t_avatar_saved'), 'success'); this.loadAvatar(); }
166
+ else this.toast(d.error || 'Erreur', 'error');
167
+ };
168
+ reader.readAsDataURL(f);
169
+ },
170
+ async removeAvatar() {
171
+ await fetch('/api/avatar', { method:'DELETE' });
172
+ this.toast(t('t_avatar_removed'), 'success');
173
+ this.loadAvatar();
174
+ },
175
 
176
+ // SETTINGS
177
+ openSettings() {
178
+ document.getElementById('settings-modal').style.display = 'flex';
179
+ document.getElementById('set-lang').value = getLang();
180
+ document.getElementById('set-theme').value = localStorage.getItem('pd_theme') || 'dark';
181
+ document.getElementById('set-sort').value = this.sortMode;
182
+ this.loadAvatar();
183
+ },
184
+ closeSettings() { document.getElementById('settings-modal').style.display = 'none'; },
185
+ setLang(l) {
186
+ localStorage.setItem('pd_lang', l);
187
+ applyI18n();
188
+ this.applyDynamicI18n();
189
+ this.toast(t('t_lang_changed'), 'success');
190
+ this.refresh();
191
+ },
192
+ setTheme(th) { localStorage.setItem('pd_theme', th); document.body.classList.toggle('light', th === 'light'); },
193
+ setDefSort(m) { localStorage.setItem('pd_sort', m); this.sortMode = m; const s = document.getElementById('sort-select'); if (s) s.value = m; this.refresh(); },
194
 
195
+ // ---------- NAVIGATION ----------
196
+ toggleSidebar() { document.getElementById('sidebar').classList.toggle('open'); document.querySelector('.sidebar-overlay').classList.toggle('active'); },
197
+ navTo(section) {
198
+ this.currentSection = section;
199
+ this.currentFolder = null;
200
+ document.querySelectorAll('.nav-item').forEach(el => el.classList.remove('active'));
201
+ document.getElementById('nav-' + section).classList.add('active');
202
+ if (this.isMobile) this.toggleSidebar();
203
+ document.getElementById('toolbar-actions').style.display = (section === 'files') ? 'flex' : 'none';
204
+ this.refresh();
205
+ },
206
+ goRoot() {
207
+ if (this.currentSection === 'shared') { this.currentFolder = null; this.loadSharedPage(); return; }
208
+ this.currentFolder = null;
209
+ this.loadContent();
210
+ },
211
+ refresh() {
212
+ if (this.currentSection === 'files') this.loadContent();
213
+ else if (this.currentSection === 'shared') this.currentFolder ? this.loadContent() : this.loadSharedPage();
214
+ else this.loadRecent();
215
+ },
216
+ setSort(m) { this.sortMode = m; this.refresh(); },
217
+ sortFiles(files) {
218
+ const a = [...files];
219
+ if (this.sortMode === 'name-asc') a.sort((x,y) => x.name.localeCompare(y.name, undefined, {sensitivity:'base'}));
220
+ else if (this.sortMode === 'name-desc') a.sort((x,y) => y.name.localeCompare(x.name, undefined, {sensitivity:'base'}));
221
+ else if (this.sortMode === 'size-desc') a.sort((x,y) => (y.size||0) - (x.size||0));
222
+ else a.sort((x,y) => (y.updated_at||y.created_at||0) - (x.updated_at||x.created_at||0));
223
+ return a;
224
+ },
225
 
226
+ // ---------- FILES ----------
227
+ async loadContent() {
228
+ const area = document.getElementById('file-area');
229
+ area.innerHTML = '<div style="padding:2rem;text-align:center;color:var(--text-muted);">' + t('loading') + '</div>';
230
+ const bc = document.getElementById('breadcrumb');
231
+ bc.innerHTML = (this.currentSection === 'shared' ? '<span onclick="app.goRoot()">' + t('nav_shared') + '</span>' : '<span onclick="app.goRoot()">' + t('nav_files') + '</span>') +
232
+ (this.currentFolder ? ' <span class="sep">/</span> <span>' + this.esc(this.currentFolder.name) + '</span>' : '');
233
+ try {
234
+ const [fRes, dRes] = await Promise.all([
235
+ fetch('/api/files?folder_id=' + (this.currentFolder ? this.currentFolder.id : '')),
236
+ fetch('/api/folders?parent_id=' + (this.currentFolder ? this.currentFolder.id : ''))
237
+ ]);
238
+ this.files = await fRes.json();
239
+ this.folders = await dRes.json();
240
+ this.renderGrid(this.folders, this.files, this.currentSection === 'shared' ? 'shared' : 'mine');
241
+ } catch (e) { area.innerHTML = '<div style="padding:2rem;color:var(--danger);text-align:center;">Erreur</div>'; }
242
+ },
243
 
244
+ async loadRecent() {
245
+ const area = document.getElementById('file-area');
246
+ area.innerHTML = '<div style="padding:2rem;text-align:center;color:var(--text-muted);">' + t('loading') + '</div>';
247
+ document.getElementById('breadcrumb').innerHTML = '<span>' + t('nav_recent') + '</span>';
248
+ try {
249
+ const all = [];
250
+ const walk = async (pid) => {
251
+ const fr = await fetch('/api/files?folder_id=' + (pid || ''));
252
+ (await fr.json()).forEach(f => all.push(f));
253
+ const dr = await fetch('/api/folders?parent_id=' + (pid || ''));
254
+ for (const d of await dr.json()) await walk(d.id);
255
+ };
256
+ await walk(null);
257
+ this.renderGrid([], this.sortFiles(all).slice(0, 20), 'mine');
258
+ } catch (e) { area.innerHTML = '<div style="padding:2rem;color:var(--danger);text-align:center;">Erreur</div>'; }
259
+ },
260
 
261
+ renderGrid(folders, files, mode) {
262
+ const area = document.getElementById('file-area');
263
+ area.innerHTML = '';
264
+ if (!folders.length && !files.length) {
265
+ area.innerHTML = '<div class="empty-state"><svg class="icon icon-lg"><use href="#icon-folder"></use></svg><p>' + t('empty_folder') + '</p></div>';
266
+ return;
267
+ }
268
+ const grid = document.createElement('div');
269
+ grid.className = 'grid';
270
 
271
+ folders.forEach(f => {
272
+ const el = document.createElement('div');
273
+ el.className = 'card';
274
+ el.innerHTML = '<div class="card-thumb folder-thumb">' + (f.owner_name ? '<span class="owner-badge">' + this.esc(f.owner_name) + '</span>' : '') + '<svg class="icon"><use href="#icon-folder"></use></svg></div><div class="card-info"><div class="card-name">' + this.esc(f.name) + '</div><div class="card-meta"><span>' + t('folder') + '</span><span></span></div></div>';
275
+ el.onclick = () => { if (this.suppressClick) { this.suppressClick = false; return; } this.currentFolder = f; this.loadContent(); };
276
+ el.oncontextmenu = (e) => this.showContextMenu(e, mode === 'shared' ? 'shared-folder' : 'folder', f);
277
+ this.bindLongPress(el, mode === 'shared' ? 'shared-folder' : 'folder', f);
278
+ grid.appendChild(el);
279
+ });
280
 
281
+ this.sortFiles(files).forEach(f => {
282
+ const el = document.createElement('div');
283
+ el.className = 'card';
284
+ let thumb = '<svg class="icon icon-placeholder"><use href="#icon-file"></use></svg>';
285
+ if (f.mime && (f.mime.startsWith('image/') || f.mime.startsWith('video/'))) thumb = '<img src="/api/files/' + f.id + '/thumb" alt="" loading="lazy">';
286
+ el.innerHTML = '<div class="card-thumb">' + (f.owner_name ? '<span class="owner-badge"><img src="/api/users/' + f.owner_id + '/avatar" onerror="this.remove()">' + this.esc(f.owner_name) + '</span>' : '') + thumb + '</div><div class="card-info"><div class="card-name">' + this.esc(f.name) + '</div><div class="card-meta"><span>' + this.formatSize(f.size) + '</span><span>' + this.statusLabel(f.status) + '</span></div></div>';
287
+ el.onclick = () => { if (this.suppressClick) { this.suppressClick = false; return; } if (f.status === 'ready') this.previewFile(f); };
288
+ el.oncontextmenu = (e) => this.showContextMenu(e, mode === 'shared' ? 'shared-file' : 'file', f);
289
+ this.bindLongPress(el, mode === 'shared' ? 'shared-file' : 'file', f);
290
+ grid.appendChild(el);
291
+ });
292
+ area.appendChild(grid);
293
+ },
294
 
295
+ bindLongPress(el, type, item) {
296
+ let t2 = null;
297
+ el.addEventListener('touchstart', (e) => { t2 = setTimeout(() => { this.suppressClick = true; this.showContextMenu(e, type, item); }, 500); }, {passive:true});
298
+ el.addEventListener('touchend', () => clearTimeout(t2));
299
+ el.addEventListener('touchmove', () => clearTimeout(t2));
300
+ },
301
 
302
+ // ✅ ONGLET PARTAGÉS (3 sections)
303
+ async loadSharedPage() {
304
+ const area = document.getElementById('file-area');
305
+ area.innerHTML = '<div style="padding:2rem;text-align:center;color:var(--text-muted);">' + t('loading') + '</div>';
306
+ document.getElementById('breadcrumb').innerHTML = '<span>' + t('nav_shared') + '</span>';
307
+ try {
308
+ const [shRes, myRes, lkRes] = await Promise.all([fetch('/api/shared-with-me'), fetch('/api/my-shares'), fetch('/api/share-links')]);
309
+ const shared = await shRes.json();
310
+ const myShares = await myRes.json();
311
+ const links = await lkRes.json();
312
+ area.innerHTML = '';
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
313
 
314
+ const t1 = document.createElement('div'); t1.className = 'section-title'; t1.textContent = t('sec_shared_me'); area.appendChild(t1);
315
+ if (!shared.length) {
316
+ area.insertAdjacentHTML('beforeend', '<div class="empty-state" style="padding:2rem 1rem;"><svg class="icon icon-lg"><use href="#icon-share"></use></svg><p>' + t('empty_shared') + '</p></div>');
317
+ } else {
318
+ const grid = document.createElement('div'); grid.className = 'grid';
319
+ shared.forEach(item => {
320
+ const el = document.createElement('div'); el.className = 'card';
321
+ const badge = '<span class="owner-badge"><img src="/api/users/' + item.owner_id + '/avatar" onerror="this.remove()">' + this.esc(item.owner_name) + '</span>';
322
+ if (item.folder_id) {
323
+ el.innerHTML = '<div class="card-thumb folder-thumb">' + badge + '<svg class="icon"><use href="#icon-folder"></use></svg></div><div class="card-info"><div class="card-name">' + this.esc(item.folder_name) + '</div><div class="card-meta"><span>' + t('folder') + '</span><span></span></div></div>';
324
+ el.onclick = () => { this.currentFolder = {id: item.folder_id, name: item.folder_name}; this.loadContent(); };
325
+ el.oncontextmenu = (e) => this.showContextMenu(e, 'shared-folder', item);
326
+ this.bindLongPress(el, 'shared-folder', item);
327
+ } else {
328
+ let thumb = '<svg class="icon icon-placeholder"><use href="#icon-file"></use></svg>';
329
+ if (item.mime && (item.mime.startsWith('image/') || item.mime.startsWith('video/'))) thumb = '<img src="/api/files/' + item.file_id + '/thumb" alt="" loading="lazy">';
330
+ const fobj = Object.assign({share_id: item.share_id}, {id: item.file_id, name: item.file_name, mime: item.mime, size: item.size, status: item.status});
331
+ el.innerHTML = '<div class="card-thumb">' + badge + thumb + '</div><div class="card-info"><div class="card-name">' + this.esc(item.file_name || '—') + '</div><div class="card-meta"><span>' + this.formatSize(item.size) + '</span><span>' + this.statusLabel(item.status) + '</span></div></div>';
332
+ el.onclick = () => { if (item.status === 'ready') this.previewFile(fobj); };
333
+ el.oncontextmenu = (e) => this.showContextMenu(e, 'shared-file', fobj);
334
+ this.bindLongPress(el, 'shared-file', fobj);
335
+ }
336
+ grid.appendChild(el);
337
+ });
338
+ area.appendChild(grid);
339
+ }
340
 
341
+ const t2 = document.createElement('div'); t2.className = 'section-title'; t2.textContent = t('sec_my_shares'); area.appendChild(t2);
342
+ if (!myShares.length) {
343
+ area.insertAdjacentHTML('beforeend', '<div class="empty-state" style="padding:1.5rem 1rem;"><p>' + t('empty_my_shares') + '</p></div>');
344
+ } else {
345
+ const list = document.createElement('div'); list.className = 'share-list';
346
+ myShares.forEach(sh => {
347
+ const name = sh.file_name || sh.folder_name || '—';
348
+ const kind = sh.file_id ? t('folder').replace(t('folder'), sh.file_id ? 'Fichier' : 'Dossier') : 'Dossier';
349
+ const item = document.createElement('div'); item.className = 'share-item';
350
+ item.innerHTML =
351
+ '<div class="share-item-header"><div class="share-item-info"><svg class="icon icon-sm"><use href="#' + (sh.file_id ? 'icon-file' : 'icon-folder') + '"></use></svg><div style="min-width:0;"><div class="share-item-name">' + this.esc(name) + '</div><div class="share-item-sub">' + (sh.file_id ? 'Fichier' : 'Dossier') + ' · ' + t('shared_with') + ' <strong style="color:var(--primary);">' + this.esc(sh.shared_with_name) + '</strong></div></div></div><button class="btn-icon myshare-revoke" data-id="' + sh.id + '"><svg class="icon icon-sm"><use href="#icon-trash"></use></svg></button></div>' +
352
+ '<div class="share-item-footer">' + t('shared_on') + ' ' + this.formatDate(sh.created_at) + '</div>';
353
+ list.appendChild(item);
354
+ });
355
+ area.appendChild(list);
356
+ area.querySelectorAll('.myshare-revoke').forEach(btn => {
357
+ btn.onclick = async () => {
358
+ const ok = await this.confirm({ title: t('revoke'), message: t('dlg_remove_msg'), confirmText: t('revoke'), danger: true });
359
+ if (!ok) return;
360
+ await fetch('/api/my-shares/' + btn.dataset.id, {method:'DELETE'});
361
+ this.toast(t('t_revoked'), 'success');
362
+ this.loadSharedPage();
363
+ };
364
+ });
365
+ }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
366
 
367
+ const t3 = document.createElement('div'); t3.className = 'section-title'; t3.textContent = t('sec_my_links'); area.appendChild(t3);
368
+ if (!links.length) {
369
+ area.insertAdjacentHTML('beforeend', '<div class="empty-state" style="padding:1.5rem 1rem;"><p>' + t('empty_links') + '</p></div>');
370
+ } else {
371
+ const grouped = {};
372
+ links.forEach(l => { if (!grouped[l.file_id]) grouped[l.file_id] = {name: l.file_name, links: []}; grouped[l.file_id].links.push(l); });
373
+ const list = document.createElement('div'); list.className = 'share-list';
374
+ Object.entries(grouped).forEach(([fid, data]) => {
375
+ const emb = data.links.find(l => l.type === 'embed');
376
+ const dl = data.links.find(l => l.type === 'download');
377
+ const exp = this.formatDate(Math.max(...data.links.map(l => l.expires_at || 0)));
378
+ const origin = window.location.origin;
379
+ const item = document.createElement('div'); item.className = 'share-item';
380
+ item.innerHTML =
381
+ '<div class="share-item-header"><div class="share-item-info"><svg class="icon icon-sm"><use href="#icon-file"></use></svg><span class="share-item-name">' + this.esc(data.name || '—') + '</span></div><button class="btn-icon link-del-btn" data-fid="' + fid + '"><svg class="icon icon-sm"><use href="#icon-trash"></use></svg></button></div>' +
382
+ '<div class="share-item-links">' +
383
+ (emb ? '<div class="share-link-row"><span class="share-link-label">' + t('ctx_preview') + '</span><input type="text" readonly class="form-input share-link-input" value="' + origin + '/s/' + emb.token + '"><button class="btn-icon share-copy-btn" data-url="' + origin + '/s/' + emb.token + '"><svg class="icon icon-sm"><use href="#icon-link"></use></svg></button></div>' : '') +
384
+ (dl ? '<div class="share-link-row"><span class="share-link-label">' + t('ctx_download') + '</span><input type="text" readonly class="form-input share-link-input" value="' + origin + '/d/' + dl.token + '"><button class="btn-icon share-copy-btn" data-url="' + origin + '/d/' + dl.token + '"><svg class="icon icon-sm"><use href="#icon-link"></use></svg></button></div>' : '') +
385
+ '</div><div class="share-item-footer">' + t('expires') + ' ' + exp + '</div>';
386
+ list.appendChild(item);
387
+ });
388
+ area.appendChild(list);
389
+ area.querySelectorAll('.share-copy-btn').forEach(btn => {
390
+ btn.onclick = async () => {
391
+ try { await navigator.clipboard.writeText(btn.dataset.url); } catch (e) { btn.previousElementSibling.select(); document.execCommand('copy'); }
392
+ btn.classList.add('copied'); setTimeout(() => btn.classList.remove('copied'), 1500);
393
+ this.toast(t('t_copied'), 'success');
394
+ };
395
+ });
396
+ area.querySelectorAll('.link-del-btn').forEach(btn => {
397
+ btn.onclick = async () => {
398
+ const ok = await this.confirm({ title: t('ctx_delete'), message: t('dlg_links_msg'), confirmText: t('ctx_delete'), danger: true });
399
+ if (!ok) return;
400
+ for (const tk of grouped[btn.dataset.fid].links.map(l => l.token)) await fetch('/api/share-links/' + tk, {method:'DELETE'});
401
+ this.toast(t('t_deleted'), 'success');
402
+ this.loadSharedPage();
403
+ };
404
+ });
405
+ }
406
+ } catch (e) { area.innerHTML = '<div style="padding:2rem;color:var(--danger);text-align:center;">Erreur</div>'; }
407
+ },
408
 
409
+ // MULTI-UPLOAD + vitesse
410
+ async handleUpload(input) {
411
+ const files = Array.from(input.files);
412
+ input.value = '';
413
+ if (!files.length) return;
414
+ document.getElementById('upload-overlay').style.display = 'block';
415
+ let okCount = 0, errCount = 0;
416
+ for (let i = 0; i < files.length; i++) {
417
+ const result = await this.uploadOne(files[i], i + 1, files.length);
418
+ if (result) okCount++; else errCount++;
419
+ if (this._uploadCancelled) { this._uploadCancelled = false; break; }
420
+ }
421
+ document.getElementById('upload-overlay').style.display = 'none';
422
+ if (this.currentSection === 'files') this.loadContent();
423
+ if (okCount) this.toast(okCount + ' ✓' + (errCount ? ', ' + errCount + ' ✗' : ''), okCount && !errCount ? 'success' : 'error');
424
+ },
425
 
426
+ uploadOne(file, idx, total) {
427
+ return new Promise(async (resolve) => {
428
+ const t0 = performance.now();
429
+ document.getElementById('upload-count').textContent = '(' + idx + '/' + total + ')';
430
+ document.getElementById('upload-filename').textContent = file.name;
431
+ document.getElementById('upload-progress').style.width = '0%';
432
+ document.getElementById('upload-percent').textContent = '0%';
433
+ document.getElementById('upload-speed').textContent = '';
434
+ const fd = new FormData();
435
+ fd.append('file', file);
436
+ if (this.currentFolder) fd.append('folder_id', this.currentFolder.id);
437
+ this.uploadController = new AbortController();
438
+ const finish = (ok) => {
439
+ const secs = (performance.now() - t0) / 1000;
440
+ const mbps = (file.size / 1048576) / secs;
441
+ if (ok && isFinite(mbps)) document.getElementById('upload-speed').textContent = mbps.toFixed(2) + ' MB/s';
442
+ setTimeout(() => resolve(ok), 600);
443
+ };
444
+ try {
445
+ const res = await fetch('/api/files/upload', {method:'POST', body: fd, signal: this.uploadController.signal});
446
+ if (!res.ok) throw new Error('fail');
447
+ const d = await res.json();
448
+ const es = new EventSource('/api/files/' + d.fileId + '/progress');
449
+ es.onmessage = (e) => {
450
+ const data = JSON.parse(e.data);
451
+ if (data.type === 'progress') {
452
+ document.getElementById('upload-progress').style.width = Math.round(data.progress) + '%';
453
+ document.getElementById('upload-percent').textContent = Math.round(data.progress) + '%';
454
+ const secs = (performance.now() - t0) / 1000;
455
+ if (secs > 0.5) document.getElementById('upload-speed').textContent = ((file.size / 1048576) / secs).toFixed(2) + ' MB/s';
456
+ }
457
+ if (data.type === 'done') { es.close(); finish(true); }
458
+ if (data.type === 'error') { es.close(); this.toast('Erreur: ' + file.name, 'error'); finish(false); }
459
+ };
460
+ es.onerror = () => { es.close(); finish(false); };
461
+ } catch (e) {
462
+ if (e.name === 'AbortError') { this._uploadCancelled = true; resolve(false); }
463
+ else { this.toast('Erreur upload: ' + file.name, 'error'); finish(false); }
464
+ }
465
+ });
466
+ },
467
 
468
+ cancelUpload() { if (this.uploadController) this.uploadController.abort(); document.getElementById('upload-overlay').style.display = 'none'; },
 
 
 
 
 
 
 
 
 
 
 
 
469
 
470
+ async createFolder() {
471
+ const name = await this.prompt({ title: t('new_folder'), placeholder: t('new_folder'), confirmText: t('new_folder') });
472
+ if (!name) return;
473
+ const res = await fetch('/api/folders', {method:'POST', headers:{'Content-Type':'application/json'}, body: JSON.stringify({n: name, p: this.currentFolder ? this.currentFolder.id : null})});
474
+ if (res.ok) { this.toast(t('t_folder_created'), 'success'); this.loadContent(); }
475
+ else this.toast('Erreur', 'error');
476
+ },
 
 
 
 
 
 
477
 
478
+ previewFile(file) {
479
+ const container = document.getElementById('preview-container');
480
+ container.innerHTML = '';
481
+ document.getElementById('preview-modal').style.display = 'flex';
482
+ const url = '/api/files/' + file.id + '/download';
483
+ document.getElementById('preview-download-btn').onclick = () => { const a = document.createElement('a'); a.href = url; a.download = file.name; a.click(); };
484
+ if (file.mime && file.mime.startsWith('image/')) { const i = document.createElement('img'); i.src = url; i.id = 'preview-content'; container.appendChild(i); }
485
+ else if (file.mime && file.mime.startsWith('video/')) { const v = document.createElement('video'); v.src = url; v.controls = true; v.autoplay = true; v.playsInline = true; v.id = 'preview-content'; container.appendChild(v); }
486
+ else if (file.mime && file.mime.startsWith('audio/')) { const a = document.createElement('audio'); a.src = url; a.controls = true; container.appendChild(a); }
487
+ else container.innerHTML = '<div style="text-align:center;padding:2rem;"><p style="color:var(--text-muted);">—</p><p style="font-weight:500;margin-top:0.5rem;">' + this.esc(file.name) + '</p></div>';
488
+ },
489
+ closePreview() {
490
+ const c = document.getElementById('preview-container');
491
+ const v = c.querySelector('video'); const a = c.querySelector('audio');
492
+ if (v) v.pause(); if (a) a.pause();
493
+ c.innerHTML = '';
494
+ document.getElementById('preview-modal').style.display = 'none';
495
+ },
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
496
 
497
+ async showShareModal(file) {
498
+ document.getElementById('share-modal').style.display = 'flex';
499
+ const pi = document.getElementById('share-preview-url'), di = document.getElementById('share-download-url'), ei = document.getElementById('share-embed-code');
500
+ pi.value = di.value = ei.value = '...';
501
+ try {
502
+ const res = await fetch('/api/files/' + file.id + '/share', {method:'POST'});
503
+ if (!res.ok) throw new Error();
504
+ const d = await res.json();
505
+ const o = window.location.origin;
506
+ pi.value = o + d.preview;
507
+ di.value = o + d.download;
508
+ ei.value = file.mime.startsWith('image/') ? '<img src="' + o + d.download + '" alt="">' :
509
+ file.mime.startsWith('video/') ? '<video controls src="' + o + d.download + '"></video>' :
510
+ file.mime.startsWith('audio/') ? '<audio controls src="' + o + d.download + '"></audio>' :
511
+ '<a href="' + o + d.download + '">' + this.esc(file.name) + '</a>';
512
+ document.querySelectorAll('.share-copy-btn').forEach(b => { b.onclick = () => this.copyShare(b.dataset.type); });
513
+ } catch (e) { this.toast('Erreur', 'error'); this.closeShare(); }
514
+ },
515
+ closeShare() { document.getElementById('share-modal').style.display = 'none'; },
516
+ async copyShare(type) {
517
+ const id = type === 'preview' ? 'share-preview-url' : type === 'download' ? 'share-download-url' : 'share-embed-code';
518
+ const input = document.getElementById(id);
519
+ if (!input.value || input.value === '...') return;
520
+ try { await navigator.clipboard.writeText(input.value); } catch (e) { input.select(); document.execCommand('copy'); }
521
+ const btn = document.querySelector('.share-copy-btn[data-type="' + type + '"]');
522
+ if (btn) { btn.classList.add('copied'); setTimeout(() => btn.classList.remove('copied'), 1500); }
523
+ this.toast(t('t_copied'), 'success');
524
+ },
525
+
526
+ async shareWithUser(type, item) {
527
+ const username = await this.prompt({ title: t('ctx_share_user'), placeholder: t('username'), confirmText: t('ctx_share_user') });
528
+ if (!username) return;
529
+ const res = await fetch('/api/share-user', {method:'POST', headers:{'Content-Type':'application/json'}, body: JSON.stringify({type, id: item.id, username})});
530
+ const d = await res.json();
531
+ this.toast(d.ok ? '✓ ' + username : (d.error || 'Erreur'), d.ok ? 'success' : 'error');
532
+ },
533
+
534
+ showContextMenu(e, type, item) {
535
+ try { e.preventDefault(); } catch (err) {}
536
+ e.stopPropagation();
537
+ const menu = document.getElementById('context-menu');
538
+ menu.style.display = 'block';
539
+ if (this.isMobile) { menu.style.left = '0'; menu.style.right = '0'; menu.style.bottom = '0'; menu.style.top = 'auto'; menu.style.width = '100%'; }
540
+ else {
541
+ const cx = e.pageX || 0, cy = e.pageY || 0;
542
+ menu.style.left = cx + 'px'; menu.style.top = cy + 'px'; menu.style.bottom = 'auto'; menu.style.right = 'auto'; menu.style.width = 'auto';
543
+ const r = menu.getBoundingClientRect();
544
+ if (r.right > window.innerWidth) menu.style.left = (cx - r.width) + 'px';
545
+ if (r.bottom > window.innerHeight) menu.style.top = (cy - r.height) + 'px';
546
+ }
547
+ const isMineFile = type === 'file', isMineFolder = type === 'folder';
548
+ const isSharedFile = type === 'shared-file', isSharedFolder = type === 'shared-folder';
549
+ document.getElementById('ctx-preview').style.display = (isMineFile || isSharedFile) ? 'flex' : 'none';
550
+ document.getElementById('ctx-download').style.display = (isMineFile || isSharedFile) ? 'flex' : 'none';
551
+ document.getElementById('ctx-share').style.display = isMineFile ? 'flex' : 'none';
552
+ document.getElementById('ctx-share-user').style.display = (isMineFile || isMineFolder) ? 'flex' : 'none';
553
+ document.getElementById('ctx-remove').style.display = (isSharedFile || isSharedFolder) ? 'flex' : 'none';
554
+ document.getElementById('ctx-delete').style.display = (isMineFile || isMineFolder) ? 'flex' : 'none';
555
 
556
+ document.getElementById('ctx-preview').onclick = () => { this.closeMenu(); this.previewFile(item); };
557
+ document.getElementById('ctx-download').onclick = () => { this.closeMenu(); const a = document.createElement('a'); a.href = '/api/files/' + item.id + '/download'; a.download = item.name; a.click(); };
558
+ document.getElementById('ctx-share').onclick = () => { this.closeMenu(); this.showShareModal(item); };
559
+ document.getElementById('ctx-share-user').onclick = () => { this.closeMenu(); this.shareWithUser(isMineFile ? 'file' : 'folder', item); };
560
+ document.getElementById('ctx-remove').onclick = async () => {
561
+ this.closeMenu();
562
+ const ok = await this.confirm({ title: t('ctx_remove'), message: t('dlg_remove_msg'), confirmText: t('ctx_remove'), danger: true });
563
+ if (!ok) return;
564
+ await fetch('/api/shared-with-me/' + item.share_id, {method:'DELETE'});
565
+ this.toast(t('t_removed'), 'success');
566
+ this.loadSharedPage();
567
+ };
568
+ document.getElementById('ctx-delete').onclick = async () => {
569
+ this.closeMenu();
570
+ const ok = await this.confirm({ title: t('ctx_delete'), message: t('dlg_delete_msg') + ' "' + (item.name || '') + '" ?', confirmText: t('ctx_delete'), danger: true });
571
+ if (!ok) return;
572
+ await fetch('/api/' + (isMineFile ? 'files' : 'folders') + '/' + item.id, {method:'DELETE'});
573
+ this.toast(t('t_deleted'), 'success');
574
+ this.refresh();
575
+ };
576
+ },
577
+ closeMenu() { document.getElementById('context-menu').style.display = 'none'; }
578
+ };
579
 
580
+ app.init();