gh-here 2.1.0 → 3.0.1

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.js CHANGED
@@ -1,1896 +1,270 @@
1
- // Octicons directly embedded for frontend use
2
- const octicons = {
3
- 'file-directory': '<svg version="1.1" width="16" height="16" viewBox="0 0 16 16" class="octicon octicon-file-directory" aria-hidden="true"><path d="M0 2.75C0 1.784.784 1 1.75 1H5c.55 0 1.07.26 1.4.7l.9 1.2a.25.25 0 0 0 .2.1h6.75c.966 0 1.75.784 1.75 1.75v8.5A1.75 1.75 0 0 1 14.25 15H1.75A1.75 1.75 0 0 1 0 13.25Zm1.75-.25a.25.25 0 0 0-.25.25v10.5c0 .138.112.25.25.25h12.5a.25.25 0 0 0 .25-.25v-8.5a.25.25 0 0 0-.25-.25H7.5c-.55 0-1.07-.26-1.4-.7l-.9-1.2a.25.25 0 0 0-.2-.1Z"></path></svg>',
4
- 'git-commit': '<svg version="1.1" width="16" height="16" viewBox="0 0 16 16" class="octicon octicon-git-commit" aria-hidden="true"><path d="M11.93 8.5a4.002 4.002 0 0 1-7.86 0H.75a.75.75 0 0 1 0-1.5h3.32a4.002 4.002 0 0 1 7.86 0h3.32a.75.75 0 0 1 0 1.5Zm-1.43-.75a2.5 2.5 0 1 0-5 0 2.5 2.5 0 0 0 5 0Z"></path></svg>',
5
- 'diff': '<svg version="1.1" width="16" height="16" viewBox="0 0 16 16" class="octicon octicon-diff" aria-hidden="true"><path d="M8.75 1.75V5H12a.75.75 0 0 1 0 1.5H8.75v3.25a.75.75 0 0 1-1.5 0V6.5H4A.75.75 0 0 1 4 5h3.25V1.75a.75.75 0 0 1 1.5 0ZM4 13h8a.75.75 0 0 1 0 1.5H4A.75.75 0 0 1 4 13Z"></path></svg>',
6
- 'copy': '<svg version="1.1" width="16" height="16" viewBox="0 0 16 16" class="octicon octicon-copy" aria-hidden="true"><path d="M0 6.75C0 5.784.784 5 1.75 5h1.5a.75.75 0 0 1 0 1.5h-1.5a.25.25 0 0 0-.25.25v7.5c0 .138.112.25.25.25h7.5a.25.25 0 0 0 .25-.25v-1.5a.75.75 0 0 1 1.5 0v1.5A1.75 1.75 0 0 1 9.25 16h-7.5A1.75 1.75 0 0 1 0 14.25Z"></path><path d="M5 1.75C5 .784 5.784 0 6.75 0h7.5C15.216 0 16 .784 16 1.75v7.5A1.75 1.75 0 0 1 14.25 11h-7.5A1.75 1.75 0 0 1 5 9.25Zm1.75-.25a.25.25 0 0 0-.25.25v7.5c0 .138.112.25.25.25h7.5a.25.25 0 0 0 .25-.25v-7.5a.25.25 0 0 0-.25-.25Z"></path></svg>',
7
- 'download': '<svg version="1.1" width="16" height="16" viewBox="0 0 16 16" class="octicon octicon-download" aria-hidden="true"><path d="M2.75 14A1.75 1.75 0 0 1 1 12.25v-2.5a.75.75 0 0 1 1.5 0v2.5c0 .138.112.25.25.25h10.5a.25.25 0 0 0 .25-.25v-2.5a.75.75 0 0 1 1.5 0v2.5A1.75 1.75 0 0 1 13.25 14Z"></path><path d="M7.25 7.689V2a.75.75 0 0 1 1.5 0v5.689l1.97-1.969a.749.749 0 1 1 1.06 1.06l-3.25 3.25a.749.749 0 0 1-1.06 0L4.22 6.78a.749.749 0 1 1 1.06-1.06l1.97 1.969Z"></path></svg>',
8
- 'pencil': '<svg version="1.1" width="16" height="16" viewBox="0 0 16 16" class="octicon octicon-pencil" aria-hidden="true"><path d="M11.013 1.427a1.75 1.75 0 0 1 2.474 0l1.086 1.086a1.75 1.75 0 0 1 0 2.474l-8.61 8.61c-.21.21-.47.364-.756.445l-3.251.93a.75.75 0 0 1-.927-.928l.929-3.25c.081-.286.235-.547.445-.758l8.61-8.61Zm.176 4.823L9.75 4.81l-6.286 6.287a.253.253 0 0 0-.064.108l-.558 1.953 1.953-.558a.253.253 0 0 0 .108-.064Zm1.238-3.763a.25.25 0 0 0-.354 0L10.811 3.75l1.439 1.44 1.263-1.263a.25.25 0 0 0 0-.354Z"></path></svg>',
9
- 'x': '<svg version="1.1" width="16" height="16" viewBox="0 0 16 16" class="octicon octicon-x" aria-hidden="true"><path d="M3.72 3.72a.75.75 0 0 1 1.06 0L8 6.94l3.22-3.22a.749.749 0 0 1 1.275.326.749.749 0 0 1-.215.734L9.06 8l3.22 3.22a.749.749 0 0 1-.326 1.275.749.749 0 0 1-.734-.215L8 9.06l-3.22 3.22a.751.751 0 0 1-1.042-.018.751.751 0 0 1-.018-1.042L6.94 8 3.72 4.78a.75.75 0 0 1 0-1.06Z"></path></svg>',
10
- 'check': '<svg version="1.1" width="16" height="16" viewBox="0 0 16 16" class="octicon octicon-check" aria-hidden="true"><path d="M13.78 4.22a.75.75 0 0 1 0 1.06l-7.25 7.25a.75.75 0 0 1-1.06 0L2.22 9.28a.751.751 0 0 1 .018-1.042.751.751 0 0 1 1.042-.018L6 10.94l6.72-6.72a.75.75 0 0 1 1.06 0Z"></path></svg>',
11
-
12
- // Helper to get an icon with optional classes
13
- get(name, options = {}) {
14
- const svg = this[name];
15
- if (!svg) return `<span class="missing-icon">[${name}]</span>`;
16
-
17
- if (options.class) {
18
- return svg.replace('class="octicon', `class="${options.class} octicon`);
19
- }
20
- return svg;
21
- }
22
- };
23
-
24
- // Language detection utility - pure function, easily testable
25
- function getLanguageFromExtension(filename) {
26
- const ext = filename.split('.').pop().toLowerCase();
27
- const languageMap = {
28
- // JavaScript family
29
- 'js': 'javascript',
30
- 'mjs': 'javascript',
31
- 'jsx': 'javascript',
32
- 'ts': 'typescript',
33
- 'tsx': 'typescript',
34
-
35
- // Web languages
36
- 'html': 'html',
37
- 'htm': 'html',
38
- 'css': 'css',
39
- 'scss': 'scss',
40
- 'sass': 'sass',
41
- 'less': 'less',
42
-
43
- // Data formats
44
- 'json': 'json',
45
- 'xml': 'xml',
46
- 'yaml': 'yaml',
47
- 'yml': 'yaml',
48
-
49
- // Programming languages
50
- 'py': 'python',
51
- 'java': 'java',
52
- 'go': 'go',
53
- 'rs': 'rust',
54
- 'php': 'php',
55
- 'rb': 'ruby',
56
- 'swift': 'swift',
57
- 'kt': 'kotlin',
58
- 'dart': 'dart',
59
-
60
- // Systems languages
61
- 'c': 'c',
62
- 'cpp': 'cpp',
63
- 'cc': 'cpp',
64
- 'cxx': 'cpp',
65
- 'h': 'c',
66
- 'hpp': 'cpp',
67
-
68
- // Shell and scripts
69
- 'sh': 'shell',
70
- 'bash': 'shell',
71
- 'zsh': 'shell',
72
- 'fish': 'shell',
73
- 'ps1': 'powershell',
74
-
75
- // Other languages
76
- 'sql': 'sql',
77
- 'r': 'r',
78
- 'scala': 'scala',
79
- 'clj': 'clojure',
80
- 'lua': 'lua',
81
- 'pl': 'perl',
82
- 'groovy': 'groovy',
83
-
84
- // Config and text
85
- 'md': 'markdown',
86
- 'txt': 'plaintext',
87
- 'log': 'plaintext'
88
- };
89
-
90
- // Special filename handling
91
- const basename = filename.toLowerCase();
92
- if (basename === 'dockerfile' || basename.startsWith('dockerfile.')) return 'dockerfile';
93
- if (basename === 'makefile') return 'makefile';
94
- if (basename.startsWith('.env')) return 'dotenv';
95
- if (basename === 'package.json' || basename === 'composer.json') return 'json';
96
-
97
- return languageMap[ext] || 'plaintext';
98
- }
99
-
100
- // Draft management utilities - pure functions, easily testable
101
- const DraftManager = {
102
- STORAGE_PREFIX: 'gh-here-draft-',
103
-
104
- saveDraft(filePath, content) {
105
- localStorage.setItem(`${this.STORAGE_PREFIX}${filePath}`, content);
106
- },
107
-
108
- loadDraft(filePath) {
109
- return localStorage.getItem(`${this.STORAGE_PREFIX}${filePath}`);
110
- },
111
-
112
- clearDraft(filePath) {
113
- localStorage.removeItem(`${this.STORAGE_PREFIX}${filePath}`);
114
- },
115
-
116
- // Helper to check if draft exists and differs from content
117
- hasDraftChanges(filePath, originalContent) {
118
- const draft = this.loadDraft(filePath);
119
- return draft !== null && draft !== originalContent;
120
- },
121
-
122
- // Get all draft keys for cleanup/debugging
123
- getAllDrafts() {
124
- const drafts = {};
125
- for (let i = 0; i < localStorage.length; i++) {
126
- const key = localStorage.key(i);
127
- if (key.startsWith(this.STORAGE_PREFIX)) {
128
- const filePath = key.replace(this.STORAGE_PREFIX, '');
129
- drafts[filePath] = localStorage.getItem(key);
130
- }
131
- }
132
- return drafts;
133
- }
134
- };
135
-
136
- // Path utility functions - pure functions, easily testable
137
- const PathUtils = {
138
- // Extract current path from URL parameters
139
- getCurrentPath() {
140
- const currentUrl = new URL(window.location.href);
141
- return currentUrl.searchParams.get('path') || '';
142
- },
143
-
144
- // Navigate to parent directory
145
- getParentPath(currentPath) {
146
- if (!currentPath || currentPath === '') {
147
- return null; // Already at root
148
- }
149
-
150
- const pathParts = currentPath.split('/').filter(p => p);
151
- if (pathParts.length === 0) {
152
- return null; // Already at root
153
- }
154
-
155
- pathParts.pop();
156
- return pathParts.join('/');
157
- },
158
-
159
- // Build file path from directory and filename
160
- buildFilePath(currentPath, filename) {
161
- return currentPath ? `${currentPath}/${filename}` : filename;
162
- },
163
-
164
- // Get filename from full path
165
- getFileName(filePath) {
166
- return filePath.split('/').pop() || 'file.txt';
167
- },
168
-
169
- // Build URL with encoded path parameter
170
- buildPathUrl(basePath, targetPath) {
171
- return targetPath ? `${basePath}?path=${encodeURIComponent(targetPath)}` : basePath;
172
- },
173
-
174
- // Extract directory path from file path
175
- getDirectoryPath(filePath) {
176
- const parts = filePath.split('/').filter(p => p);
177
- if (parts.length <= 1) return '';
178
- return parts.slice(0, -1).join('/');
179
- }
180
- };
181
-
182
- // Notification system - global scope for access from commit functions
183
- function showNotification(message, type = 'info') {
184
- // Remove existing notifications
185
- const existingNotifications = document.querySelectorAll('.notification');
186
- existingNotifications.forEach(n => n.remove());
187
-
188
- const notification = document.createElement('div');
189
- notification.className = `notification notification-${type}`;
190
- notification.textContent = message;
191
-
192
- document.body.appendChild(notification);
193
-
194
- // Auto-remove after 4 seconds
195
- setTimeout(() => {
196
- if (notification.parentNode) {
197
- notification.style.opacity = '0';
198
- setTimeout(() => notification.remove(), 300);
199
- }
200
- }, 4000);
201
- }
1
+ /**
2
+ * Main application entry point
3
+ * Coordinates all modules and initializes the application
4
+ */
5
+
6
+ import { ThemeManager } from './js/theme-manager.js';
7
+ import { SearchHandler } from './js/search-handler.js';
8
+ import { KeyboardHandler } from './js/keyboard-handler.js';
9
+ import { FileTreeNavigator } from './js/file-tree.js';
10
+ import { NavigationHandler } from './js/navigation.js';
11
+ import { PathUtils } from './js/utils.js';
12
+ import { showNotification } from './js/notification.js';
13
+ import { copyToClipboard } from './js/clipboard-utils.js';
14
+
15
+ class Application {
16
+ constructor() {
17
+ this.themeManager = null;
18
+ this.searchHandler = null;
19
+ this.keyboardHandler = null;
20
+ this.fileTree = null;
21
+ this.navigationHandler = null;
22
+ }
23
+
24
+ init() {
25
+ document.addEventListener('DOMContentLoaded', () => {
26
+ // Initialize navigation first so listeners are ready
27
+ this.navigation = new NavigationHandler();
28
+ this.initializeComponents();
29
+ });
202
30
 
203
- document.addEventListener('DOMContentLoaded', function() {
204
-
205
- const themeToggle = document.getElementById('theme-toggle');
206
- const html = document.documentElement;
207
- const searchInput = document.getElementById('file-search');
208
- const fileTable = document.getElementById('file-table');
209
-
210
- let currentFocusIndex = -1;
211
- let fileRows = [];
212
-
213
- // Removed loading state utilities - not needed for local operations
214
-
215
- // Monaco Editor integration
216
- let monacoFileEditor = null;
217
- let monacoNewFileEditor = null;
218
-
219
- // Note: getLanguageFromExtension is now a global utility function
220
-
221
- // Initialize Monaco Editor
222
- function initializeMonaco() {
223
- if (typeof require !== 'undefined') {
224
- require.config({ paths: { 'vs': 'https://unpkg.com/monaco-editor@0.45.0/min/vs' }});
225
-
226
- require(['vs/editor/editor.main'], function () {
227
- // Configure Monaco Editor to work without web workers (avoids CORS issues)
228
- self.MonacoEnvironment = {
229
- getWorker: function(workerId, label) {
230
- // Return undefined to disable workers and use main thread processing
231
- // This avoids CORS issues with unpkg CDN while still providing syntax highlighting
232
- return undefined;
233
- }
234
- };
31
+ // Re-initialize components after client-side navigation
32
+ document.addEventListener('content-loaded', () => {
33
+ try {
34
+ // Re-initialize theme manager listeners (button might be re-rendered)
35
+ if (this.themeManager) {
36
+ this.themeManager.setupListeners();
37
+ }
235
38
 
236
- // Set Monaco theme based on current theme
237
- const currentTheme = html.getAttribute('data-theme');
238
- const monacoTheme = currentTheme === 'dark' ? 'vs-dark' : 'vs';
239
- monaco.editor.setTheme(monacoTheme);
39
+ // Re-initialize components that need fresh DOM references
40
+ this.searchHandler = new SearchHandler();
41
+ this.keyboardHandler = new KeyboardHandler(this.searchHandler);
240
42
 
241
- // Initialize new file editor if container exists
242
- const newFileEditorContainer = document.getElementById('new-file-content');
243
- if (newFileEditorContainer) {
244
- monacoNewFileEditor = monaco.editor.create(newFileEditorContainer, {
245
- value: '',
246
- language: 'plaintext',
247
- theme: monacoTheme,
248
- minimap: { enabled: false },
249
- lineNumbers: 'on',
250
- wordWrap: 'off',
251
- scrollBeyondLastLine: false,
252
- fontSize: 12,
253
- lineHeight: 20, // Match view mode breathing
254
- fontFamily: "'SFMono-Regular', Consolas, 'Liberation Mono', Menlo, monospace",
255
- padding: { top: 20, bottom: 20, left: 20, right: 20 },
256
- renderLineHighlight: 'line',
257
- selectOnLineNumbers: true,
258
- automaticLayout: true,
259
- folding: true,
260
- foldingHighlight: true,
261
- foldingStrategy: 'auto',
262
- showFoldingControls: 'mouseover',
263
- bracketPairColorization: { enabled: true },
264
- guides: {
265
- bracketPairs: true,
266
- indentation: true
267
- }
268
- });
43
+ // Re-initialize file tree when sidebar becomes visible
44
+ const sidebar = document.querySelector('.file-tree-sidebar');
45
+ const treeContainer = document.getElementById('file-tree');
46
+
47
+ if (sidebar && treeContainer && !sidebar.classList.contains('hidden')) {
48
+ // Sidebar is visible - initialize or re-initialize file tree
49
+ if (!this.fileTree || !this.fileTree.isInitialized || this.fileTree.treeContainer !== treeContainer) {
50
+ this.fileTree = new FileTreeNavigator();
51
+ }
52
+ } else if (this.fileTree) {
53
+ // Sidebar is hidden - don't initialize but keep reference for when it becomes visible
54
+ this.fileTree = null;
269
55
  }
270
56
 
271
- // Set global flag that Monaco is ready
272
- window.monacoReady = true;
273
- });
274
- }
275
- }
276
-
277
- // Initialize Monaco when DOM is ready
278
- if (document.readyState === 'loading') {
279
- document.addEventListener('DOMContentLoaded', initializeMonaco);
280
- } else {
281
- initializeMonaco();
282
- }
283
-
284
- // Initialize
285
- updateFileRows();
286
-
287
- // Detect system theme preference
288
- const systemPrefersDark = window.matchMedia && window.matchMedia('(prefers-color-scheme: dark)').matches;
289
- const systemTheme = systemPrefersDark ? 'dark' : 'light';
290
-
291
- // Load saved theme or default to system preference
292
- const savedTheme = localStorage.getItem('gh-here-theme') || systemTheme;
293
- html.setAttribute('data-theme', savedTheme);
294
- updateThemeIcon(savedTheme);
295
-
296
- // Listen for system theme changes (only if no manual override is saved)
297
- if (window.matchMedia) {
298
- const mediaQuery = window.matchMedia('(prefers-color-scheme: dark)');
299
- mediaQuery.addListener(function(e) {
300
- // Only auto-update if user hasn't manually set a theme
301
- if (!localStorage.getItem('gh-here-theme')) {
302
- const newTheme = e.matches ? 'dark' : 'light';
303
- html.setAttribute('data-theme', newTheme);
304
- updateThemeIcon(newTheme);
57
+ this.setupGlobalEventListeners();
58
+ this.setupGitignoreToggle();
59
+ this.setupFileOperations();
60
+ } catch (error) {
61
+ console.error('Error re-initializing components:', error);
305
62
  }
306
63
  });
307
64
  }
308
-
309
- // Theme toggle functionality
310
- themeToggle.addEventListener('click', function() {
311
- const currentTheme = html.getAttribute('data-theme');
312
- const newTheme = currentTheme === 'dark' ? 'light' : 'dark';
313
-
314
- html.setAttribute('data-theme', newTheme);
315
- localStorage.setItem('gh-here-theme', newTheme);
316
- updateThemeIcon(newTheme);
317
-
318
- // Update Monaco editor themes
319
- if (typeof monaco !== 'undefined') {
320
- const monacoTheme = newTheme === 'dark' ? 'vs-dark' : 'vs';
321
- monaco.editor.setTheme(monacoTheme);
65
+
66
+ initializeComponents() {
67
+ // Theme manager persists across navigations
68
+ if (!this.themeManager) {
69
+ this.themeManager = new ThemeManager();
322
70
  }
323
- });
324
71
 
325
- // Gitignore toggle functionality
326
- const gitignoreToggle = document.getElementById('gitignore-toggle');
327
- if (gitignoreToggle) {
328
- gitignoreToggle.addEventListener('click', function() {
329
- const currentUrl = new URL(window.location.href);
330
- const currentGitignoreState = currentUrl.searchParams.get('gitignore');
331
- const newGitignoreState = currentGitignoreState === 'false' ? null : 'false';
332
-
333
- if (newGitignoreState) {
334
- currentUrl.searchParams.set('gitignore', newGitignoreState);
335
- } else {
336
- currentUrl.searchParams.delete('gitignore');
337
- }
338
-
339
- // Navigate to the new URL
340
- window.location.href = currentUrl.toString();
341
- });
342
- }
343
-
344
- // Search functionality
345
- if (searchInput) {
346
- searchInput.addEventListener('input', function() {
347
- const query = this.value.toLowerCase().trim();
348
- filterFiles(query);
349
- });
72
+ // Initialize components
73
+ this.searchHandler = new SearchHandler();
74
+ this.keyboardHandler = new KeyboardHandler(this.searchHandler);
350
75
 
351
- // Focus search with Ctrl+K or Cmd+K
352
- document.addEventListener('keydown', function(e) {
353
- if ((e.ctrlKey || e.metaKey) && e.key === 'k') {
354
- e.preventDefault();
355
- searchInput.focus();
356
- searchInput.select();
357
- }
358
- });
359
- }
360
-
361
- // Keyboard shortcuts help overlay
362
- function showKeyboardHelp() {
363
- // Remove existing help if present
364
- const existingHelp = document.getElementById('keyboard-help');
365
- if (existingHelp) {
366
- existingHelp.remove();
367
- return;
76
+ // Initialize file tree if sidebar is visible (not hidden)
77
+ const sidebar = document.querySelector('.file-tree-sidebar');
78
+ const treeContainer = document.getElementById('file-tree');
79
+ if (sidebar && treeContainer && !sidebar.classList.contains('hidden')) {
80
+ this.fileTree = new FileTreeNavigator();
368
81
  }
369
82
 
370
- const helpOverlay = document.createElement('div');
371
- helpOverlay.id = 'keyboard-help';
372
- helpOverlay.className = 'keyboard-help-overlay';
373
-
374
- helpOverlay.innerHTML = `
375
- <div class="keyboard-help-content">
376
- <div class="keyboard-help-header">
377
- <h2>Keyboard shortcuts</h2>
378
- <button class="keyboard-help-close" aria-label="Close help">&times;</button>
379
- </div>
380
- <div class="keyboard-help-body">
381
- <div class="shortcuts-container">
382
- <div class="shortcut-section">
383
- <h3>Repositories</h3>
384
- <div class="shortcut-list">
385
- <div class="shortcut-item">
386
- <span class="shortcut-desc">Go to parent directory</span>
387
- <div class="shortcut-keys"><kbd>H</kbd></div>
388
- </div>
389
- <div class="shortcut-item">
390
- <span class="shortcut-desc">Toggle .gitignore filter</span>
391
- <div class="shortcut-keys"><kbd>I</kbd></div>
392
- </div>
393
- <div class="shortcut-item">
394
- <span class="shortcut-desc">Create new file</span>
395
- <div class="shortcut-keys"><kbd>C</kbd></div>
396
- </div>
397
- <div class="shortcut-item">
398
- <span class="shortcut-desc">Edit focused file</span>
399
- <div class="shortcut-keys"><kbd>E</kbd></div>
400
- </div>
401
- <div class="shortcut-item">
402
- <span class="shortcut-desc">Show diff for focused file</span>
403
- <div class="shortcut-keys"><kbd>D</kbd></div>
404
- </div>
405
- <div class="shortcut-item">
406
- <span class="shortcut-desc">Refresh page</span>
407
- <div class="shortcut-keys"><kbd>R</kbd></div>
408
- </div>
409
- </div>
410
- </div>
411
-
412
- <div class="shortcut-section">
413
- <h3>Site-wide shortcuts</h3>
414
- <div class="shortcut-list">
415
- <div class="shortcut-item">
416
- <span class="shortcut-desc">Focus search</span>
417
- <div class="shortcut-keys"><kbd>S</kbd> or <kbd>/</kbd></div>
418
- </div>
419
- <div class="shortcut-item">
420
- <span class="shortcut-desc">Focus search</span>
421
- <div class="shortcut-keys"><kbd>⌘</kbd> <kbd>K</kbd></div>
422
- </div>
423
- <div class="shortcut-item">
424
- <span class="shortcut-desc">Toggle theme</span>
425
- <div class="shortcut-keys"><kbd>T</kbd></div>
426
- </div>
427
- <div class="shortcut-item">
428
- <span class="shortcut-desc">Bring up this help dialog</span>
429
- <div class="shortcut-keys"><kbd>?</kbd></div>
430
- </div>
431
- <div class="shortcut-item">
432
- <span class="shortcut-desc">Move selection down</span>
433
- <div class="shortcut-keys"><kbd>J</kbd></div>
434
- </div>
435
- <div class="shortcut-item">
436
- <span class="shortcut-desc">Move selection up</span>
437
- <div class="shortcut-keys"><kbd>K</kbd></div>
438
- </div>
439
- <div class="shortcut-item">
440
- <span class="shortcut-desc">Open selection</span>
441
- <div class="shortcut-keys"><kbd>O</kbd> or <kbd>↵</kbd></div>
442
- </div>
443
- <div class="shortcut-item">
444
- <span class="shortcut-desc">Save file (in editor)</span>
445
- <div class="shortcut-keys"><kbd>⌘</kbd> <kbd>S</kbd></div>
446
- </div>
447
- <div class="shortcut-item">
448
- <span class="shortcut-desc">Toggle word wrap (in editor)</span>
449
- <div class="shortcut-keys"><kbd>Alt</kbd> <kbd>Z</kbd></div>
450
- </div>
451
- <div class="shortcut-item">
452
- <span class="shortcut-desc">Close help/cancel</span>
453
- <div class="shortcut-keys"><kbd>Esc</kbd></div>
454
- </div>
455
- </div>
456
- </div>
457
- </div>
458
- </div>
459
- </div>
460
- `;
461
-
462
- document.body.appendChild(helpOverlay);
463
-
464
- // Close on click outside or escape
465
- helpOverlay.addEventListener('click', function(e) {
466
- if (e.target === helpOverlay) {
467
- helpOverlay.remove();
468
- }
469
- });
470
-
471
- helpOverlay.querySelector('.keyboard-help-close').addEventListener('click', function() {
472
- helpOverlay.remove();
473
- });
83
+ this.setupGlobalEventListeners();
84
+ this.setupGitignoreToggle();
85
+ this.setupFileOperations();
474
86
  }
475
87
 
476
- // Keyboard navigation
477
- document.addEventListener('keydown', function(e) {
478
- // Handle help overlay first
479
- if (e.key === '?' && document.activeElement !== searchInput) {
480
- e.preventDefault();
481
- showKeyboardHelp();
482
- return;
88
+ setupGlobalEventListeners() {
89
+ // Use event delegation - single listener for all clicks (no duplicates)
90
+ // This method can be called multiple times safely
91
+ if (!this.globalClickHandler) {
92
+ this.globalClickHandler = this.handleGlobalClick.bind(this);
93
+ document.addEventListener('click', this.globalClickHandler);
483
94
  }
484
-
485
- // Close help with Escape
486
- if (e.key === 'Escape') {
487
- const helpOverlay = document.getElementById('keyboard-help');
488
- if (helpOverlay) {
489
- helpOverlay.remove();
490
- return;
95
+
96
+ // Setup file row clicks (event delegation on table)
97
+ if (!this.fileRowClickHandler) {
98
+ this.fileRowClickHandler = this.handleFileRowClick.bind(this);
99
+ const fileTable = document.getElementById('file-table');
100
+ if (fileTable) {
101
+ fileTable.addEventListener('click', this.fileRowClickHandler);
491
102
  }
492
103
  }
493
-
494
- // Don't handle shortcuts when editor is active
495
- const editorContainer = document.getElementById('editor-container');
496
- if (editorContainer && editorContainer.style.display !== 'none' &&
497
- editorContainer.contains(document.activeElement)) {
498
- return;
499
- }
500
-
501
- if (searchInput && document.activeElement === searchInput) {
502
- handleSearchKeydown(e);
503
- } else {
504
- handleGlobalKeydown(e);
505
- }
506
- });
507
-
508
- // New file and folder functionality
509
- const newFileBtn = document.getElementById('new-file-btn');
510
- const newFolderBtn = document.getElementById('new-folder-btn');
511
-
512
- if (newFileBtn) {
513
- newFileBtn.addEventListener('click', function() {
514
- const currentPath = PathUtils.getCurrentPath();
515
-
516
- // Navigate to new file creation mode
517
- window.location.href = PathUtils.buildPathUrl('/new', currentPath);
518
- });
519
104
  }
520
105
 
521
- if (newFolderBtn) {
522
- newFolderBtn.addEventListener('click', function() {
523
- const foldername = prompt('Enter folder name:');
524
- if (foldername && foldername.trim()) {
525
- const currentUrl = new URL(window.location.href);
526
- const currentPath = currentUrl.searchParams.get('path') || '';
527
-
528
- fetch('/api/create-folder', {
529
- method: 'POST',
530
- headers: {
531
- 'Content-Type': 'application/json',
532
- },
533
- body: JSON.stringify({
534
- path: currentPath,
535
- foldername: foldername.trim()
536
- })
537
- })
538
- .then(response => response.json())
539
- .then(data => {
540
- if (data.success) {
541
- // Refresh the current directory view
542
- window.location.reload();
543
- } else {
544
- alert('Failed to create folder: ' + data.error);
545
- }
546
- })
547
- .catch(error => {
548
- console.error('Error creating folder:', error);
549
- alert('Failed to create folder');
550
- });
551
- }
552
- });
553
- }
554
-
555
- // File row click navigation
556
- fileRows.forEach((row, index) => {
557
- row.addEventListener('click', function(e) {
558
- // Don't navigate if clicking on quick actions
559
- if (e.target.closest('.quick-actions')) {
560
- return;
561
- }
562
-
563
- const link = row.querySelector('a');
564
- if (link) {
565
- link.click();
566
- }
567
- });
568
- });
569
-
570
- // Quick actions functionality
571
- document.addEventListener('click', function(e) {
572
- if (e.target.closest('.copy-path-btn')) {
106
+ handleGlobalClick(e) {
107
+ // Copy path button
108
+ const copyPathBtn = e.target.closest('.copy-path-btn, .file-path-copy-btn');
109
+ if (copyPathBtn) {
573
110
  e.preventDefault();
574
111
  e.stopPropagation();
575
- const button = e.target.closest('.copy-path-btn');
576
- const path = button.dataset.path;
577
- copyToClipboard(path, button);
112
+ copyToClipboard(copyPathBtn.dataset.path, copyPathBtn);
113
+ return;
578
114
  }
579
- });
580
-
581
- // Line selection functionality
582
- let lastClickedLine = null;
583
- document.addEventListener('click', function(e) {
584
- if (e.target.classList.contains('line-number')) {
115
+
116
+ // Copy raw button
117
+ const copyRawBtn = e.target.closest('.copy-raw-btn');
118
+ if (copyRawBtn) {
585
119
  e.preventDefault();
586
- const lineContainer = e.target.closest('.line-container');
587
- const lineNum = parseInt(lineContainer.dataset.line);
588
-
589
- if (e.shiftKey && lastClickedLine !== null) {
590
- // Range selection
591
- selectLineRange(Math.min(lastClickedLine, lineNum), Math.max(lastClickedLine, lineNum));
592
- } else if (e.ctrlKey || e.metaKey) {
593
- // Toggle individual line
594
- toggleLineSelection(lineNum);
595
- } else {
596
- // Single line selection
597
- clearAllSelections();
598
- selectLine(lineNum);
599
- lastClickedLine = lineNum;
600
- }
601
-
602
- updateURL();
603
- }
604
- });
605
-
606
- function updateThemeIcon(theme) {
607
- const iconSvg = themeToggle.querySelector('.theme-icon');
608
- if (iconSvg) {
609
- if (theme === 'dark') {
610
- iconSvg.innerHTML = '<path d="M8 12a4 4 0 1 0 0-8 4 4 0 0 0 0 8zM8 0a.75.75 0 0 1 .75.75v1.5a.75.75 0 0 1-1.5 0V.75A.75.75 0 0 1 8 0zm0 13a.75.75 0 0 1 .75.75v1.5a.75.75 0 0 1-1.5 0v-1.5A.75.75 0 0 1 8 13zM2.343 2.343a.75.75 0 0 1 1.061 0l1.06 1.061a.75.75 0 0 1-1.06 1.06l-1.06-1.06a.75.75 0 0 1 0-1.061zm9.193 9.193a.75.75 0 0 1 1.06 0l1.061 1.06a.75.75 0 0 1-1.06 1.061l-1.061-1.06a.75.75 0 0 1 0-1.061zM16 8a.75.75 0 0 1-.75.75h-1.5a.75.75 0 0 1 0-1.5h1.5A.75.75 0 0 1 16 8zM3 8a.75.75 0 0 1-.75.75H.75a.75.75 0 0 1 0-1.5h1.5A.75.75 0 0 1 3 8zm10.657-5.657a.75.75 0 0 1 0 1.061l-1.061 1.06a.75.75 0 1 1-1.06-1.06l1.06-1.06a.75.75 0 0 1 1.061 0zm-9.193 9.193a.75.75 0 0 1 0 1.06l-1.06 1.061a.75.75 0 1 1-1.061-1.06l1.06-1.061a.75.75 0 0 1 1.061 0z"></path>';
611
- } else {
612
- iconSvg.innerHTML = '<path d="M9.598 1.591a.75.75 0 0 1 .785-.175 7 7 0 1 1-8.967 8.967.75.75 0 0 1 .961-.96 5.5 5.5 0 0 0 7.046-7.046.75.75 0 0 1 .175-.786zm1.616 1.945a7 7 0 0 1-7.678 7.678 5.5 5.5 0 1 0 7.678-7.678z"></path>';
613
- }
614
- }
615
- }
616
-
617
- function updateFileRows() {
618
- if (fileTable) {
619
- fileRows = Array.from(fileTable.querySelectorAll('.file-row'));
620
- }
621
- }
622
-
623
- function filterFiles(query) {
624
- if (!query) {
625
- fileRows.forEach(row => {
626
- row.classList.remove('hidden');
627
- });
120
+ e.stopPropagation();
121
+ this.copyRawContent(copyRawBtn.dataset.path, copyRawBtn);
628
122
  return;
629
123
  }
630
-
631
- fileRows.forEach(row => {
632
- const fileName = row.dataset.name;
633
- const isVisible = fileName.includes(query);
634
- row.classList.toggle('hidden', !isVisible);
635
- });
636
-
637
- // Reset focus when filtering
638
- clearFocus();
639
- currentFocusIndex = -1;
640
- }
641
-
642
- function handleSearchKeydown(e) {
643
- switch(e.key) {
644
- case 'Escape':
645
- searchInput.blur();
646
- searchInput.value = '';
647
- filterFiles('');
648
- break;
649
- case 'ArrowDown':
650
- e.preventDefault();
651
- searchInput.blur();
652
- focusFirstVisibleRow();
653
- break;
654
- case 'Enter':
655
- if (searchInput.value.trim()) {
656
- const firstVisible = getVisibleRows()[0];
657
- if (firstVisible) {
658
- const link = firstVisible.querySelector('a');
659
- if (link) link.click();
660
- }
661
- }
662
- break;
663
- }
664
- }
665
-
666
- function handleGlobalKeydown(e) {
667
- const visibleRows = getVisibleRows();
668
-
669
- switch(e.key) {
670
- case 'ArrowDown':
671
- case 'j':
672
- e.preventDefault();
673
- navigateDown(visibleRows);
674
- break;
675
- case 'ArrowUp':
676
- case 'k':
677
- e.preventDefault();
678
- navigateUp(visibleRows);
679
- break;
680
- case 'Enter':
681
- case 'o':
682
- if (currentFocusIndex >= 0 && visibleRows[currentFocusIndex]) {
683
- const link = visibleRows[currentFocusIndex].querySelector('a');
684
- if (link) link.click();
685
- }
686
- break;
687
- case '/':
688
- case 's':
689
- if (searchInput && !e.ctrlKey && !e.metaKey) {
690
- e.preventDefault();
691
- searchInput.focus();
692
- }
693
- break;
694
- case 'g':
695
- if (e.ctrlKey || e.metaKey) {
696
- e.preventDefault();
697
- if (visibleRows.length > 0) {
698
- currentFocusIndex = 0;
699
- updateFocus(visibleRows);
700
- }
701
- }
702
- break;
703
- case 'G':
704
- if (e.shiftKey) {
705
- e.preventDefault();
706
- if (visibleRows.length > 0) {
707
- currentFocusIndex = visibleRows.length - 1;
708
- updateFocus(visibleRows);
709
- }
710
- }
711
- break;
712
- case 'h':
713
- // Go back/up one directory
714
- e.preventDefault();
715
- goUpDirectory();
716
- break;
717
- case 'r':
718
- // Refresh page
719
- if (!e.ctrlKey && !e.metaKey) {
720
- e.preventDefault();
721
- location.reload();
722
- }
723
- break;
724
- case 't':
725
- // Toggle theme
726
- if (!e.ctrlKey && !e.metaKey) {
727
- e.preventDefault();
728
- themeToggle.click();
729
- }
730
- break;
731
- case '?':
732
- // Show keyboard shortcuts help
733
- e.preventDefault();
734
- showKeyboardHelp();
735
- break;
736
- case 'c':
737
- // Create new file
738
- if (!e.ctrlKey && !e.metaKey) {
739
- e.preventDefault();
740
- const newFileBtn = document.getElementById('new-file-btn');
741
- if (newFileBtn) {
742
- newFileBtn.click();
743
- }
744
- }
745
- break;
746
- case 'e':
747
- // Edit focused file
748
- if (!e.ctrlKey && !e.metaKey && currentFocusIndex >= 0 && visibleRows[currentFocusIndex]) {
749
- e.preventDefault();
750
- const focusedRow = visibleRows[currentFocusIndex];
751
- const rowType = focusedRow.dataset.type;
752
-
753
- // If we're in a directory listing and focused on a file
754
- if (rowType === 'file') {
755
- const filePath = focusedRow.dataset.path;
756
- // Navigate to the file and trigger edit mode
757
- window.location.href = PathUtils.buildPathUrl('/', filePath) + '#edit';
758
- } else {
759
- // If we're on a file page, use the edit button
760
- const editBtn = document.getElementById('edit-btn');
761
- if (editBtn && editBtn.style.display !== 'none') {
762
- editBtn.click();
763
- }
764
- }
765
- }
766
- break;
767
- case 'i':
768
- // Toggle gitignore
769
- if (!e.ctrlKey && !e.metaKey) {
770
- e.preventDefault();
771
- const gitignoreToggle = document.getElementById('gitignore-toggle');
772
- if (gitignoreToggle) {
773
- gitignoreToggle.click();
774
- }
775
- }
776
- break;
777
- case 'd':
778
- // Show diff for focused file (if it has git status)
779
- if (!e.ctrlKey && !e.metaKey && currentFocusIndex >= 0 && visibleRows[currentFocusIndex]) {
780
- e.preventDefault();
781
- const focusedRow = visibleRows[currentFocusIndex];
782
- const rowType = focusedRow.dataset.type;
783
-
784
- if (rowType === 'file') {
785
- const filePath = focusedRow.dataset.path;
786
- const diffBtn = focusedRow.querySelector('.diff-btn');
787
- if (diffBtn) {
788
- showDiffViewer(filePath);
789
- }
790
- }
791
- }
792
- break;
793
- }
794
- }
795
-
796
- function getVisibleRows() {
797
- return fileRows.filter(row => !row.classList.contains('hidden'));
798
- }
799
-
800
- function focusFirstVisibleRow() {
801
- const visibleRows = getVisibleRows();
802
- if (visibleRows.length > 0) {
803
- currentFocusIndex = 0;
804
- updateFocus(visibleRows);
805
- }
806
- }
807
-
808
- function navigateDown(visibleRows) {
809
- if (visibleRows.length === 0) return;
810
-
811
- currentFocusIndex++;
812
- if (currentFocusIndex >= visibleRows.length) {
813
- currentFocusIndex = 0;
814
- }
815
- updateFocus(visibleRows);
816
- }
817
-
818
- function navigateUp(visibleRows) {
819
- if (visibleRows.length === 0) return;
820
-
821
- currentFocusIndex--;
822
- if (currentFocusIndex < 0) {
823
- currentFocusIndex = visibleRows.length - 1;
824
- }
825
- updateFocus(visibleRows);
826
- }
827
-
828
- function updateFocus(visibleRows) {
829
- clearFocus();
830
- if (currentFocusIndex >= 0 && visibleRows[currentFocusIndex]) {
831
- visibleRows[currentFocusIndex].classList.add('focused');
832
- visibleRows[currentFocusIndex].scrollIntoView({
833
- block: 'nearest',
834
- behavior: 'smooth'
835
- });
124
+
125
+ // Diff button
126
+ const diffBtn = e.target.closest('.diff-btn');
127
+ if (diffBtn) {
128
+ e.preventDefault();
129
+ e.stopPropagation();
130
+ this.showDiffViewer(diffBtn.dataset.path);
836
131
  }
837
132
  }
838
-
839
- function clearFocus() {
840
- fileRows.forEach(row => row.classList.remove('focused'));
841
- }
842
-
843
- function goUpDirectory() {
844
- const currentPath = PathUtils.getCurrentPath();
845
- const newPath = PathUtils.getParentPath(currentPath);
846
-
847
- if (newPath === null) {
848
- // Already at root, do nothing
133
+
134
+ handleFileRowClick(e) {
135
+ const fileRow = e.target.closest('.file-row');
136
+ if (!fileRow || e.target.closest('.quick-actions')) {
849
137
  return;
850
138
  }
851
-
852
- window.location.href = PathUtils.buildPathUrl('/', newPath);
853
- }
854
-
855
139
 
856
- function selectLine(lineNum) {
857
- const lineContainer = document.querySelector(`[data-line="${lineNum}"]`);
858
- if (lineContainer) {
859
- lineContainer.classList.add('selected');
860
- }
861
- }
862
-
863
- function toggleLineSelection(lineNum) {
864
- const lineContainer = document.querySelector(`[data-line="${lineNum}"]`);
865
- if (lineContainer) {
866
- lineContainer.classList.toggle('selected');
867
- }
868
- }
869
-
870
- function selectLineRange(startLine, endLine) {
871
- clearAllSelections();
872
- for (let i = startLine; i <= endLine; i++) {
873
- selectLine(i);
874
- }
875
- }
876
-
877
- function clearAllSelections() {
878
- document.querySelectorAll('.line-container.selected').forEach(line => {
879
- line.classList.remove('selected');
880
- });
881
- }
882
-
883
- function updateURL() {
884
- const selectedLines = Array.from(document.querySelectorAll('.line-container.selected'))
885
- .map(line => parseInt(line.dataset.line))
886
- .sort((a, b) => a - b);
887
-
888
- const url = new URL(window.location);
889
-
890
- if (selectedLines.length === 0) {
891
- url.hash = '';
892
- } else if (selectedLines.length === 1) {
893
- url.hash = `#L${selectedLines[0]}`;
894
- } else {
895
- // Find ranges
896
- const ranges = [];
897
- let rangeStart = selectedLines[0];
898
- let rangeEnd = selectedLines[0];
899
-
900
- for (let i = 1; i < selectedLines.length; i++) {
901
- if (selectedLines[i] === rangeEnd + 1) {
902
- rangeEnd = selectedLines[i];
903
- } else {
904
- if (rangeStart === rangeEnd) {
905
- ranges.push(`L${rangeStart}`);
906
- } else {
907
- ranges.push(`L${rangeStart}-L${rangeEnd}`);
908
- }
909
- rangeStart = rangeEnd = selectedLines[i];
910
- }
911
- }
912
-
913
- if (rangeStart === rangeEnd) {
914
- ranges.push(`L${rangeStart}`);
915
- } else {
916
- ranges.push(`L${rangeStart}-L${rangeEnd}`);
917
- }
918
-
919
- url.hash = `#${ranges.join(',')}`;
140
+ // Get the path from the row and navigate directly
141
+ const path = fileRow.dataset.path;
142
+ if (path !== undefined) {
143
+ e.preventDefault();
144
+ e.stopPropagation();
145
+ // Dispatch navigate event which navigation handler will catch
146
+ document.dispatchEvent(new CustomEvent('navigate', {
147
+ detail: { path, isDirectory: fileRow.dataset.type === 'dir' }
148
+ }));
920
149
  }
921
-
922
- window.history.replaceState({}, '', url);
923
- }
924
-
925
- // Initialize line selections from URL on page load
926
- function initLineSelections() {
927
- const hash = window.location.hash.slice(1);
928
- if (!hash) return;
929
-
930
- const parts = hash.split(',');
931
- parts.forEach(part => {
932
- if (part.includes('-')) {
933
- const [start, end] = part.split('-').map(p => parseInt(p.replace('L', '')));
934
- selectLineRange(start, end);
935
- } else {
936
- const lineNum = parseInt(part.replace('L', ''));
937
- selectLine(lineNum);
938
- }
939
- });
940
- }
941
-
942
- // Initialize line selections if we're on a file page
943
- if (document.querySelector('.with-line-numbers')) {
944
- initLineSelections();
945
150
  }
946
151
 
947
- // Editor functionality
948
- const editBtn = document.getElementById('edit-btn');
949
- const editorContainer = document.getElementById('editor-container');
950
- const saveBtn = document.getElementById('save-btn');
951
- const cancelBtn = document.getElementById('cancel-btn');
952
- const wordWrapBtn = document.getElementById('word-wrap-btn');
953
- const fileContent = document.querySelector('.file-content');
954
-
955
-
956
-
957
- // Auto-open editor if hash is #edit
958
- if (window.location.hash === '#edit' && editBtn) {
959
- // Remove hash and trigger edit
960
- window.location.hash = '';
961
- setTimeout(() => editBtn.click(), 100);
962
- }
152
+ setupGitignoreToggle() {
153
+ const toggle = document.getElementById('gitignore-toggle');
154
+ if (!toggle) return;
963
155
 
964
- if (editBtn && editorContainer) {
965
- let originalContent = '';
966
- let wordWrapEnabled = false; // Default to disabled
156
+ // Only setup if not already configured (check for data attribute)
157
+ if (toggle.dataset.configured) return;
158
+ toggle.dataset.configured = 'true';
967
159
 
968
- // Word wrap toggle functionality
969
- function toggleWordWrap() {
970
- if (monacoFileEditor) {
971
- wordWrapEnabled = !wordWrapEnabled;
972
- monacoFileEditor.updateOptions({ wordWrap: wordWrapEnabled ? 'on' : 'off' });
973
-
974
- // Update button appearance
975
- if (wordWrapBtn) {
976
- wordWrapBtn.classList.toggle('btn-primary', wordWrapEnabled);
977
- wordWrapBtn.classList.toggle('btn-secondary', !wordWrapEnabled);
978
- wordWrapBtn.textContent = wordWrapEnabled ? '↩ Wrap ON' : '↩ Wrap OFF';
979
- wordWrapBtn.title = `Toggle word wrap (Alt+Z) - ${wordWrapEnabled ? 'ON' : 'OFF'}`;
980
- }
981
- }
160
+ // Set initial state from localStorage
161
+ const showGitignored = localStorage.getItem('gh-here-show-gitignored') === 'true';
162
+ if (showGitignored) {
163
+ toggle.classList.add('showing-ignored');
164
+ } else {
165
+ toggle.classList.remove('showing-ignored');
982
166
  }
983
167
 
984
- // Word wrap button click
985
- if (wordWrapBtn) {
986
- wordWrapBtn.addEventListener('click', toggleWordWrap);
987
- }
168
+ toggle.addEventListener('click', () => {
169
+ // Toggle state in localStorage
170
+ const current = localStorage.getItem('gh-here-show-gitignored') === 'true';
171
+ const newState = !current;
172
+ localStorage.setItem('gh-here-show-gitignored', newState.toString());
988
173
 
989
- // Editor keyboard shortcuts
990
- document.addEventListener('keydown', function(e) {
991
- if (editorContainer && editorContainer.style.display !== 'none') {
992
- // Cmd/Ctrl+S to save
993
- if ((e.ctrlKey || e.metaKey) && e.key === 's') {
994
- e.preventDefault();
995
- saveBtn.click();
996
- }
997
- // Alt+Z to toggle word wrap
998
- if (e.altKey && e.key === 'z') {
999
- e.preventDefault();
1000
- toggleWordWrap();
1001
- }
1002
- // Escape to cancel
1003
- if (e.key === 'Escape') {
1004
- e.preventDefault();
1005
- cancelBtn.click();
1006
- }
1007
- }
1008
- });
174
+ // Clear file tree cache since gitignore state is changing
175
+ sessionStorage.removeItem('gh-here-file-tree');
1009
176
 
1010
- // Note: Draft management functions are now in global DraftManager object
1011
-
1012
- // Show draft dialog with Load/Discard/Cancel options
1013
- function showDraftDialog(filePath) {
1014
- return new Promise((resolve) => {
1015
- const modal = document.createElement('div');
1016
- modal.className = 'modal-overlay';
1017
- modal.innerHTML = `
1018
- <div class="modal-content draft-modal">
1019
- <h3>Unsaved Changes Found</h3>
1020
- <p>You have unsaved changes for this file. What would you like to do?</p>
1021
- <div class="draft-actions">
1022
- <button class="btn btn-primary" data-action="load">Load Draft</button>
1023
- <button class="btn btn-secondary" data-action="discard">Discard Draft</button>
1024
- <button class="btn btn-secondary" data-action="cancel">Cancel</button>
1025
- </div>
1026
- </div>
1027
- `;
1028
-
1029
- modal.addEventListener('click', (e) => {
1030
- if (e.target.matches('[data-action]') || e.target === modal) {
1031
- const action = e.target.dataset?.action || 'cancel';
1032
- modal.remove();
1033
- resolve(action);
1034
- }
1035
- });
1036
-
1037
- document.body.appendChild(modal);
1038
- });
1039
- }
1040
-
1041
- editBtn.addEventListener('click', async function() {
1042
- // Get current file path
1043
- const currentUrl = new URL(window.location.href);
1044
- const filePath = currentUrl.searchParams.get('path') || '';
1045
-
1046
-
1047
- // Fetch original file content
1048
- try {
1049
- const response = await fetch(`/api/file-content?path=${encodeURIComponent(filePath)}`);
1050
- if (!response.ok) {
1051
- throw new Error(`HTTP ${response.status}: ${response.statusText}`);
1052
- }
1053
- const content = await response.text();
1054
- originalContent = content;
1055
-
1056
- // Check for draft
1057
- const draft = DraftManager.loadDraft(filePath);
1058
- let contentToLoad = content;
1059
-
1060
- if (DraftManager.hasDraftChanges(filePath, content)) {
1061
- // Show custom draft dialog with 3 options
1062
- const draftChoice = await showDraftDialog(filePath);
1063
- if (draftChoice === 'load') {
1064
- contentToLoad = draft;
1065
- } else if (draftChoice === 'discard') {
1066
- DraftManager.clearDraft(filePath);
1067
- contentToLoad = content;
1068
- } else {
1069
- // User cancelled, use original content
1070
- contentToLoad = content;
1071
- }
1072
- }
1073
-
1074
- if (draft && draft === content) {
1075
- DraftManager.clearDraft(filePath);
1076
- }
1077
-
1078
- // Create Monaco editor if it doesn't exist
1079
- if (!monacoFileEditor && window.monacoReady) {
1080
- const fileEditorContainer = document.getElementById('file-editor');
1081
- if (fileEditorContainer) {
1082
- // Get filename reliably from the current URL path parameter
1083
- const currentUrl = new URL(window.location.href);
1084
- const filePath = currentUrl.searchParams.get('path') || '';
1085
- const filename = PathUtils.getFileName(filePath);
1086
-
1087
- const language = getLanguageFromExtension(filename);
1088
-
1089
- // Validate language exists in Monaco
1090
- const availableLanguages = monaco.languages.getLanguages().map(lang => lang.id);
1091
- const validLanguage = availableLanguages.includes(language) ? language : 'plaintext';
1092
-
1093
- const currentTheme = html.getAttribute('data-theme');
1094
- const monacoTheme = currentTheme === 'dark' ? 'vs-dark' : 'vs';
1095
-
1096
- // Ensure Monaco is fully ready before creating editor
1097
- const createEditorWhenReady = () => {
1098
- if (monaco && monaco.editor && monaco.languages) {
1099
- monacoFileEditor = monaco.editor.create(fileEditorContainer, {
1100
- value: '',
1101
- language: validLanguage,
1102
- theme: monacoTheme,
1103
- minimap: { enabled: false },
1104
- lineNumbers: 'on',
1105
- wordWrap: 'off',
1106
- scrollBeyondLastLine: false,
1107
- fontSize: 12,
1108
- lineHeight: 20, // Match view mode (12px * 1.5 = 18, but Monaco uses pixels)
1109
- fontFamily: "'SFMono-Regular', Consolas, 'Liberation Mono', Menlo, monospace",
1110
- padding: { top: 20, bottom: 20, left: 20, right: 20 },
1111
- renderLineHighlight: 'line',
1112
- selectOnLineNumbers: true,
1113
- automaticLayout: true,
1114
- folding: true,
1115
- foldingHighlight: true,
1116
- foldingStrategy: 'auto',
1117
- showFoldingControls: 'mouseover',
1118
- bracketPairColorization: { enabled: true },
1119
- guides: {
1120
- bracketPairs: true,
1121
- indentation: true
1122
- }
1123
- });
1124
- } else {
1125
- // Monaco not fully ready yet, retry
1126
- setTimeout(createEditorWhenReady, 10);
1127
- }
1128
- };
1129
-
1130
- // Start the ready check
1131
- createEditorWhenReady();
1132
- }
1133
- }
1134
-
1135
- // Set content in Monaco editor - wait for Monaco to be ready
1136
- const setContentWhenReady = () => {
1137
- if (monacoFileEditor && window.monacoReady) {
1138
- // Make sure the editor is visible and properly sized
1139
- monacoFileEditor.layout();
1140
- monacoFileEditor.setValue(contentToLoad);
1141
-
1142
- // Force another layout after a brief delay to ensure proper sizing
1143
- setTimeout(() => {
1144
- monacoFileEditor.layout();
1145
- }, 50);
1146
-
1147
- // Language is already set during editor creation, no need to set it again
1148
-
1149
- // Set up auto-save
1150
- monacoFileEditor.onDidChangeModelContent(() => {
1151
- DraftManager.saveDraft(filePath, monacoFileEditor.getValue());
1152
- });
1153
- } else {
1154
- // Monaco not ready yet, wait and try again
1155
- setTimeout(setContentWhenReady, 100);
1156
- }
1157
- };
1158
- setContentWhenReady();
1159
-
1160
- // Show editor UI
1161
- fileContent.style.display = 'none';
1162
- editorContainer.style.display = 'block';
1163
-
1164
- // Focus Monaco editor
1165
- if (monacoFileEditor) {
1166
- monacoFileEditor.focus();
1167
- }
1168
-
1169
- } catch (error) {
1170
- console.error('Error fetching file content:', error);
1171
- let errorMessage = 'Failed to load file content for editing';
1172
- if (error.message.includes('HTTP 403')) {
1173
- errorMessage = 'Access denied: Cannot read this file';
1174
- } else if (error.message.includes('HTTP 404')) {
1175
- errorMessage = 'File not found';
1176
- } else if (error.message.includes('NetworkError') || error.message.includes('Failed to fetch')) {
1177
- errorMessage = 'Network error: Please check your connection';
1178
- }
1179
- showNotification(errorMessage, 'error');
1180
- }
1181
- });
1182
-
1183
- cancelBtn.addEventListener('click', function() {
1184
- editorContainer.style.display = 'none';
1185
- fileContent.style.display = 'block';
1186
- });
1187
-
1188
- saveBtn.addEventListener('click', function() {
1189
- const currentUrl = new URL(window.location.href);
1190
- const filePath = currentUrl.searchParams.get('path') || '';
1191
-
1192
-
1193
- fetch('/api/save-file', {
1194
- method: 'POST',
1195
- headers: {
1196
- 'Content-Type': 'application/json',
1197
- },
1198
- body: JSON.stringify({
1199
- path: filePath,
1200
- content: monacoFileEditor ? monacoFileEditor.getValue() : ''
1201
- })
1202
- })
1203
- .then(response => {
1204
- if (!response.ok) {
1205
- throw new Error(`HTTP ${response.status}: ${response.statusText}`);
1206
- }
1207
- return response.json();
1208
- })
1209
- .then(data => {
1210
- if (data.success) {
1211
- // Clear draft on successful save
1212
- DraftManager.clearDraft(filePath);
1213
- // Refresh the page to show updated content
1214
- window.location.reload();
1215
- } else {
1216
- showNotification('Failed to save file: ' + data.error, 'error');
1217
- }
1218
- })
1219
- .catch(error => {
1220
- console.error('Error saving file:', error);
1221
- let errorMessage = 'Failed to save file';
1222
- if (error.message.includes('HTTP 403')) {
1223
- errorMessage = 'Access denied: Cannot write to this file';
1224
- } else if (error.message.includes('HTTP 413')) {
1225
- errorMessage = 'File too large to save';
1226
- } else if (error.message.includes('NetworkError') || error.message.includes('Failed to fetch')) {
1227
- errorMessage = 'Network error: Please check your connection';
1228
- }
1229
- showNotification(errorMessage, 'error');
1230
- });
177
+ // Reload page to apply new filter
178
+ window.location.reload();
1231
179
  });
1232
180
  }
1233
181
 
1234
- // Quick edit file functionality
1235
- document.addEventListener('click', function(e) {
1236
- if (e.target.closest('.edit-file-btn')) {
1237
- e.preventDefault();
1238
- e.stopPropagation();
1239
- const button = e.target.closest('.edit-file-btn');
1240
- const filePath = button.dataset.path;
1241
- // Navigate to the file and trigger edit mode
1242
- window.location.href = PathUtils.buildPathUrl('/', filePath) + '#edit';
1243
- }
1244
-
1245
- // Git diff viewer functionality
1246
- if (e.target.closest('.diff-btn')) {
1247
- e.preventDefault();
1248
- e.stopPropagation();
1249
- const button = e.target.closest('.diff-btn');
1250
- const filePath = button.dataset.path;
1251
- showDiffViewer(filePath);
1252
- }
1253
- });
1254
182
 
1255
- // Git diff viewer functions
1256
- function showDiffViewer(filePath) {
1257
- // Create diff viewer overlay
1258
- const overlay = document.createElement('div');
1259
- overlay.className = 'diff-viewer-overlay';
1260
- overlay.innerHTML = `
1261
- <div class="diff-viewer-modal">
1262
- <div class="diff-viewer-header">
1263
- <h3 class="diff-viewer-title">
1264
- 📋 Diff: ${filePath}
1265
- </h3>
1266
- <button class="diff-close-btn" aria-label="Close diff viewer">&times;</button>
1267
- </div>
1268
- <div class="diff-viewer-content">
1269
- <div style="padding: 40px; text-align: center;">Fetching diff...</div>
1270
- </div>
1271
- </div>
1272
- `;
1273
-
1274
- document.body.appendChild(overlay);
1275
-
1276
- // Close on overlay click or close button
1277
- overlay.addEventListener('click', function(e) {
1278
- if (e.target === overlay || e.target.classList.contains('diff-close-btn')) {
1279
- document.body.removeChild(overlay);
1280
- }
1281
- });
1282
-
1283
- // Close with Escape key
1284
- const escHandler = function(e) {
1285
- if (e.key === 'Escape') {
1286
- document.body.removeChild(overlay);
1287
- document.removeEventListener('keydown', escHandler);
1288
- }
1289
- };
1290
- document.addEventListener('keydown', escHandler);
1291
-
1292
- // Load diff content
1293
- loadDiffContent(filePath, overlay);
1294
- }
183
+ setupFileOperations() {
184
+ document.addEventListener('click', async e => {
185
+ if (e.target.closest('.delete-btn')) {
186
+ const btn = e.target.closest('.delete-btn');
187
+ const itemPath = btn.dataset.path;
188
+ const itemName = btn.dataset.name;
189
+ const isDirectory = btn.dataset.isDirectory === 'true';
1295
190
 
1296
- function loadDiffContent(filePath, overlay) {
1297
- fetch(`/api/git-diff?path=${encodeURIComponent(filePath)}`)
1298
- .then(response => response.json())
1299
- .then(data => {
1300
- if (data.success) {
1301
- renderDiff(data.diff, data.filePath, overlay);
1302
- } else {
1303
- showDiffError(data.error, overlay);
191
+ const message = `Are you sure you want to delete ${isDirectory ? 'folder' : 'file'} "${itemName}"?`;
192
+ if (!confirm(message)) {
193
+ return;
1304
194
  }
1305
- })
1306
- .catch(error => {
1307
- console.error('Error loading diff:', error);
1308
- showDiffError('Failed to load diff', overlay);
1309
- });
1310
- }
1311
195
 
1312
- function renderDiff(diffText, filePath, overlay) {
1313
- if (!diffText || diffText.trim() === '') {
1314
- const content = overlay.querySelector('.diff-viewer-content');
1315
- content.innerHTML = `
1316
- <div style="padding: 40px; text-align: center; color: var(--text-secondary);">
1317
- No changes detected for this file
1318
- </div>
1319
- `;
1320
- return;
1321
- }
1322
-
1323
- const lines = diffText.split('\n');
1324
- const parsedDiff = parseDiff(lines);
1325
-
1326
- const content = overlay.querySelector('.diff-viewer-content');
1327
- content.innerHTML = `
1328
- <div class="diff-container">
1329
- <div class="diff-side">
1330
- <div class="diff-side-header">Original</div>
1331
- <div class="diff-side-content" id="diff-original"></div>
1332
- </div>
1333
- <div class="diff-side">
1334
- <div class="diff-side-header">Modified</div>
1335
- <div class="diff-side-content" id="diff-modified"></div>
1336
- </div>
1337
- </div>
1338
- `;
196
+ try {
197
+ const response = await fetch('/api/delete', {
198
+ method: 'POST',
199
+ headers: { 'Content-Type': 'application/json' },
200
+ body: JSON.stringify({ path: itemPath })
201
+ });
1339
202
 
1340
- const originalSide = content.querySelector('#diff-original');
1341
- const modifiedSide = content.querySelector('#diff-modified');
1342
-
1343
- renderDiffSide(parsedDiff.original, originalSide, 'original');
1344
- renderDiffSide(parsedDiff.modified, modifiedSide, 'modified');
1345
- }
203
+ if (!response.ok) {
204
+ throw new Error('Delete failed');
205
+ }
1346
206
 
1347
- function parseDiff(lines) {
1348
- const original = [];
1349
- const modified = [];
1350
- let originalLineNum = 1;
1351
- let modifiedLineNum = 1;
1352
-
1353
- for (let i = 0; i < lines.length; i++) {
1354
- const line = lines[i];
1355
-
1356
- if (line.startsWith('@@')) {
1357
- // Parse hunk header
1358
- const match = line.match(/@@ -(\d+)(?:,\d+)? \+(\d+)(?:,\d+)? @@/);
1359
- if (match) {
1360
- originalLineNum = parseInt(match[1]);
1361
- modifiedLineNum = parseInt(match[2]);
207
+ showNotification(`${isDirectory ? 'Folder' : 'File'} deleted successfully`, 'success');
208
+ setTimeout(() => window.location.reload(), 600);
209
+ } catch (error) {
210
+ showNotification('Failed to delete item', 'error');
1362
211
  }
1363
- continue;
1364
212
  }
1365
-
1366
- if (line.startsWith('---') || line.startsWith('+++') || line.startsWith('diff ') || line.startsWith('index ')) {
1367
- continue;
1368
- }
1369
-
1370
- if (line.startsWith('-')) {
1371
- original.push({
1372
- lineNum: originalLineNum++,
1373
- content: line.substring(1),
1374
- type: 'removed'
1375
- });
1376
- } else if (line.startsWith('+')) {
1377
- modified.push({
1378
- lineNum: modifiedLineNum++,
1379
- content: line.substring(1),
1380
- type: 'added'
1381
- });
1382
- } else {
1383
- // Context line
1384
- const content = line.startsWith(' ') ? line.substring(1) : line;
1385
- original.push({
1386
- lineNum: originalLineNum++,
1387
- content: content,
1388
- type: 'context'
1389
- });
1390
- modified.push({
1391
- lineNum: modifiedLineNum++,
1392
- content: content,
1393
- type: 'context'
1394
- });
1395
- }
1396
- }
1397
-
1398
- return { original, modified };
1399
- }
1400
213
 
1401
- function renderDiffSide(lines, container, side) {
1402
- container.innerHTML = lines.map(line => {
1403
- let content = escapeHtml(line.content);
1404
-
1405
- // Apply syntax highlighting if hljs is available
1406
- if (window.hljs && line.content.trim() !== '') {
1407
- try {
1408
- const highlighted = hljs.highlightAuto(line.content);
1409
- content = highlighted.value;
1410
- } catch (e) {
1411
- // Fall back to escaped HTML if highlighting fails
1412
- content = escapeHtml(line.content);
1413
- }
1414
- }
1415
-
1416
- return `
1417
- <div class="diff-line diff-line-${line.type}">
1418
- <div class="diff-line-number">${line.lineNum}</div>
1419
- <div class="diff-line-content">${content}</div>
1420
- </div>
1421
- `;
1422
- }).join('');
1423
- }
214
+ if (e.target.closest('.rename-btn')) {
215
+ const btn = e.target.closest('.rename-btn');
216
+ const itemPath = btn.dataset.path;
217
+ const currentName = btn.dataset.name;
218
+ const isDirectory = btn.dataset.isDirectory === 'true';
1424
219
 
1425
- function escapeHtml(text) {
1426
- const div = document.createElement('div');
1427
- div.textContent = text;
1428
- return div.innerHTML;
1429
- }
220
+ const newName = prompt(`Rename ${isDirectory ? 'folder' : 'file'}:`, currentName);
221
+ if (!newName || newName.trim() === currentName) {
222
+ return;
223
+ }
1430
224
 
1431
- function showDiffError(error, overlay) {
1432
- const content = overlay.querySelector('.diff-viewer-content');
1433
- content.innerHTML = `
1434
- <div style="padding: 40px; text-align: center; color: var(--text-secondary);">
1435
- <p>Error loading diff:</p>
1436
- <p style="color: #dc3545; margin-top: 8px;">${error}</p>
1437
- </div>
1438
- `;
1439
- }
225
+ try {
226
+ const response = await fetch('/api/rename', {
227
+ method: 'POST',
228
+ headers: { 'Content-Type': 'application/json' },
229
+ body: JSON.stringify({ path: itemPath, newName: newName.trim() })
230
+ });
1440
231
 
1441
- // File operations (delete, rename)
1442
- document.addEventListener('click', function(e) {
1443
- if (e.target.closest('.delete-btn')) {
1444
- const btn = e.target.closest('.delete-btn');
1445
- const itemPath = btn.dataset.path;
1446
- const itemName = btn.dataset.name;
1447
- const isDirectory = btn.dataset.isDirectory === 'true';
1448
-
1449
- const confirmMessage = `Are you sure you want to delete ${isDirectory ? 'folder' : 'file'} "${itemName}"?${isDirectory ? ' This will permanently delete the folder and all its contents.' : ''}`;
1450
-
1451
- if (confirm(confirmMessage)) {
1452
- btn.style.opacity = '0.5';
1453
-
1454
- fetch('/api/delete', {
1455
- method: 'POST',
1456
- headers: {
1457
- 'Content-Type': 'application/json',
1458
- },
1459
- body: JSON.stringify({
1460
- path: itemPath
1461
- })
1462
- })
1463
- .then(response => {
1464
- if (!response.ok) {
1465
- throw new Error(`HTTP ${response.status}: ${response.statusText}`);
1466
- }
1467
- return response.json();
1468
- })
1469
- .then(data => {
1470
- if (data.success) {
1471
- showNotification(`${isDirectory ? 'Folder' : 'File'} "${itemName}" deleted successfully`, 'success');
1472
- setTimeout(() => window.location.reload(), 600);
1473
- } else {
1474
- btn.style.opacity = '1';
1475
- showNotification('Failed to delete: ' + data.error, 'error');
1476
- }
1477
- })
1478
- .catch(error => {
1479
- console.error('Error deleting item:', error);
1480
- btn.style.opacity = '1';
1481
- let errorMessage = 'Failed to delete item';
1482
- if (error.message.includes('HTTP 403')) {
1483
- errorMessage = 'Access denied: Cannot delete this item';
1484
- } else if (error.message.includes('HTTP 404')) {
1485
- errorMessage = 'Item not found';
1486
- } else if (error.message.includes('NetworkError') || error.message.includes('Failed to fetch')) {
1487
- errorMessage = 'Network error: Please check your connection';
1488
- }
1489
- showNotification(errorMessage, 'error');
1490
- });
1491
- }
1492
- }
1493
-
1494
- if (e.target.closest('.rename-btn')) {
1495
- const btn = e.target.closest('.rename-btn');
1496
- const itemPath = btn.dataset.path;
1497
- const currentName = btn.dataset.name;
1498
- const isDirectory = btn.dataset.isDirectory === 'true';
1499
-
1500
- const newName = prompt(`Rename ${isDirectory ? 'folder' : 'file'}:`, currentName);
1501
- if (newName && newName.trim() && newName !== currentName) {
1502
- btn.style.opacity = '0.5';
1503
-
1504
- fetch('/api/rename', {
1505
- method: 'POST',
1506
- headers: {
1507
- 'Content-Type': 'application/json',
1508
- },
1509
- body: JSON.stringify({
1510
- path: itemPath,
1511
- newName: newName.trim()
1512
- })
1513
- })
1514
- .then(response => {
1515
232
  if (!response.ok) {
1516
- throw new Error(`HTTP ${response.status}: ${response.statusText}`);
1517
- }
1518
- return response.json();
1519
- })
1520
- .then(data => {
1521
- if (data.success) {
1522
- showNotification(`${isDirectory ? 'Folder' : 'File'} renamed to "${newName.trim()}"`, 'success');
1523
- setTimeout(() => window.location.reload(), 600);
1524
- } else {
1525
- btn.style.opacity = '1';
1526
- showNotification('Failed to rename: ' + data.error, 'error');
233
+ throw new Error('Rename failed');
1527
234
  }
1528
- })
1529
- .catch(error => {
1530
- console.error('Error renaming item:', error);
1531
- btn.style.opacity = '1';
1532
- let errorMessage = 'Failed to rename item';
1533
- if (error.message.includes('HTTP 403')) {
1534
- errorMessage = 'Access denied: Cannot rename this item';
1535
- } else if (error.message.includes('HTTP 404')) {
1536
- errorMessage = 'Item not found';
1537
- } else if (error.message.includes('NetworkError') || error.message.includes('Failed to fetch')) {
1538
- errorMessage = 'Network error: Please check your connection';
1539
- }
1540
- showNotification(errorMessage, 'error');
1541
- });
1542
- }
1543
- }
1544
- });
1545
235
 
1546
- // New file interface functionality
1547
- const newFilenameInput = document.getElementById('new-filename-input');
1548
-
1549
- // Update Monaco language when filename changes
1550
- if (newFilenameInput) {
1551
- newFilenameInput.addEventListener('input', function() {
1552
- const filename = this.value.trim();
1553
- if (monacoNewFileEditor && filename) {
1554
- const language = getLanguageFromExtension(filename);
1555
- const model = monacoNewFileEditor.getModel();
1556
- if (model) {
1557
- monaco.editor.setModelLanguage(model, language);
236
+ showNotification(`Renamed to "${newName.trim()}"`, 'success');
237
+ setTimeout(() => window.location.reload(), 600);
238
+ } catch (error) {
239
+ showNotification('Failed to rename item', 'error');
1558
240
  }
1559
241
  }
1560
242
  });
1561
243
  }
1562
- const createNewFileBtn = document.getElementById('create-new-file');
1563
- const cancelNewFileBtn = document.getElementById('cancel-new-file');
1564
244
 
1565
- if (createNewFileBtn) {
1566
- createNewFileBtn.addEventListener('click', function() {
1567
- const filename = newFilenameInput.value.trim();
1568
- const content = monacoNewFileEditor ? monacoNewFileEditor.getValue() : '';
1569
-
1570
- if (!filename) {
1571
- showNotification('Please enter a filename', 'error');
1572
- newFilenameInput.focus();
1573
- return;
1574
- }
1575
-
1576
-
1577
- const currentPath = PathUtils.getCurrentPath();
1578
-
1579
- fetch('/api/create-file', {
1580
- method: 'POST',
1581
- headers: {
1582
- 'Content-Type': 'application/json',
1583
- },
1584
- body: JSON.stringify({
1585
- path: currentPath,
1586
- filename: filename
1587
- })
1588
- })
1589
- .then(response => {
1590
- if (!response.ok) {
1591
- throw new Error(`HTTP ${response.status}: ${response.statusText}`);
1592
- }
1593
- return response.json();
1594
- })
1595
- .then(data => {
1596
- if (data.success) {
1597
- // If there's content, save it
1598
- if (content.trim()) {
1599
- const filePath = PathUtils.buildFilePath(currentPath, filename);
1600
- return fetch('/api/save-file', {
1601
- method: 'POST',
1602
- headers: {
1603
- 'Content-Type': 'application/json',
1604
- },
1605
- body: JSON.stringify({
1606
- path: filePath,
1607
- content: content
1608
- })
1609
- });
1610
- }
1611
- return { json: () => Promise.resolve({ success: true }) };
1612
- } else {
1613
- throw new Error(data.error);
1614
- }
1615
- })
1616
- .then(response => response.json ? response.json() : response)
1617
- .then(data => {
1618
- if (data.success) {
1619
- showNotification(`File "${filename}" created successfully`, 'success');
1620
- // Navigate back to the directory or to the new file
1621
- const redirectPath = PathUtils.buildPathUrl('/', currentPath);
1622
- setTimeout(() => window.location.href = redirectPath, 800);
1623
- } else {
1624
- throw new Error(data.error);
1625
- }
1626
- })
1627
- .catch(error => {
1628
- console.error('Error creating file:', error);
1629
- let errorMessage = 'Failed to create file: ' + error.message;
1630
- if (error.message.includes('HTTP 403')) {
1631
- errorMessage = 'Access denied: Cannot create files in this directory';
1632
- } else if (error.message.includes('HTTP 409') || error.message.includes('already exists')) {
1633
- errorMessage = 'File already exists';
1634
- } else if (error.message.includes('NetworkError') || error.message.includes('Failed to fetch')) {
1635
- errorMessage = 'Network error: Please check your connection';
1636
- }
1637
- showNotification(errorMessage, 'error');
1638
- });
1639
- });
245
+ showDiffViewer(filePath) {
246
+ // Simplified - redirect to diff view
247
+ const url = new URL(window.location.href);
248
+ url.searchParams.set('path', filePath);
249
+ url.searchParams.set('view', 'diff');
250
+ window.location.href = url.toString();
1640
251
  }
1641
252
 
1642
- if (cancelNewFileBtn) {
1643
- cancelNewFileBtn.addEventListener('click', function() {
1644
- const currentPath = PathUtils.getCurrentPath();
1645
- const redirectPath = PathUtils.buildPathUrl('/', currentPath);
1646
- window.location.href = redirectPath;
1647
- });
1648
- }
1649
-
1650
- async function copyToClipboard(text, button) {
253
+ async copyRawContent(filePath, button) {
1651
254
  try {
1652
- await navigator.clipboard.writeText(text);
1653
-
1654
- // Show success feedback
1655
- const originalIcon = button.innerHTML;
1656
- const checkIcon = '<svg class="quick-icon" viewBox="0 0 16 16" width="16" height="16" fill="currentColor"><path d="M13.78 4.22a.75.75 0 0 1 0 1.06l-7.25 7.25a.75.75 0 0 1-1.06 0L2.22 9.28a.75.75 0 0 1 1.06-1.06L6 10.94l6.72-6.72a.75.75 0 0 1 1.06 0Z"></path></svg>';
1657
-
1658
- button.innerHTML = checkIcon;
1659
- button.style.color = '#28a745';
1660
-
1661
- setTimeout(() => {
1662
- button.innerHTML = originalIcon;
1663
- button.style.color = '';
1664
- }, 1000);
1665
- } catch (err) {
1666
- // Fallback for older browsers
1667
- const textArea = document.createElement('textarea');
1668
- textArea.value = text;
1669
- textArea.style.position = 'fixed';
1670
- textArea.style.left = '-999999px';
1671
- textArea.style.top = '-999999px';
1672
- document.body.appendChild(textArea);
1673
- textArea.focus();
1674
- textArea.select();
1675
-
1676
- try {
1677
- document.execCommand('copy');
1678
- // Show success feedback (same as above)
1679
- const originalIcon = button.innerHTML;
1680
- const checkIcon = '<svg class="quick-icon" viewBox="0 0 16 16" width="16" height="16" fill="currentColor"><path d="M13.78 4.22a.75.75 0 0 1 0 1.06l-7.25 7.25a.75.75 0 0 1-1.06 0L2.22 9.28a.75.75 0 0 1 1.06-1.06L6 10.94l6.72-6.72a.75.75 0 0 1 1.06 0Z"></path></svg>';
1681
-
1682
- button.innerHTML = checkIcon;
1683
- button.style.color = '#28a745';
1684
-
1685
- setTimeout(() => {
1686
- button.innerHTML = originalIcon;
1687
- button.style.color = '';
1688
- }, 1000);
1689
- } catch (fallbackErr) {
1690
- console.error('Could not copy text: ', fallbackErr);
255
+ const response = await fetch(`/api/file-content?path=${encodeURIComponent(filePath)}`);
256
+ if (!response.ok) {
257
+ throw new Error(`HTTP ${response.status}`);
1691
258
  }
1692
-
1693
- document.body.removeChild(textArea);
259
+ const content = await response.text();
260
+ await copyToClipboard(content, button);
261
+ showNotification('Raw content copied to clipboard', 'success');
262
+ } catch (error) {
263
+ console.error('Failed to copy raw content:', error);
264
+ showNotification('Failed to copy raw content', 'error');
1694
265
  }
1695
266
  }
1696
- });
1697
-
1698
- // Simple commit modal functionality
1699
- document.addEventListener('click', (e) => {
1700
- if (e.target.matches('#commit-btn') || e.target.closest('#commit-btn')) {
1701
- e.preventDefault();
1702
- showCommitModal();
1703
- }
1704
- });
1705
-
1706
- async function showCommitModal() {
1707
- try {
1708
- // Get current path from URL
1709
- const urlParams = new URLSearchParams(window.location.search);
1710
- const currentPath = urlParams.get('path') || '';
1711
-
1712
- // Fetch git changes from current directory and subdirectories only
1713
- const response = await fetch(`/api/git-status?currentPath=${encodeURIComponent(currentPath)}`);
1714
- const data = await response.json();
1715
-
1716
- if (!data.success) {
1717
- showNotification('❌ Failed to load git changes', 'error');
1718
- return;
1719
- }
1720
-
1721
- const changedFiles = data.changes;
1722
-
1723
- if (changedFiles.length === 0) {
1724
- showNotification('ℹ️ No changes to commit', 'info');
1725
- return;
1726
- }
1727
-
1728
- showCommitModalWithFiles(changedFiles);
1729
- } catch (error) {
1730
- console.error('Error fetching git status:', error);
1731
- showNotification('❌ Failed to load git changes', 'error');
1732
- }
1733
- }
1734
-
1735
- function groupFilesByDirectory(files) {
1736
- const groups = new Map();
1737
-
1738
- files.forEach(file => {
1739
- const parts = file.name.split('/');
1740
- if (parts.length === 1) {
1741
- // Root level file
1742
- if (!groups.has('')) {
1743
- groups.set('', { directory: null, files: [] });
1744
- }
1745
- groups.get('').files.push(file);
1746
- } else {
1747
- // File in subdirectory
1748
- const directory = PathUtils.getDirectoryPath(file.name);
1749
- if (!groups.has(directory)) {
1750
- groups.set(directory, { directory, files: [] });
1751
- }
1752
- groups.get(directory).files.push(file);
1753
- }
1754
- });
1755
-
1756
- // Convert to array and sort
1757
- const result = Array.from(groups.values());
1758
- result.sort((a, b) => {
1759
- if (!a.directory && b.directory) return -1; // Root files first
1760
- if (a.directory && !b.directory) return 1;
1761
- if (!a.directory && !b.directory) return 0;
1762
- return a.directory.localeCompare(b.directory);
1763
- });
1764
-
1765
- return result;
1766
267
  }
1767
268
 
1768
- async function showCommitModalWithFiles(changedFiles) {
1769
- // Group files by directory for better display
1770
- const groupedFiles = groupFilesByDirectory(changedFiles);
1771
-
1772
- // Get octicons for the modal
1773
- const folderIcon = octicons.get('file-directory', { class: 'folder-icon' });
1774
-
1775
- // Create modal
1776
- const modal = document.createElement('div');
1777
- modal.className = 'commit-modal-overlay';
1778
- modal.innerHTML = `
1779
- <div class="commit-modal">
1780
- <div class="commit-modal-header">
1781
- <h3>Commit Changes</h3>
1782
- <button class="modal-close">&times;</button>
1783
- </div>
1784
- <div class="commit-modal-body">
1785
- <div class="changed-files">
1786
- <h4>Changed Files (${changedFiles.length})</h4>
1787
- <ul class="file-list">
1788
- ${groupedFiles.map(group => `
1789
- ${group.directory ? `<li class="directory-group"><strong>${folderIcon} ${group.directory}/</strong></li>` : ''}
1790
- ${group.files.map(file => `
1791
- <li class="file-item ${group.directory ? 'indented' : ''}">
1792
- <label class="file-checkbox-label">
1793
- <input type="checkbox" class="file-checkbox" data-file="${file.name}" checked>
1794
- <span class="file-status">${file.status}</span>
1795
- <span class="file-name">${group.directory ? file.name.split('/').pop() : file.name}</span>
1796
- </label>
1797
- </li>
1798
- `).join('')}
1799
- `).join('')}
1800
- </ul>
1801
- </div>
1802
- <div class="commit-message-section">
1803
- <textarea id="modal-commit-message" placeholder="Enter commit message..." rows="4"></textarea>
1804
- </div>
1805
- </div>
1806
- <div class="commit-modal-footer">
1807
- <button class="btn-cancel">Cancel</button>
1808
- <button class="btn-commit" disabled>Commit Changes</button>
1809
- </div>
1810
- </div>
1811
- `;
1812
-
1813
- document.body.appendChild(modal);
1814
-
1815
- // Handle modal interactions
1816
- const messageInput = modal.querySelector('#modal-commit-message');
1817
- const commitBtn = modal.querySelector('.btn-commit');
1818
- const cancelBtn = modal.querySelector('.btn-cancel');
1819
- const closeBtn = modal.querySelector('.modal-close');
1820
-
1821
- // Update commit button based on message and selected files
1822
- const updateCommitButton = () => {
1823
- const hasMessage = messageInput.value.trim();
1824
- const selectedFiles = modal.querySelectorAll('.file-checkbox:checked').length;
1825
- commitBtn.disabled = !hasMessage || selectedFiles === 0;
1826
- commitBtn.textContent = selectedFiles > 0
1827
- ? `Commit ${selectedFiles} file${selectedFiles === 1 ? '' : 's'}`
1828
- : 'No files selected';
1829
- };
1830
-
1831
- messageInput.addEventListener('input', updateCommitButton);
1832
-
1833
- // Handle checkbox changes
1834
- modal.querySelectorAll('.file-checkbox').forEach(checkbox => {
1835
- checkbox.addEventListener('change', updateCommitButton);
1836
- });
1837
-
1838
- // Initial button state
1839
- updateCommitButton();
1840
-
1841
- // Close modal handlers
1842
- const closeModal = () => {
1843
- document.body.removeChild(modal);
1844
- };
1845
-
1846
- cancelBtn.addEventListener('click', closeModal);
1847
- closeBtn.addEventListener('click', closeModal);
1848
- modal.addEventListener('click', (e) => {
1849
- if (e.target === modal) closeModal();
1850
- });
1851
-
1852
- // Prevent keyboard shortcuts from interfering with the modal
1853
- modal.addEventListener('keydown', (e) => {
1854
- e.stopPropagation();
1855
- });
1856
-
1857
- // Commit handler
1858
- commitBtn.addEventListener('click', async () => {
1859
- const message = messageInput.value.trim();
1860
- const selectedCheckboxes = modal.querySelectorAll('.file-checkbox:checked');
1861
- const selectedFiles = Array.from(selectedCheckboxes).map(cb => cb.dataset.file);
1862
-
1863
- if (!message || selectedFiles.length === 0) return;
1864
-
1865
- const originalText = commitBtn.textContent;
1866
- commitBtn.textContent = 'Committing...';
1867
- commitBtn.disabled = true;
1868
-
1869
- try {
1870
- // Commit selected files
1871
- const response = await fetch('/api/git-commit-selected', {
1872
- method: 'POST',
1873
- headers: { 'Content-Type': 'application/json' },
1874
- body: JSON.stringify({ message, files: selectedFiles })
1875
- });
1876
-
1877
- if (response.ok) {
1878
- showNotification(`✅ Successfully committed ${selectedFiles.length} file${selectedFiles.length === 1 ? '' : 's'}!`, 'success');
1879
- closeModal();
1880
- setTimeout(() => location.reload(), 1000);
1881
- } else {
1882
- const error = await response.text();
1883
- showNotification(`❌ Commit failed: ${error}`, 'error');
1884
- commitBtn.textContent = originalText;
1885
- commitBtn.disabled = false;
1886
- }
1887
- } catch (err) {
1888
- showNotification('❌ Commit failed', 'error');
1889
- commitBtn.textContent = originalText;
1890
- commitBtn.disabled = false;
1891
- }
1892
- });
1893
-
1894
- // Focus the message input
1895
- messageInput.focus();
1896
- }
269
+ const app = new Application();
270
+ app.init();