gh-here 2.1.0 → 3.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.claude/settings.local.json +20 -11
- package/README.md +30 -101
- package/lib/constants.js +38 -0
- package/lib/error-handler.js +55 -0
- package/lib/file-tree-builder.js +81 -0
- package/lib/file-utils.js +43 -12
- package/lib/renderers.js +423 -194
- package/lib/server.js +120 -32
- package/lib/validation.js +77 -0
- package/package.json +1 -1
- package/public/app.js +199 -1825
- package/public/app.js.backup +1902 -0
- package/public/js/clipboard-utils.js +45 -0
- package/public/js/constants.js +60 -0
- package/public/js/draft-manager.js +36 -0
- package/public/js/editor-manager.js +159 -0
- package/public/js/file-tree.js +321 -0
- package/public/js/keyboard-handler.js +41 -0
- package/public/js/modal-manager.js +70 -0
- package/public/js/navigation.js +254 -0
- package/public/js/notification.js +23 -0
- package/public/js/search-handler.js +238 -0
- package/public/js/theme-manager.js +108 -0
- package/public/js/utils.js +123 -0
- package/public/styles.css +874 -570
- package/.channels_cache_v2.json +0 -10882
- package/.users_cache.json +0 -16187
- package/blog-post.md +0 -100
package/public/app.js
CHANGED
|
@@ -1,1896 +1,270 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
}
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
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
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
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
|
-
//
|
|
237
|
-
|
|
238
|
-
|
|
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
|
-
//
|
|
242
|
-
const
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
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
|
-
|
|
272
|
-
|
|
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
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
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
|
-
|
|
326
|
-
|
|
327
|
-
|
|
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
|
-
//
|
|
352
|
-
document.
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
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
|
-
|
|
371
|
-
|
|
372
|
-
|
|
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">×</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
|
-
|
|
477
|
-
|
|
478
|
-
//
|
|
479
|
-
if (
|
|
480
|
-
|
|
481
|
-
|
|
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
|
-
//
|
|
486
|
-
if (
|
|
487
|
-
|
|
488
|
-
|
|
489
|
-
|
|
490
|
-
|
|
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
|
-
|
|
522
|
-
|
|
523
|
-
|
|
524
|
-
|
|
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
|
-
|
|
576
|
-
|
|
577
|
-
copyToClipboard(path, button);
|
|
112
|
+
copyToClipboard(copyPathBtn.dataset.path, copyPathBtn);
|
|
113
|
+
return;
|
|
578
114
|
}
|
|
579
|
-
|
|
580
|
-
|
|
581
|
-
|
|
582
|
-
|
|
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
|
-
|
|
587
|
-
|
|
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
|
-
|
|
632
|
-
|
|
633
|
-
|
|
634
|
-
|
|
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
|
-
|
|
840
|
-
|
|
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
|
-
|
|
857
|
-
const
|
|
858
|
-
if (
|
|
859
|
-
|
|
860
|
-
|
|
861
|
-
|
|
862
|
-
|
|
863
|
-
|
|
864
|
-
|
|
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
|
-
|
|
948
|
-
|
|
949
|
-
|
|
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
|
-
|
|
965
|
-
|
|
966
|
-
|
|
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
|
-
//
|
|
969
|
-
|
|
970
|
-
|
|
971
|
-
|
|
972
|
-
|
|
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
|
-
|
|
985
|
-
|
|
986
|
-
|
|
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
|
-
|
|
990
|
-
|
|
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
|
-
|
|
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
|
-
|
|
1256
|
-
|
|
1257
|
-
|
|
1258
|
-
|
|
1259
|
-
|
|
1260
|
-
|
|
1261
|
-
|
|
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">×</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
|
-
|
|
1297
|
-
|
|
1298
|
-
|
|
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
|
-
|
|
1313
|
-
|
|
1314
|
-
|
|
1315
|
-
|
|
1316
|
-
|
|
1317
|
-
|
|
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
|
-
|
|
1341
|
-
|
|
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
|
-
|
|
1348
|
-
|
|
1349
|
-
|
|
1350
|
-
|
|
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
|
-
|
|
1402
|
-
|
|
1403
|
-
|
|
1404
|
-
|
|
1405
|
-
|
|
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
|
-
|
|
1426
|
-
|
|
1427
|
-
|
|
1428
|
-
|
|
1429
|
-
}
|
|
220
|
+
const newName = prompt(`Rename ${isDirectory ? 'folder' : 'file'}:`, currentName);
|
|
221
|
+
if (!newName || newName.trim() === currentName) {
|
|
222
|
+
return;
|
|
223
|
+
}
|
|
1430
224
|
|
|
1431
|
-
|
|
1432
|
-
|
|
1433
|
-
|
|
1434
|
-
|
|
1435
|
-
|
|
1436
|
-
|
|
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(
|
|
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
|
-
|
|
1547
|
-
|
|
1548
|
-
|
|
1549
|
-
|
|
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
|
-
|
|
1566
|
-
|
|
1567
|
-
|
|
1568
|
-
|
|
1569
|
-
|
|
1570
|
-
|
|
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
|
-
|
|
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
|
|
1653
|
-
|
|
1654
|
-
|
|
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
|
-
|
|
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
|
-
|
|
1769
|
-
|
|
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">×</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();
|