mohsin-devs commited on
Commit
af270a2
·
1 Parent(s): 05f2e3b

Fix file options menu and starred sync behavior

Browse files
Files changed (4) hide show
  1. js/main.js +43 -12
  2. js/state/stateManager.js +32 -0
  3. js/ui/uiRenderer.js +34 -5
  4. styles.css +8 -0
js/main.js CHANGED
@@ -240,6 +240,34 @@ class App {
240
  return parseFloat((bytes / Math.pow(k, i)).toFixed(2)) + ' ' + sizes[i];
241
  }
242
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
243
  render() {
244
  const browseMode = this.state.currentBrowse;
245
  let displayFiles = [];
@@ -256,7 +284,7 @@ class App {
256
  displayFiles = this.state.recent;
257
  document.getElementById('breadcrumbs').innerHTML = '<span class="breadcrumb-item active">Recent</span>';
258
  } else if (browseMode === 'starred') {
259
- displayFiles = this.state.cachedFiles.filter(f => this.state.starred.includes(f.path));
260
  document.getElementById('breadcrumbs').innerHTML = '<span class="breadcrumb-item active">Starred</span>';
261
  }
262
 
@@ -276,7 +304,11 @@ class App {
276
  onPreview: (file) => this.openPreview(file),
277
  onDownload: (url, name) => this.downloadFile(url, name),
278
  onRename: (path, name) => this.openRenameModal(path, name),
279
- onStar: (path) => this.state.toggleStar(path),
 
 
 
 
280
  onDelete: (path, name) => this.openDeleteModal(path, name),
281
  onHistory: (path, name) => this.openHistory(path, name),
282
  getUrl: (path) => getFileUrl(this.hf.apiBase || '/api', path)
@@ -406,23 +438,19 @@ class App {
406
  if (!this.pendingDelete) return;
407
  const path = this.pendingDelete;
408
  const btn = document.getElementById('confirmDeleteBtn');
409
-
410
- // Check if item still exists in local set to avoid stale deletes
411
- const exists = this.state.cachedFiles.some(f => f.path === path) || this.cachedFolders.some(f => f.path === path);
412
- if (!exists) {
413
- this.ui.showToast('Item no longer exists', 'warning');
414
- this.pendingDelete = null;
415
- document.getElementById('deleteModal').classList.remove('active');
416
- return;
417
- }
418
 
419
  if (btn) btn.classList.add('loading');
420
  try {
421
  const isFolder = this.cachedFolders.some(f => f.path === path);
422
  if (isFolder) {
423
  await this.hf.deleteFolder(path);
 
 
 
424
  } else {
425
  await this.hf.deleteFile(path);
 
 
426
  }
427
  this.ui.showToast('Deleted successfully', 'success');
428
  document.getElementById('deleteModal').classList.remove('active');
@@ -470,11 +498,14 @@ class App {
470
 
471
  const path = this.pendingRename.path;
472
  const oldName = this.pendingRename.originalName;
473
-
 
 
474
  btn.classList.add('loading');
475
  try {
476
  const res = await this.hf.renameItem(path, newName);
477
  if (res.success) {
 
478
  this.ui.showToast(`Renamed "${oldName}" to "${newName}"`, 'success');
479
  document.getElementById('renameModal').classList.remove('active');
480
  this.pendingRename = null;
 
240
  return parseFloat((bytes / Math.pow(k, i)).toFixed(2)) + ' ' + sizes[i];
241
  }
242
 
243
+ buildStarredDisplayFiles() {
244
+ const cachedByPath = new Map(this.state.cachedFiles.map(file => [file.path, file]));
245
+ const recentByPath = new Map(this.state.recent.map(file => [file.path, file]));
246
+
247
+ return this.state.starred.map((path) => {
248
+ const cached = cachedByPath.get(path);
249
+ if (cached) return cached;
250
+
251
+ const recent = recentByPath.get(path);
252
+ if (recent) {
253
+ return {
254
+ path,
255
+ name: recent.name || path.split('/').pop(),
256
+ size: recent.size || 0,
257
+ type: 'file',
258
+ lastModified: recent.lastModified
259
+ };
260
+ }
261
+
262
+ return {
263
+ path,
264
+ name: path.split('/').pop(),
265
+ size: 0,
266
+ type: 'file'
267
+ };
268
+ });
269
+ }
270
+
271
  render() {
272
  const browseMode = this.state.currentBrowse;
273
  let displayFiles = [];
 
284
  displayFiles = this.state.recent;
285
  document.getElementById('breadcrumbs').innerHTML = '<span class="breadcrumb-item active">Recent</span>';
286
  } else if (browseMode === 'starred') {
287
+ displayFiles = this.buildStarredDisplayFiles();
288
  document.getElementById('breadcrumbs').innerHTML = '<span class="breadcrumb-item active">Starred</span>';
289
  }
290
 
 
304
  onPreview: (file) => this.openPreview(file),
305
  onDownload: (url, name) => this.downloadFile(url, name),
306
  onRename: (path, name) => this.openRenameModal(path, name),
307
+ onStar: (path) => {
308
+ const wasStarred = this.state.starred.includes(path);
309
+ this.state.toggleStar(path);
310
+ this.ui.showToast(wasStarred ? 'Removed from Starred' : 'Added to Starred', 'success');
311
+ },
312
  onDelete: (path, name) => this.openDeleteModal(path, name),
313
  onHistory: (path, name) => this.openHistory(path, name),
314
  getUrl: (path) => getFileUrl(this.hf.apiBase || '/api', path)
 
438
  if (!this.pendingDelete) return;
439
  const path = this.pendingDelete;
440
  const btn = document.getElementById('confirmDeleteBtn');
 
 
 
 
 
 
 
 
 
441
 
442
  if (btn) btn.classList.add('loading');
443
  try {
444
  const isFolder = this.cachedFolders.some(f => f.path === path);
445
  if (isFolder) {
446
  await this.hf.deleteFolder(path);
447
+ this.state.starred
448
+ .filter(starredPath => starredPath === path || starredPath.startsWith(`${path}/`))
449
+ .forEach(starredPath => this.state.removeStar(starredPath));
450
  } else {
451
  await this.hf.deleteFile(path);
452
+ this.state.removeStar(path);
453
+ this.state.removeRecent(path);
454
  }
455
  this.ui.showToast('Deleted successfully', 'success');
456
  document.getElementById('deleteModal').classList.remove('active');
 
498
 
499
  const path = this.pendingRename.path;
500
  const oldName = this.pendingRename.originalName;
501
+ const parentPath = path.includes('/') ? path.slice(0, path.lastIndexOf('/')) : '';
502
+ const newPath = parentPath ? `${parentPath}/${newName}` : newName;
503
+
504
  btn.classList.add('loading');
505
  try {
506
  const res = await this.hf.renameItem(path, newName);
507
  if (res.success) {
508
+ this.state.replacePathReferences(path, newPath, newName);
509
  this.ui.showToast(`Renamed "${oldName}" to "${newName}"`, 'success');
510
  document.getElementById('renameModal').classList.remove('active');
511
  this.pendingRename = null;
js/state/stateManager.js CHANGED
@@ -73,6 +73,38 @@ class StateManager {
73
  this.notify('TOGGLE_STAR', path);
74
  }
75
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
76
  addToRecent(file) {
77
  if (!file || !file.path) return;
78
  this.recent = [file, ...this.recent.filter(f => f.path !== file.path)].slice(0, 20);
 
73
  this.notify('TOGGLE_STAR', path);
74
  }
75
 
76
+ removeStar(path) {
77
+ if (typeof path !== 'string') return;
78
+ this.starred = this.starred.filter(p => p !== path);
79
+ this.save(STARRED_KEY, this.starred);
80
+ this.notify('REMOVE_STAR', path);
81
+ }
82
+
83
+ removeRecent(path) {
84
+ if (typeof path !== 'string') return;
85
+ this.recent = this.recent.filter(f => f.path !== path);
86
+ this.save(RECENT_KEY, this.recent);
87
+ this.notify('REMOVE_RECENT', path);
88
+ }
89
+
90
+ replacePathReferences(oldPath, newPath, newName = null) {
91
+ if (typeof oldPath !== 'string' || typeof newPath !== 'string') return;
92
+
93
+ this.starred = this.starred.map(p => (p === oldPath ? newPath : p));
94
+ this.save(STARRED_KEY, this.starred);
95
+
96
+ this.recent = this.recent.map(item => {
97
+ if (item.path !== oldPath) return item;
98
+ return {
99
+ ...item,
100
+ path: newPath,
101
+ name: newName || item.name
102
+ };
103
+ });
104
+ this.save(RECENT_KEY, this.recent);
105
+ this.notify('REPLACE_PATH_REFERENCES', { oldPath, newPath });
106
+ }
107
+
108
  addToRecent(file) {
109
  if (!file || !file.path) return;
110
  this.recent = [file, ...this.recent.filter(f => f.path !== file.path)].slice(0, 20);
js/ui/uiRenderer.js CHANGED
@@ -134,7 +134,7 @@ export class UIRenderer {
134
  files.forEach(file => {
135
  const card = document.createElement('div');
136
  card.className = mode === 'grid' ? 'file-card' : 'file-list-item';
137
-
138
  const { icon, color } = getFileIcon(file.name);
139
  const ext = getExt(file.name).toUpperCase();
140
  const size = formatSize(file.size);
@@ -144,7 +144,7 @@ export class UIRenderer {
144
  if (mode === 'grid') {
145
  const isImg = isImage(file.name);
146
  const previewHTML = isImg
147
- ? `<img src="${url}" alt="${file.name}" loading="lazy" onerror="this.parentElement.innerHTML='<span class=\\'file-icon\\'>📄</span>'">`
148
  : `<span class="file-icon">${getFileEmoji(ext)}</span>`;
149
 
150
  card.innerHTML = `
@@ -154,10 +154,24 @@ export class UIRenderer {
154
  <h4 class="file-name" title="${file.name}">${file.name}</h4>
155
  <p class="file-meta">${size} • ${file.lastModified ? formatDate(file.lastModified) : 'Recently'}</p>
156
  </div>
157
- <div class="file-actions">⋮</div>
 
 
 
 
 
 
 
 
 
 
 
 
 
158
  <div class="quick-actions">
159
  <button class="quick-btn" data-action="preview" title="Preview"><i class="ph-fill ph-eye"></i></button>
160
  <button class="quick-btn" data-action="download" title="Download"><i class="ph-fill ph-download-simple"></i></button>
 
161
  <button class="quick-btn" data-action="rename" title="Rename"><i class="ph-fill ph-pencil-simple"></i></button>
162
  <button class="quick-btn" data-action="history" title="History"><i class="ph-fill ph-clock-counter-clockwise"></i></button>
163
  </div>`;
@@ -182,10 +196,11 @@ export class UIRenderer {
182
  actions.onPreview(file);
183
  };
184
 
185
- // Action Handlers
186
  card.addEventListener('click', (e) => {
187
  const btn = e.target.closest('[data-action]');
188
  if (!btn) return;
 
 
189
  const action = btn.dataset.action;
190
  if (action === 'preview') actions.onPreview(file);
191
  else if (action === 'download') actions.onDownload(url, file.name);
@@ -195,10 +210,24 @@ export class UIRenderer {
195
  else if (action === 'history') actions.onHistory(file.path, file.name);
196
  });
197
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
198
  this.containers.files.appendChild(card);
199
  });
200
  }
201
-
202
  showEmpty() {
203
  this.containers.files.innerHTML = `
204
  <div class="empty-state">
 
134
  files.forEach(file => {
135
  const card = document.createElement('div');
136
  card.className = mode === 'grid' ? 'file-card' : 'file-list-item';
137
+
138
  const { icon, color } = getFileIcon(file.name);
139
  const ext = getExt(file.name).toUpperCase();
140
  const size = formatSize(file.size);
 
144
  if (mode === 'grid') {
145
  const isImg = isImage(file.name);
146
  const previewHTML = isImg
147
+ ? `<img src="${url}" alt="${file.name}" loading="lazy" onerror="this.parentElement.innerHTML='<span class=\\'file-icon\\'>&#128196;</span>'">`
148
  : `<span class="file-icon">${getFileEmoji(ext)}</span>`;
149
 
150
  card.innerHTML = `
 
154
  <h4 class="file-name" title="${file.name}">${file.name}</h4>
155
  <p class="file-meta">${size} • ${file.lastModified ? formatDate(file.lastModified) : 'Recently'}</p>
156
  </div>
157
+ <div class="file-actions card-menu">
158
+ <button class="icon-btn card-menu-btn" title="More options"><i class="ph-bold ph-dots-three-vertical"></i></button>
159
+ <div class="dropdown-menu">
160
+ <button class="dropdown-item" data-action="star">
161
+ <i class="ph-fill ph-star"></i> ${starred ? 'Unstar' : 'Star'}
162
+ </button>
163
+ <button class="dropdown-item" data-action="rename">
164
+ <i class="ph-fill ph-pencil-simple"></i> Rename
165
+ </button>
166
+ <button class="dropdown-item danger" data-action="delete">
167
+ <i class="ph-fill ph-trash"></i> Delete
168
+ </button>
169
+ </div>
170
+ </div>
171
  <div class="quick-actions">
172
  <button class="quick-btn" data-action="preview" title="Preview"><i class="ph-fill ph-eye"></i></button>
173
  <button class="quick-btn" data-action="download" title="Download"><i class="ph-fill ph-download-simple"></i></button>
174
+ <button class="quick-btn" data-action="star" title="${starred ? 'Unstar' : 'Star'}"><i class="ph-fill ph-star${starred ? '' : '-bold'}"></i></button>
175
  <button class="quick-btn" data-action="rename" title="Rename"><i class="ph-fill ph-pencil-simple"></i></button>
176
  <button class="quick-btn" data-action="history" title="History"><i class="ph-fill ph-clock-counter-clockwise"></i></button>
177
  </div>`;
 
196
  actions.onPreview(file);
197
  };
198
 
 
199
  card.addEventListener('click', (e) => {
200
  const btn = e.target.closest('[data-action]');
201
  if (!btn) return;
202
+ e.stopPropagation();
203
+ card.querySelectorAll('.dropdown-menu.open').forEach(m => m.classList.remove('open'));
204
  const action = btn.dataset.action;
205
  if (action === 'preview') actions.onPreview(file);
206
  else if (action === 'download') actions.onDownload(url, file.name);
 
210
  else if (action === 'history') actions.onHistory(file.path, file.name);
211
  });
212
 
213
+ const menuBtn = card.querySelector('.card-menu-btn');
214
+ const menu = card.querySelector('.dropdown-menu');
215
+ if (menuBtn && menu) {
216
+ menuBtn.onclick = (e) => {
217
+ e.preventDefault();
218
+ e.stopPropagation();
219
+ document.querySelectorAll('.dropdown-menu.open').forEach(m => {
220
+ if (m !== menu) m.classList.remove('open');
221
+ });
222
+ menu.classList.toggle('open');
223
+ };
224
+
225
+ menu.onclick = (e) => e.stopPropagation();
226
+ }
227
+
228
  this.containers.files.appendChild(card);
229
  });
230
  }
 
231
  showEmpty() {
232
  this.containers.files.innerHTML = `
233
  <div class="empty-state">
styles.css CHANGED
@@ -732,6 +732,14 @@ body {
732
  border: 1px solid var(--border-color);
733
  }
734
 
 
 
 
 
 
 
 
 
735
  .file-card:hover .file-actions {
736
  opacity: 1;
737
  }
 
732
  border: 1px solid var(--border-color);
733
  }
734
 
735
+ .file-actions .card-menu-btn {
736
+ width: 100%;
737
+ height: 100%;
738
+ padding: 0;
739
+ border-radius: 50%;
740
+ font-size: 16px;
741
+ }
742
+
743
  .file-card:hover .file-actions {
744
  opacity: 1;
745
  }