gh-here 1.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/README.md +156 -0
- package/bin/gh-here.js +1403 -0
- package/package.json +21 -0
- package/public/app.js +1316 -0
- package/public/highlight.css +121 -0
- package/public/styles.css +1757 -0
package/public/app.js
ADDED
|
@@ -0,0 +1,1316 @@
|
|
|
1
|
+
document.addEventListener('DOMContentLoaded', function() {
|
|
2
|
+
|
|
3
|
+
const themeToggle = document.getElementById('theme-toggle');
|
|
4
|
+
const html = document.documentElement;
|
|
5
|
+
const searchInput = document.getElementById('file-search');
|
|
6
|
+
const fileTable = document.getElementById('file-table');
|
|
7
|
+
const fileEditor = document.getElementById('file-editor');
|
|
8
|
+
|
|
9
|
+
let currentFocusIndex = -1;
|
|
10
|
+
let fileRows = [];
|
|
11
|
+
|
|
12
|
+
// Notification system
|
|
13
|
+
function showNotification(message, type = 'info') {
|
|
14
|
+
// Remove existing notifications
|
|
15
|
+
const existingNotifications = document.querySelectorAll('.notification');
|
|
16
|
+
existingNotifications.forEach(n => n.remove());
|
|
17
|
+
|
|
18
|
+
const notification = document.createElement('div');
|
|
19
|
+
notification.className = `notification notification-${type}`;
|
|
20
|
+
notification.textContent = message;
|
|
21
|
+
|
|
22
|
+
document.body.appendChild(notification);
|
|
23
|
+
|
|
24
|
+
// Auto-remove after 4 seconds
|
|
25
|
+
setTimeout(() => {
|
|
26
|
+
if (notification.parentNode) {
|
|
27
|
+
notification.style.opacity = '0';
|
|
28
|
+
setTimeout(() => notification.remove(), 300);
|
|
29
|
+
}
|
|
30
|
+
}, 4000);
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
// Removed loading state utilities - not needed for local operations
|
|
34
|
+
|
|
35
|
+
// Initialize
|
|
36
|
+
updateFileRows();
|
|
37
|
+
|
|
38
|
+
// Load saved theme or default to dark
|
|
39
|
+
const savedTheme = localStorage.getItem('gh-here-theme') || 'dark';
|
|
40
|
+
html.setAttribute('data-theme', savedTheme);
|
|
41
|
+
updateThemeIcon(savedTheme);
|
|
42
|
+
|
|
43
|
+
// Theme toggle functionality
|
|
44
|
+
themeToggle.addEventListener('click', function() {
|
|
45
|
+
const currentTheme = html.getAttribute('data-theme');
|
|
46
|
+
const newTheme = currentTheme === 'dark' ? 'light' : 'dark';
|
|
47
|
+
|
|
48
|
+
html.setAttribute('data-theme', newTheme);
|
|
49
|
+
localStorage.setItem('gh-here-theme', newTheme);
|
|
50
|
+
updateThemeIcon(newTheme);
|
|
51
|
+
});
|
|
52
|
+
|
|
53
|
+
// Gitignore toggle functionality
|
|
54
|
+
const gitignoreToggle = document.getElementById('gitignore-toggle');
|
|
55
|
+
if (gitignoreToggle) {
|
|
56
|
+
gitignoreToggle.addEventListener('click', function() {
|
|
57
|
+
const currentUrl = new URL(window.location.href);
|
|
58
|
+
const currentGitignoreState = currentUrl.searchParams.get('gitignore');
|
|
59
|
+
const newGitignoreState = currentGitignoreState === 'false' ? null : 'false';
|
|
60
|
+
|
|
61
|
+
if (newGitignoreState) {
|
|
62
|
+
currentUrl.searchParams.set('gitignore', newGitignoreState);
|
|
63
|
+
} else {
|
|
64
|
+
currentUrl.searchParams.delete('gitignore');
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
// Navigate to the new URL
|
|
68
|
+
window.location.href = currentUrl.toString();
|
|
69
|
+
});
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
// Search functionality
|
|
73
|
+
if (searchInput) {
|
|
74
|
+
searchInput.addEventListener('input', function() {
|
|
75
|
+
const query = this.value.toLowerCase().trim();
|
|
76
|
+
filterFiles(query);
|
|
77
|
+
});
|
|
78
|
+
|
|
79
|
+
// Focus search with Ctrl+K or Cmd+K
|
|
80
|
+
document.addEventListener('keydown', function(e) {
|
|
81
|
+
if ((e.ctrlKey || e.metaKey) && e.key === 'k') {
|
|
82
|
+
e.preventDefault();
|
|
83
|
+
searchInput.focus();
|
|
84
|
+
searchInput.select();
|
|
85
|
+
}
|
|
86
|
+
});
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
// Keyboard shortcuts help overlay
|
|
90
|
+
function showKeyboardHelp() {
|
|
91
|
+
// Remove existing help if present
|
|
92
|
+
const existingHelp = document.getElementById('keyboard-help');
|
|
93
|
+
if (existingHelp) {
|
|
94
|
+
existingHelp.remove();
|
|
95
|
+
return;
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
const helpOverlay = document.createElement('div');
|
|
99
|
+
helpOverlay.id = 'keyboard-help';
|
|
100
|
+
helpOverlay.className = 'keyboard-help-overlay';
|
|
101
|
+
|
|
102
|
+
helpOverlay.innerHTML = `
|
|
103
|
+
<div class="keyboard-help-content">
|
|
104
|
+
<div class="keyboard-help-header">
|
|
105
|
+
<h2>Keyboard shortcuts</h2>
|
|
106
|
+
<button class="keyboard-help-close" aria-label="Close help">×</button>
|
|
107
|
+
</div>
|
|
108
|
+
<div class="keyboard-help-body">
|
|
109
|
+
<div class="shortcuts-container">
|
|
110
|
+
<div class="shortcut-section">
|
|
111
|
+
<h3>Repositories</h3>
|
|
112
|
+
<div class="shortcut-list">
|
|
113
|
+
<div class="shortcut-item">
|
|
114
|
+
<span class="shortcut-desc">Go to parent directory</span>
|
|
115
|
+
<div class="shortcut-keys"><kbd>H</kbd></div>
|
|
116
|
+
</div>
|
|
117
|
+
<div class="shortcut-item">
|
|
118
|
+
<span class="shortcut-desc">Toggle .gitignore filter</span>
|
|
119
|
+
<div class="shortcut-keys"><kbd>I</kbd></div>
|
|
120
|
+
</div>
|
|
121
|
+
<div class="shortcut-item">
|
|
122
|
+
<span class="shortcut-desc">Create new file</span>
|
|
123
|
+
<div class="shortcut-keys"><kbd>C</kbd></div>
|
|
124
|
+
</div>
|
|
125
|
+
<div class="shortcut-item">
|
|
126
|
+
<span class="shortcut-desc">Edit focused file</span>
|
|
127
|
+
<div class="shortcut-keys"><kbd>E</kbd></div>
|
|
128
|
+
</div>
|
|
129
|
+
<div class="shortcut-item">
|
|
130
|
+
<span class="shortcut-desc">Show diff for focused file</span>
|
|
131
|
+
<div class="shortcut-keys"><kbd>D</kbd></div>
|
|
132
|
+
</div>
|
|
133
|
+
<div class="shortcut-item">
|
|
134
|
+
<span class="shortcut-desc">Refresh page</span>
|
|
135
|
+
<div class="shortcut-keys"><kbd>R</kbd></div>
|
|
136
|
+
</div>
|
|
137
|
+
</div>
|
|
138
|
+
</div>
|
|
139
|
+
|
|
140
|
+
<div class="shortcut-section">
|
|
141
|
+
<h3>Site-wide shortcuts</h3>
|
|
142
|
+
<div class="shortcut-list">
|
|
143
|
+
<div class="shortcut-item">
|
|
144
|
+
<span class="shortcut-desc">Focus search</span>
|
|
145
|
+
<div class="shortcut-keys"><kbd>S</kbd> or <kbd>/</kbd></div>
|
|
146
|
+
</div>
|
|
147
|
+
<div class="shortcut-item">
|
|
148
|
+
<span class="shortcut-desc">Focus search</span>
|
|
149
|
+
<div class="shortcut-keys"><kbd>⌘</kbd> <kbd>K</kbd></div>
|
|
150
|
+
</div>
|
|
151
|
+
<div class="shortcut-item">
|
|
152
|
+
<span class="shortcut-desc">Toggle theme</span>
|
|
153
|
+
<div class="shortcut-keys"><kbd>T</kbd></div>
|
|
154
|
+
</div>
|
|
155
|
+
<div class="shortcut-item">
|
|
156
|
+
<span class="shortcut-desc">Bring up this help dialog</span>
|
|
157
|
+
<div class="shortcut-keys"><kbd>?</kbd></div>
|
|
158
|
+
</div>
|
|
159
|
+
<div class="shortcut-item">
|
|
160
|
+
<span class="shortcut-desc">Move selection down</span>
|
|
161
|
+
<div class="shortcut-keys"><kbd>J</kbd></div>
|
|
162
|
+
</div>
|
|
163
|
+
<div class="shortcut-item">
|
|
164
|
+
<span class="shortcut-desc">Move selection up</span>
|
|
165
|
+
<div class="shortcut-keys"><kbd>K</kbd></div>
|
|
166
|
+
</div>
|
|
167
|
+
<div class="shortcut-item">
|
|
168
|
+
<span class="shortcut-desc">Open selection</span>
|
|
169
|
+
<div class="shortcut-keys"><kbd>O</kbd> or <kbd>↵</kbd></div>
|
|
170
|
+
</div>
|
|
171
|
+
<div class="shortcut-item">
|
|
172
|
+
<span class="shortcut-desc">Save file (in editor)</span>
|
|
173
|
+
<div class="shortcut-keys"><kbd>⌘</kbd> <kbd>S</kbd></div>
|
|
174
|
+
</div>
|
|
175
|
+
<div class="shortcut-item">
|
|
176
|
+
<span class="shortcut-desc">Close help/cancel</span>
|
|
177
|
+
<div class="shortcut-keys"><kbd>Esc</kbd></div>
|
|
178
|
+
</div>
|
|
179
|
+
</div>
|
|
180
|
+
</div>
|
|
181
|
+
</div>
|
|
182
|
+
</div>
|
|
183
|
+
</div>
|
|
184
|
+
`;
|
|
185
|
+
|
|
186
|
+
document.body.appendChild(helpOverlay);
|
|
187
|
+
|
|
188
|
+
// Close on click outside or escape
|
|
189
|
+
helpOverlay.addEventListener('click', function(e) {
|
|
190
|
+
if (e.target === helpOverlay) {
|
|
191
|
+
helpOverlay.remove();
|
|
192
|
+
}
|
|
193
|
+
});
|
|
194
|
+
|
|
195
|
+
helpOverlay.querySelector('.keyboard-help-close').addEventListener('click', function() {
|
|
196
|
+
helpOverlay.remove();
|
|
197
|
+
});
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
// Keyboard navigation
|
|
201
|
+
document.addEventListener('keydown', function(e) {
|
|
202
|
+
// Handle help overlay first
|
|
203
|
+
if (e.key === '?' && document.activeElement !== searchInput && document.activeElement !== fileEditor) {
|
|
204
|
+
e.preventDefault();
|
|
205
|
+
showKeyboardHelp();
|
|
206
|
+
return;
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
// Close help with Escape
|
|
210
|
+
if (e.key === 'Escape') {
|
|
211
|
+
const helpOverlay = document.getElementById('keyboard-help');
|
|
212
|
+
if (helpOverlay) {
|
|
213
|
+
helpOverlay.remove();
|
|
214
|
+
return;
|
|
215
|
+
}
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
// Don't handle shortcuts when editor is active
|
|
219
|
+
const editorContainer = document.getElementById('editor-container');
|
|
220
|
+
if (editorContainer && editorContainer.style.display !== 'none' &&
|
|
221
|
+
(document.activeElement === fileEditor || editorContainer.contains(document.activeElement))) {
|
|
222
|
+
return;
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
if (searchInput && document.activeElement === searchInput) {
|
|
226
|
+
handleSearchKeydown(e);
|
|
227
|
+
} else {
|
|
228
|
+
handleGlobalKeydown(e);
|
|
229
|
+
}
|
|
230
|
+
});
|
|
231
|
+
|
|
232
|
+
// New file and folder functionality
|
|
233
|
+
const newFileBtn = document.getElementById('new-file-btn');
|
|
234
|
+
const newFolderBtn = document.getElementById('new-folder-btn');
|
|
235
|
+
|
|
236
|
+
if (newFileBtn) {
|
|
237
|
+
newFileBtn.addEventListener('click', function() {
|
|
238
|
+
const currentUrl = new URL(window.location.href);
|
|
239
|
+
const currentPath = currentUrl.searchParams.get('path') || '';
|
|
240
|
+
|
|
241
|
+
// Navigate to new file creation mode
|
|
242
|
+
const newFileUrl = `/new?path=${encodeURIComponent(currentPath)}`;
|
|
243
|
+
window.location.href = newFileUrl;
|
|
244
|
+
});
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
if (newFolderBtn) {
|
|
248
|
+
newFolderBtn.addEventListener('click', function() {
|
|
249
|
+
const foldername = prompt('Enter folder name:');
|
|
250
|
+
if (foldername && foldername.trim()) {
|
|
251
|
+
const currentUrl = new URL(window.location.href);
|
|
252
|
+
const currentPath = currentUrl.searchParams.get('path') || '';
|
|
253
|
+
|
|
254
|
+
fetch('/api/create-folder', {
|
|
255
|
+
method: 'POST',
|
|
256
|
+
headers: {
|
|
257
|
+
'Content-Type': 'application/json',
|
|
258
|
+
},
|
|
259
|
+
body: JSON.stringify({
|
|
260
|
+
path: currentPath,
|
|
261
|
+
foldername: foldername.trim()
|
|
262
|
+
})
|
|
263
|
+
})
|
|
264
|
+
.then(response => response.json())
|
|
265
|
+
.then(data => {
|
|
266
|
+
if (data.success) {
|
|
267
|
+
// Refresh the current directory view
|
|
268
|
+
window.location.reload();
|
|
269
|
+
} else {
|
|
270
|
+
alert('Failed to create folder: ' + data.error);
|
|
271
|
+
}
|
|
272
|
+
})
|
|
273
|
+
.catch(error => {
|
|
274
|
+
console.error('Error creating folder:', error);
|
|
275
|
+
alert('Failed to create folder');
|
|
276
|
+
});
|
|
277
|
+
}
|
|
278
|
+
});
|
|
279
|
+
}
|
|
280
|
+
|
|
281
|
+
// File row click navigation
|
|
282
|
+
fileRows.forEach((row, index) => {
|
|
283
|
+
row.addEventListener('click', function(e) {
|
|
284
|
+
// Don't navigate if clicking on quick actions
|
|
285
|
+
if (e.target.closest('.quick-actions')) {
|
|
286
|
+
return;
|
|
287
|
+
}
|
|
288
|
+
|
|
289
|
+
const link = row.querySelector('a');
|
|
290
|
+
if (link) {
|
|
291
|
+
link.click();
|
|
292
|
+
}
|
|
293
|
+
});
|
|
294
|
+
});
|
|
295
|
+
|
|
296
|
+
// Quick actions functionality
|
|
297
|
+
document.addEventListener('click', function(e) {
|
|
298
|
+
if (e.target.closest('.copy-path-btn')) {
|
|
299
|
+
e.preventDefault();
|
|
300
|
+
e.stopPropagation();
|
|
301
|
+
const button = e.target.closest('.copy-path-btn');
|
|
302
|
+
const path = button.dataset.path;
|
|
303
|
+
copyToClipboard(path, button);
|
|
304
|
+
}
|
|
305
|
+
});
|
|
306
|
+
|
|
307
|
+
// Line selection functionality
|
|
308
|
+
let lastClickedLine = null;
|
|
309
|
+
document.addEventListener('click', function(e) {
|
|
310
|
+
if (e.target.classList.contains('line-number')) {
|
|
311
|
+
e.preventDefault();
|
|
312
|
+
const lineContainer = e.target.closest('.line-container');
|
|
313
|
+
const lineNum = parseInt(lineContainer.dataset.line);
|
|
314
|
+
|
|
315
|
+
if (e.shiftKey && lastClickedLine !== null) {
|
|
316
|
+
// Range selection
|
|
317
|
+
selectLineRange(Math.min(lastClickedLine, lineNum), Math.max(lastClickedLine, lineNum));
|
|
318
|
+
} else if (e.ctrlKey || e.metaKey) {
|
|
319
|
+
// Toggle individual line
|
|
320
|
+
toggleLineSelection(lineNum);
|
|
321
|
+
} else {
|
|
322
|
+
// Single line selection
|
|
323
|
+
clearAllSelections();
|
|
324
|
+
selectLine(lineNum);
|
|
325
|
+
lastClickedLine = lineNum;
|
|
326
|
+
}
|
|
327
|
+
|
|
328
|
+
updateURL();
|
|
329
|
+
}
|
|
330
|
+
});
|
|
331
|
+
|
|
332
|
+
function updateThemeIcon(theme) {
|
|
333
|
+
const iconSvg = themeToggle.querySelector('.theme-icon');
|
|
334
|
+
if (iconSvg) {
|
|
335
|
+
if (theme === 'dark') {
|
|
336
|
+
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>';
|
|
337
|
+
} else {
|
|
338
|
+
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>';
|
|
339
|
+
}
|
|
340
|
+
}
|
|
341
|
+
}
|
|
342
|
+
|
|
343
|
+
function updateFileRows() {
|
|
344
|
+
if (fileTable) {
|
|
345
|
+
fileRows = Array.from(fileTable.querySelectorAll('.file-row'));
|
|
346
|
+
}
|
|
347
|
+
}
|
|
348
|
+
|
|
349
|
+
function filterFiles(query) {
|
|
350
|
+
if (!query) {
|
|
351
|
+
fileRows.forEach(row => {
|
|
352
|
+
row.classList.remove('hidden');
|
|
353
|
+
});
|
|
354
|
+
return;
|
|
355
|
+
}
|
|
356
|
+
|
|
357
|
+
fileRows.forEach(row => {
|
|
358
|
+
const fileName = row.dataset.name;
|
|
359
|
+
const isVisible = fileName.includes(query);
|
|
360
|
+
row.classList.toggle('hidden', !isVisible);
|
|
361
|
+
});
|
|
362
|
+
|
|
363
|
+
// Reset focus when filtering
|
|
364
|
+
clearFocus();
|
|
365
|
+
currentFocusIndex = -1;
|
|
366
|
+
}
|
|
367
|
+
|
|
368
|
+
function handleSearchKeydown(e) {
|
|
369
|
+
switch(e.key) {
|
|
370
|
+
case 'Escape':
|
|
371
|
+
searchInput.blur();
|
|
372
|
+
searchInput.value = '';
|
|
373
|
+
filterFiles('');
|
|
374
|
+
break;
|
|
375
|
+
case 'ArrowDown':
|
|
376
|
+
e.preventDefault();
|
|
377
|
+
searchInput.blur();
|
|
378
|
+
focusFirstVisibleRow();
|
|
379
|
+
break;
|
|
380
|
+
case 'Enter':
|
|
381
|
+
if (searchInput.value.trim()) {
|
|
382
|
+
const firstVisible = getVisibleRows()[0];
|
|
383
|
+
if (firstVisible) {
|
|
384
|
+
const link = firstVisible.querySelector('a');
|
|
385
|
+
if (link) link.click();
|
|
386
|
+
}
|
|
387
|
+
}
|
|
388
|
+
break;
|
|
389
|
+
}
|
|
390
|
+
}
|
|
391
|
+
|
|
392
|
+
function handleGlobalKeydown(e) {
|
|
393
|
+
const visibleRows = getVisibleRows();
|
|
394
|
+
|
|
395
|
+
switch(e.key) {
|
|
396
|
+
case 'ArrowDown':
|
|
397
|
+
case 'j':
|
|
398
|
+
e.preventDefault();
|
|
399
|
+
navigateDown(visibleRows);
|
|
400
|
+
break;
|
|
401
|
+
case 'ArrowUp':
|
|
402
|
+
case 'k':
|
|
403
|
+
e.preventDefault();
|
|
404
|
+
navigateUp(visibleRows);
|
|
405
|
+
break;
|
|
406
|
+
case 'Enter':
|
|
407
|
+
case 'o':
|
|
408
|
+
if (currentFocusIndex >= 0 && visibleRows[currentFocusIndex]) {
|
|
409
|
+
const link = visibleRows[currentFocusIndex].querySelector('a');
|
|
410
|
+
if (link) link.click();
|
|
411
|
+
}
|
|
412
|
+
break;
|
|
413
|
+
case '/':
|
|
414
|
+
case 's':
|
|
415
|
+
if (searchInput && !e.ctrlKey && !e.metaKey) {
|
|
416
|
+
e.preventDefault();
|
|
417
|
+
searchInput.focus();
|
|
418
|
+
}
|
|
419
|
+
break;
|
|
420
|
+
case 'g':
|
|
421
|
+
if (e.ctrlKey || e.metaKey) {
|
|
422
|
+
e.preventDefault();
|
|
423
|
+
if (visibleRows.length > 0) {
|
|
424
|
+
currentFocusIndex = 0;
|
|
425
|
+
updateFocus(visibleRows);
|
|
426
|
+
}
|
|
427
|
+
}
|
|
428
|
+
break;
|
|
429
|
+
case 'G':
|
|
430
|
+
if (e.shiftKey) {
|
|
431
|
+
e.preventDefault();
|
|
432
|
+
if (visibleRows.length > 0) {
|
|
433
|
+
currentFocusIndex = visibleRows.length - 1;
|
|
434
|
+
updateFocus(visibleRows);
|
|
435
|
+
}
|
|
436
|
+
}
|
|
437
|
+
break;
|
|
438
|
+
case 'h':
|
|
439
|
+
// Go back/up one directory
|
|
440
|
+
e.preventDefault();
|
|
441
|
+
goUpDirectory();
|
|
442
|
+
break;
|
|
443
|
+
case 'r':
|
|
444
|
+
// Refresh page
|
|
445
|
+
if (!e.ctrlKey && !e.metaKey) {
|
|
446
|
+
e.preventDefault();
|
|
447
|
+
location.reload();
|
|
448
|
+
}
|
|
449
|
+
break;
|
|
450
|
+
case 't':
|
|
451
|
+
// Toggle theme
|
|
452
|
+
if (!e.ctrlKey && !e.metaKey) {
|
|
453
|
+
e.preventDefault();
|
|
454
|
+
themeToggle.click();
|
|
455
|
+
}
|
|
456
|
+
break;
|
|
457
|
+
case '?':
|
|
458
|
+
// Show keyboard shortcuts help
|
|
459
|
+
e.preventDefault();
|
|
460
|
+
showKeyboardHelp();
|
|
461
|
+
break;
|
|
462
|
+
case 'c':
|
|
463
|
+
// Create new file
|
|
464
|
+
if (!e.ctrlKey && !e.metaKey) {
|
|
465
|
+
e.preventDefault();
|
|
466
|
+
const newFileBtn = document.getElementById('new-file-btn');
|
|
467
|
+
if (newFileBtn) {
|
|
468
|
+
newFileBtn.click();
|
|
469
|
+
}
|
|
470
|
+
}
|
|
471
|
+
break;
|
|
472
|
+
case 'e':
|
|
473
|
+
// Edit focused file
|
|
474
|
+
if (!e.ctrlKey && !e.metaKey && currentFocusIndex >= 0 && visibleRows[currentFocusIndex]) {
|
|
475
|
+
e.preventDefault();
|
|
476
|
+
const focusedRow = visibleRows[currentFocusIndex];
|
|
477
|
+
const rowType = focusedRow.dataset.type;
|
|
478
|
+
|
|
479
|
+
// If we're in a directory listing and focused on a file
|
|
480
|
+
if (rowType === 'file') {
|
|
481
|
+
const filePath = focusedRow.dataset.path;
|
|
482
|
+
// Navigate to the file and trigger edit mode
|
|
483
|
+
window.location.href = `/?path=${encodeURIComponent(filePath)}#edit`;
|
|
484
|
+
} else {
|
|
485
|
+
// If we're on a file page, use the edit button
|
|
486
|
+
const editBtn = document.getElementById('edit-btn');
|
|
487
|
+
if (editBtn && editBtn.style.display !== 'none') {
|
|
488
|
+
editBtn.click();
|
|
489
|
+
}
|
|
490
|
+
}
|
|
491
|
+
}
|
|
492
|
+
break;
|
|
493
|
+
case 'i':
|
|
494
|
+
// Toggle gitignore
|
|
495
|
+
if (!e.ctrlKey && !e.metaKey) {
|
|
496
|
+
e.preventDefault();
|
|
497
|
+
const gitignoreToggle = document.getElementById('gitignore-toggle');
|
|
498
|
+
if (gitignoreToggle) {
|
|
499
|
+
gitignoreToggle.click();
|
|
500
|
+
}
|
|
501
|
+
}
|
|
502
|
+
break;
|
|
503
|
+
case 'd':
|
|
504
|
+
// Show diff for focused file (if it has git status)
|
|
505
|
+
if (!e.ctrlKey && !e.metaKey && currentFocusIndex >= 0 && visibleRows[currentFocusIndex]) {
|
|
506
|
+
e.preventDefault();
|
|
507
|
+
const focusedRow = visibleRows[currentFocusIndex];
|
|
508
|
+
const rowType = focusedRow.dataset.type;
|
|
509
|
+
|
|
510
|
+
if (rowType === 'file') {
|
|
511
|
+
const filePath = focusedRow.dataset.path;
|
|
512
|
+
const diffBtn = focusedRow.querySelector('.diff-btn');
|
|
513
|
+
if (diffBtn) {
|
|
514
|
+
showDiffViewer(filePath);
|
|
515
|
+
}
|
|
516
|
+
}
|
|
517
|
+
}
|
|
518
|
+
break;
|
|
519
|
+
}
|
|
520
|
+
}
|
|
521
|
+
|
|
522
|
+
function getVisibleRows() {
|
|
523
|
+
return fileRows.filter(row => !row.classList.contains('hidden'));
|
|
524
|
+
}
|
|
525
|
+
|
|
526
|
+
function focusFirstVisibleRow() {
|
|
527
|
+
const visibleRows = getVisibleRows();
|
|
528
|
+
if (visibleRows.length > 0) {
|
|
529
|
+
currentFocusIndex = 0;
|
|
530
|
+
updateFocus(visibleRows);
|
|
531
|
+
}
|
|
532
|
+
}
|
|
533
|
+
|
|
534
|
+
function navigateDown(visibleRows) {
|
|
535
|
+
if (visibleRows.length === 0) return;
|
|
536
|
+
|
|
537
|
+
currentFocusIndex++;
|
|
538
|
+
if (currentFocusIndex >= visibleRows.length) {
|
|
539
|
+
currentFocusIndex = 0;
|
|
540
|
+
}
|
|
541
|
+
updateFocus(visibleRows);
|
|
542
|
+
}
|
|
543
|
+
|
|
544
|
+
function navigateUp(visibleRows) {
|
|
545
|
+
if (visibleRows.length === 0) return;
|
|
546
|
+
|
|
547
|
+
currentFocusIndex--;
|
|
548
|
+
if (currentFocusIndex < 0) {
|
|
549
|
+
currentFocusIndex = visibleRows.length - 1;
|
|
550
|
+
}
|
|
551
|
+
updateFocus(visibleRows);
|
|
552
|
+
}
|
|
553
|
+
|
|
554
|
+
function updateFocus(visibleRows) {
|
|
555
|
+
clearFocus();
|
|
556
|
+
if (currentFocusIndex >= 0 && visibleRows[currentFocusIndex]) {
|
|
557
|
+
visibleRows[currentFocusIndex].classList.add('focused');
|
|
558
|
+
visibleRows[currentFocusIndex].scrollIntoView({
|
|
559
|
+
block: 'nearest',
|
|
560
|
+
behavior: 'smooth'
|
|
561
|
+
});
|
|
562
|
+
}
|
|
563
|
+
}
|
|
564
|
+
|
|
565
|
+
function clearFocus() {
|
|
566
|
+
fileRows.forEach(row => row.classList.remove('focused'));
|
|
567
|
+
}
|
|
568
|
+
|
|
569
|
+
function goUpDirectory() {
|
|
570
|
+
const currentUrl = new URL(window.location.href);
|
|
571
|
+
const currentPath = currentUrl.searchParams.get('path');
|
|
572
|
+
|
|
573
|
+
if (!currentPath || currentPath === '') {
|
|
574
|
+
// Already at root, do nothing
|
|
575
|
+
return;
|
|
576
|
+
}
|
|
577
|
+
|
|
578
|
+
const pathParts = currentPath.split('/').filter(p => p);
|
|
579
|
+
if (pathParts.length === 0) {
|
|
580
|
+
// Go to root
|
|
581
|
+
window.location.href = '/';
|
|
582
|
+
} else {
|
|
583
|
+
// Go up one directory
|
|
584
|
+
pathParts.pop();
|
|
585
|
+
const newPath = pathParts.join('/');
|
|
586
|
+
window.location.href = `/?path=${encodeURIComponent(newPath)}`;
|
|
587
|
+
}
|
|
588
|
+
}
|
|
589
|
+
|
|
590
|
+
|
|
591
|
+
function selectLine(lineNum) {
|
|
592
|
+
const lineContainer = document.querySelector(`[data-line="${lineNum}"]`);
|
|
593
|
+
if (lineContainer) {
|
|
594
|
+
lineContainer.classList.add('selected');
|
|
595
|
+
}
|
|
596
|
+
}
|
|
597
|
+
|
|
598
|
+
function toggleLineSelection(lineNum) {
|
|
599
|
+
const lineContainer = document.querySelector(`[data-line="${lineNum}"]`);
|
|
600
|
+
if (lineContainer) {
|
|
601
|
+
lineContainer.classList.toggle('selected');
|
|
602
|
+
}
|
|
603
|
+
}
|
|
604
|
+
|
|
605
|
+
function selectLineRange(startLine, endLine) {
|
|
606
|
+
clearAllSelections();
|
|
607
|
+
for (let i = startLine; i <= endLine; i++) {
|
|
608
|
+
selectLine(i);
|
|
609
|
+
}
|
|
610
|
+
}
|
|
611
|
+
|
|
612
|
+
function clearAllSelections() {
|
|
613
|
+
document.querySelectorAll('.line-container.selected').forEach(line => {
|
|
614
|
+
line.classList.remove('selected');
|
|
615
|
+
});
|
|
616
|
+
}
|
|
617
|
+
|
|
618
|
+
function updateURL() {
|
|
619
|
+
const selectedLines = Array.from(document.querySelectorAll('.line-container.selected'))
|
|
620
|
+
.map(line => parseInt(line.dataset.line))
|
|
621
|
+
.sort((a, b) => a - b);
|
|
622
|
+
|
|
623
|
+
const url = new URL(window.location);
|
|
624
|
+
|
|
625
|
+
if (selectedLines.length === 0) {
|
|
626
|
+
url.hash = '';
|
|
627
|
+
} else if (selectedLines.length === 1) {
|
|
628
|
+
url.hash = `#L${selectedLines[0]}`;
|
|
629
|
+
} else {
|
|
630
|
+
// Find ranges
|
|
631
|
+
const ranges = [];
|
|
632
|
+
let rangeStart = selectedLines[0];
|
|
633
|
+
let rangeEnd = selectedLines[0];
|
|
634
|
+
|
|
635
|
+
for (let i = 1; i < selectedLines.length; i++) {
|
|
636
|
+
if (selectedLines[i] === rangeEnd + 1) {
|
|
637
|
+
rangeEnd = selectedLines[i];
|
|
638
|
+
} else {
|
|
639
|
+
if (rangeStart === rangeEnd) {
|
|
640
|
+
ranges.push(`L${rangeStart}`);
|
|
641
|
+
} else {
|
|
642
|
+
ranges.push(`L${rangeStart}-L${rangeEnd}`);
|
|
643
|
+
}
|
|
644
|
+
rangeStart = rangeEnd = selectedLines[i];
|
|
645
|
+
}
|
|
646
|
+
}
|
|
647
|
+
|
|
648
|
+
if (rangeStart === rangeEnd) {
|
|
649
|
+
ranges.push(`L${rangeStart}`);
|
|
650
|
+
} else {
|
|
651
|
+
ranges.push(`L${rangeStart}-L${rangeEnd}`);
|
|
652
|
+
}
|
|
653
|
+
|
|
654
|
+
url.hash = `#${ranges.join(',')}`;
|
|
655
|
+
}
|
|
656
|
+
|
|
657
|
+
window.history.replaceState({}, '', url);
|
|
658
|
+
}
|
|
659
|
+
|
|
660
|
+
// Initialize line selections from URL on page load
|
|
661
|
+
function initLineSelections() {
|
|
662
|
+
const hash = window.location.hash.slice(1);
|
|
663
|
+
if (!hash) return;
|
|
664
|
+
|
|
665
|
+
const parts = hash.split(',');
|
|
666
|
+
parts.forEach(part => {
|
|
667
|
+
if (part.includes('-')) {
|
|
668
|
+
const [start, end] = part.split('-').map(p => parseInt(p.replace('L', '')));
|
|
669
|
+
selectLineRange(start, end);
|
|
670
|
+
} else {
|
|
671
|
+
const lineNum = parseInt(part.replace('L', ''));
|
|
672
|
+
selectLine(lineNum);
|
|
673
|
+
}
|
|
674
|
+
});
|
|
675
|
+
}
|
|
676
|
+
|
|
677
|
+
// Initialize line selections if we're on a file page
|
|
678
|
+
if (document.querySelector('.with-line-numbers')) {
|
|
679
|
+
initLineSelections();
|
|
680
|
+
}
|
|
681
|
+
|
|
682
|
+
// Editor functionality
|
|
683
|
+
const editBtn = document.getElementById('edit-btn');
|
|
684
|
+
const editorContainer = document.getElementById('editor-container');
|
|
685
|
+
const saveBtn = document.getElementById('save-btn');
|
|
686
|
+
const cancelBtn = document.getElementById('cancel-btn');
|
|
687
|
+
const fileContent = document.querySelector('.file-content');
|
|
688
|
+
|
|
689
|
+
// Line numbers functionality
|
|
690
|
+
function updateLineNumbers(textarea, lineNumbersDiv) {
|
|
691
|
+
if (!textarea || !lineNumbersDiv) return;
|
|
692
|
+
|
|
693
|
+
const lines = textarea.value.split('\n');
|
|
694
|
+
const lineNumbers = lines.map((_, index) => index + 1).join('\n');
|
|
695
|
+
lineNumbersDiv.textContent = lineNumbers || '1';
|
|
696
|
+
}
|
|
697
|
+
|
|
698
|
+
// Initialize and handle line numbers for both editors
|
|
699
|
+
const editorLineNumbers = document.getElementById('editor-line-numbers');
|
|
700
|
+
const newFileContent = document.getElementById('new-file-content');
|
|
701
|
+
const newFileLineNumbers = document.getElementById('new-file-line-numbers');
|
|
702
|
+
|
|
703
|
+
// Get fileEditor reference (declared earlier in the file)
|
|
704
|
+
if (document.getElementById('file-editor') && editorLineNumbers) {
|
|
705
|
+
const fileEditorElement = document.getElementById('file-editor');
|
|
706
|
+
fileEditorElement.addEventListener('input', () => updateLineNumbers(fileEditorElement, editorLineNumbers));
|
|
707
|
+
fileEditorElement.addEventListener('scroll', () => {
|
|
708
|
+
editorLineNumbers.scrollTop = fileEditorElement.scrollTop;
|
|
709
|
+
});
|
|
710
|
+
}
|
|
711
|
+
|
|
712
|
+
if (newFileContent && newFileLineNumbers) {
|
|
713
|
+
newFileContent.addEventListener('input', () => updateLineNumbers(newFileContent, newFileLineNumbers));
|
|
714
|
+
newFileContent.addEventListener('scroll', () => {
|
|
715
|
+
newFileLineNumbers.scrollTop = newFileContent.scrollTop;
|
|
716
|
+
});
|
|
717
|
+
}
|
|
718
|
+
|
|
719
|
+
// Auto-open editor if hash is #edit
|
|
720
|
+
if (window.location.hash === '#edit' && editBtn) {
|
|
721
|
+
// Remove hash and trigger edit
|
|
722
|
+
window.location.hash = '';
|
|
723
|
+
setTimeout(() => editBtn.click(), 100);
|
|
724
|
+
}
|
|
725
|
+
|
|
726
|
+
if (editBtn && editorContainer && fileEditor) {
|
|
727
|
+
let originalContent = '';
|
|
728
|
+
|
|
729
|
+
// Editor keyboard shortcuts
|
|
730
|
+
document.addEventListener('keydown', function(e) {
|
|
731
|
+
if (editorContainer && editorContainer.style.display !== 'none') {
|
|
732
|
+
// Cmd/Ctrl+S to save
|
|
733
|
+
if ((e.ctrlKey || e.metaKey) && e.key === 's') {
|
|
734
|
+
e.preventDefault();
|
|
735
|
+
saveBtn.click();
|
|
736
|
+
}
|
|
737
|
+
// Escape to cancel
|
|
738
|
+
if (e.key === 'Escape') {
|
|
739
|
+
e.preventDefault();
|
|
740
|
+
cancelBtn.click();
|
|
741
|
+
}
|
|
742
|
+
}
|
|
743
|
+
});
|
|
744
|
+
|
|
745
|
+
// Auto-save functionality
|
|
746
|
+
function saveDraft(filePath, content) {
|
|
747
|
+
localStorage.setItem(`gh-here-draft-${filePath}`, content);
|
|
748
|
+
}
|
|
749
|
+
|
|
750
|
+
function loadDraft(filePath) {
|
|
751
|
+
return localStorage.getItem(`gh-here-draft-${filePath}`);
|
|
752
|
+
}
|
|
753
|
+
|
|
754
|
+
function clearDraft(filePath) {
|
|
755
|
+
localStorage.removeItem(`gh-here-draft-${filePath}`);
|
|
756
|
+
}
|
|
757
|
+
|
|
758
|
+
editBtn.addEventListener('click', function() {
|
|
759
|
+
// Get current file path
|
|
760
|
+
const currentUrl = new URL(window.location.href);
|
|
761
|
+
const filePath = currentUrl.searchParams.get('path') || '';
|
|
762
|
+
|
|
763
|
+
|
|
764
|
+
// Fetch original file content
|
|
765
|
+
fetch(`/api/file-content?path=${encodeURIComponent(filePath)}`)
|
|
766
|
+
.then(response => {
|
|
767
|
+
if (!response.ok) {
|
|
768
|
+
throw new Error(`HTTP ${response.status}: ${response.statusText}`);
|
|
769
|
+
}
|
|
770
|
+
return response.text();
|
|
771
|
+
})
|
|
772
|
+
.then(content => {
|
|
773
|
+
originalContent = content;
|
|
774
|
+
|
|
775
|
+
// Check for draft
|
|
776
|
+
const draft = loadDraft(filePath);
|
|
777
|
+
if (draft && draft !== content) {
|
|
778
|
+
if (confirm('You have unsaved changes for this file. Load draft?')) {
|
|
779
|
+
fileEditor.value = draft;
|
|
780
|
+
} else {
|
|
781
|
+
fileEditor.value = content;
|
|
782
|
+
clearDraft(filePath);
|
|
783
|
+
}
|
|
784
|
+
} else {
|
|
785
|
+
fileEditor.value = content;
|
|
786
|
+
}
|
|
787
|
+
|
|
788
|
+
fileContent.style.display = 'none';
|
|
789
|
+
editorContainer.style.display = 'block';
|
|
790
|
+
fileEditor.focus();
|
|
791
|
+
|
|
792
|
+
// Set up auto-save and update line numbers
|
|
793
|
+
fileEditor.addEventListener('input', function() {
|
|
794
|
+
saveDraft(filePath, fileEditor.value);
|
|
795
|
+
});
|
|
796
|
+
|
|
797
|
+
// Update line numbers for loaded content
|
|
798
|
+
updateLineNumbers(fileEditor, editorLineNumbers);
|
|
799
|
+
})
|
|
800
|
+
.catch(error => {
|
|
801
|
+
console.error('Error fetching file content:', error);
|
|
802
|
+
let errorMessage = 'Failed to load file content for editing';
|
|
803
|
+
if (error.message.includes('HTTP 403')) {
|
|
804
|
+
errorMessage = 'Access denied: Cannot read this file';
|
|
805
|
+
} else if (error.message.includes('HTTP 404')) {
|
|
806
|
+
errorMessage = 'File not found';
|
|
807
|
+
} else if (error.message.includes('NetworkError') || error.message.includes('Failed to fetch')) {
|
|
808
|
+
errorMessage = 'Network error: Please check your connection';
|
|
809
|
+
}
|
|
810
|
+
showNotification(errorMessage, 'error');
|
|
811
|
+
});
|
|
812
|
+
});
|
|
813
|
+
|
|
814
|
+
cancelBtn.addEventListener('click', function() {
|
|
815
|
+
editorContainer.style.display = 'none';
|
|
816
|
+
fileContent.style.display = 'block';
|
|
817
|
+
});
|
|
818
|
+
|
|
819
|
+
saveBtn.addEventListener('click', function() {
|
|
820
|
+
const currentUrl = new URL(window.location.href);
|
|
821
|
+
const filePath = currentUrl.searchParams.get('path') || '';
|
|
822
|
+
|
|
823
|
+
|
|
824
|
+
fetch('/api/save-file', {
|
|
825
|
+
method: 'POST',
|
|
826
|
+
headers: {
|
|
827
|
+
'Content-Type': 'application/json',
|
|
828
|
+
},
|
|
829
|
+
body: JSON.stringify({
|
|
830
|
+
path: filePath,
|
|
831
|
+
content: fileEditor.value
|
|
832
|
+
})
|
|
833
|
+
})
|
|
834
|
+
.then(response => {
|
|
835
|
+
if (!response.ok) {
|
|
836
|
+
throw new Error(`HTTP ${response.status}: ${response.statusText}`);
|
|
837
|
+
}
|
|
838
|
+
return response.json();
|
|
839
|
+
})
|
|
840
|
+
.then(data => {
|
|
841
|
+
if (data.success) {
|
|
842
|
+
// Clear draft on successful save
|
|
843
|
+
clearDraft(filePath);
|
|
844
|
+
showNotification('File saved successfully', 'success');
|
|
845
|
+
// Refresh the page to show updated content
|
|
846
|
+
setTimeout(() => window.location.reload(), 800);
|
|
847
|
+
} else {
|
|
848
|
+
showNotification('Failed to save file: ' + data.error, 'error');
|
|
849
|
+
}
|
|
850
|
+
})
|
|
851
|
+
.catch(error => {
|
|
852
|
+
console.error('Error saving file:', error);
|
|
853
|
+
let errorMessage = 'Failed to save file';
|
|
854
|
+
if (error.message.includes('HTTP 403')) {
|
|
855
|
+
errorMessage = 'Access denied: Cannot write to this file';
|
|
856
|
+
} else if (error.message.includes('HTTP 413')) {
|
|
857
|
+
errorMessage = 'File too large to save';
|
|
858
|
+
} else if (error.message.includes('NetworkError') || error.message.includes('Failed to fetch')) {
|
|
859
|
+
errorMessage = 'Network error: Please check your connection';
|
|
860
|
+
}
|
|
861
|
+
showNotification(errorMessage, 'error');
|
|
862
|
+
});
|
|
863
|
+
});
|
|
864
|
+
}
|
|
865
|
+
|
|
866
|
+
// Quick edit file functionality
|
|
867
|
+
document.addEventListener('click', function(e) {
|
|
868
|
+
if (e.target.closest('.edit-file-btn')) {
|
|
869
|
+
e.preventDefault();
|
|
870
|
+
e.stopPropagation();
|
|
871
|
+
const button = e.target.closest('.edit-file-btn');
|
|
872
|
+
const filePath = button.dataset.path;
|
|
873
|
+
// Navigate to the file and trigger edit mode
|
|
874
|
+
window.location.href = `/?path=${encodeURIComponent(filePath)}#edit`;
|
|
875
|
+
}
|
|
876
|
+
|
|
877
|
+
// Git diff viewer functionality
|
|
878
|
+
if (e.target.closest('.diff-btn')) {
|
|
879
|
+
e.preventDefault();
|
|
880
|
+
e.stopPropagation();
|
|
881
|
+
const button = e.target.closest('.diff-btn');
|
|
882
|
+
const filePath = button.dataset.path;
|
|
883
|
+
showDiffViewer(filePath);
|
|
884
|
+
}
|
|
885
|
+
});
|
|
886
|
+
|
|
887
|
+
// Git diff viewer functions
|
|
888
|
+
function showDiffViewer(filePath) {
|
|
889
|
+
// Create diff viewer overlay
|
|
890
|
+
const overlay = document.createElement('div');
|
|
891
|
+
overlay.className = 'diff-viewer-overlay';
|
|
892
|
+
overlay.innerHTML = `
|
|
893
|
+
<div class="diff-viewer-modal">
|
|
894
|
+
<div class="diff-viewer-header">
|
|
895
|
+
<h3 class="diff-viewer-title">
|
|
896
|
+
📋 Diff: ${filePath}
|
|
897
|
+
</h3>
|
|
898
|
+
<button class="diff-close-btn" aria-label="Close diff viewer">×</button>
|
|
899
|
+
</div>
|
|
900
|
+
<div class="diff-viewer-content">
|
|
901
|
+
<div style="padding: 40px; text-align: center;">Fetching diff...</div>
|
|
902
|
+
</div>
|
|
903
|
+
</div>
|
|
904
|
+
`;
|
|
905
|
+
|
|
906
|
+
document.body.appendChild(overlay);
|
|
907
|
+
|
|
908
|
+
// Close on overlay click or close button
|
|
909
|
+
overlay.addEventListener('click', function(e) {
|
|
910
|
+
if (e.target === overlay || e.target.classList.contains('diff-close-btn')) {
|
|
911
|
+
document.body.removeChild(overlay);
|
|
912
|
+
}
|
|
913
|
+
});
|
|
914
|
+
|
|
915
|
+
// Close with Escape key
|
|
916
|
+
const escHandler = function(e) {
|
|
917
|
+
if (e.key === 'Escape') {
|
|
918
|
+
document.body.removeChild(overlay);
|
|
919
|
+
document.removeEventListener('keydown', escHandler);
|
|
920
|
+
}
|
|
921
|
+
};
|
|
922
|
+
document.addEventListener('keydown', escHandler);
|
|
923
|
+
|
|
924
|
+
// Load diff content
|
|
925
|
+
loadDiffContent(filePath, overlay);
|
|
926
|
+
}
|
|
927
|
+
|
|
928
|
+
function loadDiffContent(filePath, overlay) {
|
|
929
|
+
fetch(`/api/git-diff?path=${encodeURIComponent(filePath)}`)
|
|
930
|
+
.then(response => response.json())
|
|
931
|
+
.then(data => {
|
|
932
|
+
if (data.success) {
|
|
933
|
+
renderDiff(data.diff, data.filePath, overlay);
|
|
934
|
+
} else {
|
|
935
|
+
showDiffError(data.error, overlay);
|
|
936
|
+
}
|
|
937
|
+
})
|
|
938
|
+
.catch(error => {
|
|
939
|
+
console.error('Error loading diff:', error);
|
|
940
|
+
showDiffError('Failed to load diff', overlay);
|
|
941
|
+
});
|
|
942
|
+
}
|
|
943
|
+
|
|
944
|
+
function renderDiff(diffText, filePath, overlay) {
|
|
945
|
+
if (!diffText || diffText.trim() === '') {
|
|
946
|
+
const content = overlay.querySelector('.diff-viewer-content');
|
|
947
|
+
content.innerHTML = `
|
|
948
|
+
<div style="padding: 40px; text-align: center; color: var(--text-secondary);">
|
|
949
|
+
No changes detected for this file
|
|
950
|
+
</div>
|
|
951
|
+
`;
|
|
952
|
+
return;
|
|
953
|
+
}
|
|
954
|
+
|
|
955
|
+
const lines = diffText.split('\n');
|
|
956
|
+
const parsedDiff = parseDiff(lines);
|
|
957
|
+
|
|
958
|
+
const content = overlay.querySelector('.diff-viewer-content');
|
|
959
|
+
content.innerHTML = `
|
|
960
|
+
<div class="diff-container">
|
|
961
|
+
<div class="diff-side">
|
|
962
|
+
<div class="diff-side-header">Original</div>
|
|
963
|
+
<div class="diff-side-content" id="diff-original"></div>
|
|
964
|
+
</div>
|
|
965
|
+
<div class="diff-side">
|
|
966
|
+
<div class="diff-side-header">Modified</div>
|
|
967
|
+
<div class="diff-side-content" id="diff-modified"></div>
|
|
968
|
+
</div>
|
|
969
|
+
</div>
|
|
970
|
+
`;
|
|
971
|
+
|
|
972
|
+
const originalSide = content.querySelector('#diff-original');
|
|
973
|
+
const modifiedSide = content.querySelector('#diff-modified');
|
|
974
|
+
|
|
975
|
+
renderDiffSide(parsedDiff.original, originalSide, 'original');
|
|
976
|
+
renderDiffSide(parsedDiff.modified, modifiedSide, 'modified');
|
|
977
|
+
}
|
|
978
|
+
|
|
979
|
+
function parseDiff(lines) {
|
|
980
|
+
const original = [];
|
|
981
|
+
const modified = [];
|
|
982
|
+
let originalLineNum = 1;
|
|
983
|
+
let modifiedLineNum = 1;
|
|
984
|
+
|
|
985
|
+
for (let i = 0; i < lines.length; i++) {
|
|
986
|
+
const line = lines[i];
|
|
987
|
+
|
|
988
|
+
if (line.startsWith('@@')) {
|
|
989
|
+
// Parse hunk header
|
|
990
|
+
const match = line.match(/@@ -(\d+)(?:,\d+)? \+(\d+)(?:,\d+)? @@/);
|
|
991
|
+
if (match) {
|
|
992
|
+
originalLineNum = parseInt(match[1]);
|
|
993
|
+
modifiedLineNum = parseInt(match[2]);
|
|
994
|
+
}
|
|
995
|
+
continue;
|
|
996
|
+
}
|
|
997
|
+
|
|
998
|
+
if (line.startsWith('---') || line.startsWith('+++') || line.startsWith('diff ') || line.startsWith('index ')) {
|
|
999
|
+
continue;
|
|
1000
|
+
}
|
|
1001
|
+
|
|
1002
|
+
if (line.startsWith('-')) {
|
|
1003
|
+
original.push({
|
|
1004
|
+
lineNum: originalLineNum++,
|
|
1005
|
+
content: line.substring(1),
|
|
1006
|
+
type: 'removed'
|
|
1007
|
+
});
|
|
1008
|
+
} else if (line.startsWith('+')) {
|
|
1009
|
+
modified.push({
|
|
1010
|
+
lineNum: modifiedLineNum++,
|
|
1011
|
+
content: line.substring(1),
|
|
1012
|
+
type: 'added'
|
|
1013
|
+
});
|
|
1014
|
+
} else {
|
|
1015
|
+
// Context line
|
|
1016
|
+
const content = line.startsWith(' ') ? line.substring(1) : line;
|
|
1017
|
+
original.push({
|
|
1018
|
+
lineNum: originalLineNum++,
|
|
1019
|
+
content: content,
|
|
1020
|
+
type: 'context'
|
|
1021
|
+
});
|
|
1022
|
+
modified.push({
|
|
1023
|
+
lineNum: modifiedLineNum++,
|
|
1024
|
+
content: content,
|
|
1025
|
+
type: 'context'
|
|
1026
|
+
});
|
|
1027
|
+
}
|
|
1028
|
+
}
|
|
1029
|
+
|
|
1030
|
+
return { original, modified };
|
|
1031
|
+
}
|
|
1032
|
+
|
|
1033
|
+
function renderDiffSide(lines, container, side) {
|
|
1034
|
+
container.innerHTML = lines.map(line => {
|
|
1035
|
+
let content = escapeHtml(line.content);
|
|
1036
|
+
|
|
1037
|
+
// Apply syntax highlighting if hljs is available
|
|
1038
|
+
if (window.hljs && line.content.trim() !== '') {
|
|
1039
|
+
try {
|
|
1040
|
+
const highlighted = hljs.highlightAuto(line.content);
|
|
1041
|
+
content = highlighted.value;
|
|
1042
|
+
} catch (e) {
|
|
1043
|
+
// Fall back to escaped HTML if highlighting fails
|
|
1044
|
+
content = escapeHtml(line.content);
|
|
1045
|
+
}
|
|
1046
|
+
}
|
|
1047
|
+
|
|
1048
|
+
return `
|
|
1049
|
+
<div class="diff-line diff-line-${line.type}">
|
|
1050
|
+
<div class="diff-line-number">${line.lineNum}</div>
|
|
1051
|
+
<div class="diff-line-content">${content}</div>
|
|
1052
|
+
</div>
|
|
1053
|
+
`;
|
|
1054
|
+
}).join('');
|
|
1055
|
+
}
|
|
1056
|
+
|
|
1057
|
+
function escapeHtml(text) {
|
|
1058
|
+
const div = document.createElement('div');
|
|
1059
|
+
div.textContent = text;
|
|
1060
|
+
return div.innerHTML;
|
|
1061
|
+
}
|
|
1062
|
+
|
|
1063
|
+
function showDiffError(error, overlay) {
|
|
1064
|
+
const content = overlay.querySelector('.diff-viewer-content');
|
|
1065
|
+
content.innerHTML = `
|
|
1066
|
+
<div style="padding: 40px; text-align: center; color: var(--text-secondary);">
|
|
1067
|
+
<p>Error loading diff:</p>
|
|
1068
|
+
<p style="color: #dc3545; margin-top: 8px;">${error}</p>
|
|
1069
|
+
</div>
|
|
1070
|
+
`;
|
|
1071
|
+
}
|
|
1072
|
+
|
|
1073
|
+
// File operations (delete, rename)
|
|
1074
|
+
document.addEventListener('click', function(e) {
|
|
1075
|
+
if (e.target.closest('.delete-btn')) {
|
|
1076
|
+
const btn = e.target.closest('.delete-btn');
|
|
1077
|
+
const itemPath = btn.dataset.path;
|
|
1078
|
+
const itemName = btn.dataset.name;
|
|
1079
|
+
const isDirectory = btn.dataset.isDirectory === 'true';
|
|
1080
|
+
|
|
1081
|
+
const confirmMessage = `Are you sure you want to delete ${isDirectory ? 'folder' : 'file'} "${itemName}"?${isDirectory ? ' This will permanently delete the folder and all its contents.' : ''}`;
|
|
1082
|
+
|
|
1083
|
+
if (confirm(confirmMessage)) {
|
|
1084
|
+
btn.style.opacity = '0.5';
|
|
1085
|
+
|
|
1086
|
+
fetch('/api/delete', {
|
|
1087
|
+
method: 'POST',
|
|
1088
|
+
headers: {
|
|
1089
|
+
'Content-Type': 'application/json',
|
|
1090
|
+
},
|
|
1091
|
+
body: JSON.stringify({
|
|
1092
|
+
path: itemPath
|
|
1093
|
+
})
|
|
1094
|
+
})
|
|
1095
|
+
.then(response => {
|
|
1096
|
+
if (!response.ok) {
|
|
1097
|
+
throw new Error(`HTTP ${response.status}: ${response.statusText}`);
|
|
1098
|
+
}
|
|
1099
|
+
return response.json();
|
|
1100
|
+
})
|
|
1101
|
+
.then(data => {
|
|
1102
|
+
if (data.success) {
|
|
1103
|
+
showNotification(`${isDirectory ? 'Folder' : 'File'} "${itemName}" deleted successfully`, 'success');
|
|
1104
|
+
setTimeout(() => window.location.reload(), 600);
|
|
1105
|
+
} else {
|
|
1106
|
+
btn.style.opacity = '1';
|
|
1107
|
+
showNotification('Failed to delete: ' + data.error, 'error');
|
|
1108
|
+
}
|
|
1109
|
+
})
|
|
1110
|
+
.catch(error => {
|
|
1111
|
+
console.error('Error deleting item:', error);
|
|
1112
|
+
btn.style.opacity = '1';
|
|
1113
|
+
let errorMessage = 'Failed to delete item';
|
|
1114
|
+
if (error.message.includes('HTTP 403')) {
|
|
1115
|
+
errorMessage = 'Access denied: Cannot delete this item';
|
|
1116
|
+
} else if (error.message.includes('HTTP 404')) {
|
|
1117
|
+
errorMessage = 'Item not found';
|
|
1118
|
+
} else if (error.message.includes('NetworkError') || error.message.includes('Failed to fetch')) {
|
|
1119
|
+
errorMessage = 'Network error: Please check your connection';
|
|
1120
|
+
}
|
|
1121
|
+
showNotification(errorMessage, 'error');
|
|
1122
|
+
});
|
|
1123
|
+
}
|
|
1124
|
+
}
|
|
1125
|
+
|
|
1126
|
+
if (e.target.closest('.rename-btn')) {
|
|
1127
|
+
const btn = e.target.closest('.rename-btn');
|
|
1128
|
+
const itemPath = btn.dataset.path;
|
|
1129
|
+
const currentName = btn.dataset.name;
|
|
1130
|
+
const isDirectory = btn.dataset.isDirectory === 'true';
|
|
1131
|
+
|
|
1132
|
+
const newName = prompt(`Rename ${isDirectory ? 'folder' : 'file'}:`, currentName);
|
|
1133
|
+
if (newName && newName.trim() && newName !== currentName) {
|
|
1134
|
+
btn.style.opacity = '0.5';
|
|
1135
|
+
|
|
1136
|
+
fetch('/api/rename', {
|
|
1137
|
+
method: 'POST',
|
|
1138
|
+
headers: {
|
|
1139
|
+
'Content-Type': 'application/json',
|
|
1140
|
+
},
|
|
1141
|
+
body: JSON.stringify({
|
|
1142
|
+
path: itemPath,
|
|
1143
|
+
newName: newName.trim()
|
|
1144
|
+
})
|
|
1145
|
+
})
|
|
1146
|
+
.then(response => {
|
|
1147
|
+
if (!response.ok) {
|
|
1148
|
+
throw new Error(`HTTP ${response.status}: ${response.statusText}`);
|
|
1149
|
+
}
|
|
1150
|
+
return response.json();
|
|
1151
|
+
})
|
|
1152
|
+
.then(data => {
|
|
1153
|
+
if (data.success) {
|
|
1154
|
+
showNotification(`${isDirectory ? 'Folder' : 'File'} renamed to "${newName.trim()}"`, 'success');
|
|
1155
|
+
setTimeout(() => window.location.reload(), 600);
|
|
1156
|
+
} else {
|
|
1157
|
+
btn.style.opacity = '1';
|
|
1158
|
+
showNotification('Failed to rename: ' + data.error, 'error');
|
|
1159
|
+
}
|
|
1160
|
+
})
|
|
1161
|
+
.catch(error => {
|
|
1162
|
+
console.error('Error renaming item:', error);
|
|
1163
|
+
btn.style.opacity = '1';
|
|
1164
|
+
let errorMessage = 'Failed to rename item';
|
|
1165
|
+
if (error.message.includes('HTTP 403')) {
|
|
1166
|
+
errorMessage = 'Access denied: Cannot rename this item';
|
|
1167
|
+
} else if (error.message.includes('HTTP 404')) {
|
|
1168
|
+
errorMessage = 'Item not found';
|
|
1169
|
+
} else if (error.message.includes('NetworkError') || error.message.includes('Failed to fetch')) {
|
|
1170
|
+
errorMessage = 'Network error: Please check your connection';
|
|
1171
|
+
}
|
|
1172
|
+
showNotification(errorMessage, 'error');
|
|
1173
|
+
});
|
|
1174
|
+
}
|
|
1175
|
+
}
|
|
1176
|
+
});
|
|
1177
|
+
|
|
1178
|
+
// New file interface functionality
|
|
1179
|
+
const newFilenameInput = document.getElementById('new-filename-input');
|
|
1180
|
+
const createNewFileBtn = document.getElementById('create-new-file');
|
|
1181
|
+
const cancelNewFileBtn = document.getElementById('cancel-new-file');
|
|
1182
|
+
|
|
1183
|
+
if (createNewFileBtn) {
|
|
1184
|
+
createNewFileBtn.addEventListener('click', function() {
|
|
1185
|
+
const filename = newFilenameInput.value.trim();
|
|
1186
|
+
const content = newFileContent.value;
|
|
1187
|
+
|
|
1188
|
+
if (!filename) {
|
|
1189
|
+
showNotification('Please enter a filename', 'error');
|
|
1190
|
+
newFilenameInput.focus();
|
|
1191
|
+
return;
|
|
1192
|
+
}
|
|
1193
|
+
|
|
1194
|
+
|
|
1195
|
+
const currentUrl = new URL(window.location.href);
|
|
1196
|
+
const currentPath = currentUrl.searchParams.get('path') || '';
|
|
1197
|
+
|
|
1198
|
+
fetch('/api/create-file', {
|
|
1199
|
+
method: 'POST',
|
|
1200
|
+
headers: {
|
|
1201
|
+
'Content-Type': 'application/json',
|
|
1202
|
+
},
|
|
1203
|
+
body: JSON.stringify({
|
|
1204
|
+
path: currentPath,
|
|
1205
|
+
filename: filename
|
|
1206
|
+
})
|
|
1207
|
+
})
|
|
1208
|
+
.then(response => {
|
|
1209
|
+
if (!response.ok) {
|
|
1210
|
+
throw new Error(`HTTP ${response.status}: ${response.statusText}`);
|
|
1211
|
+
}
|
|
1212
|
+
return response.json();
|
|
1213
|
+
})
|
|
1214
|
+
.then(data => {
|
|
1215
|
+
if (data.success) {
|
|
1216
|
+
// If there's content, save it
|
|
1217
|
+
if (content.trim()) {
|
|
1218
|
+
const filePath = currentPath ? `${currentPath}/${filename}` : filename;
|
|
1219
|
+
return fetch('/api/save-file', {
|
|
1220
|
+
method: 'POST',
|
|
1221
|
+
headers: {
|
|
1222
|
+
'Content-Type': 'application/json',
|
|
1223
|
+
},
|
|
1224
|
+
body: JSON.stringify({
|
|
1225
|
+
path: filePath,
|
|
1226
|
+
content: content
|
|
1227
|
+
})
|
|
1228
|
+
});
|
|
1229
|
+
}
|
|
1230
|
+
return { json: () => Promise.resolve({ success: true }) };
|
|
1231
|
+
} else {
|
|
1232
|
+
throw new Error(data.error);
|
|
1233
|
+
}
|
|
1234
|
+
})
|
|
1235
|
+
.then(response => response.json ? response.json() : response)
|
|
1236
|
+
.then(data => {
|
|
1237
|
+
if (data.success) {
|
|
1238
|
+
showNotification(`File "${filename}" created successfully`, 'success');
|
|
1239
|
+
// Navigate back to the directory or to the new file
|
|
1240
|
+
const redirectPath = currentPath ? `/?path=${encodeURIComponent(currentPath)}` : '/';
|
|
1241
|
+
setTimeout(() => window.location.href = redirectPath, 800);
|
|
1242
|
+
} else {
|
|
1243
|
+
throw new Error(data.error);
|
|
1244
|
+
}
|
|
1245
|
+
})
|
|
1246
|
+
.catch(error => {
|
|
1247
|
+
console.error('Error creating file:', error);
|
|
1248
|
+
let errorMessage = 'Failed to create file: ' + error.message;
|
|
1249
|
+
if (error.message.includes('HTTP 403')) {
|
|
1250
|
+
errorMessage = 'Access denied: Cannot create files in this directory';
|
|
1251
|
+
} else if (error.message.includes('HTTP 409') || error.message.includes('already exists')) {
|
|
1252
|
+
errorMessage = 'File already exists';
|
|
1253
|
+
} else if (error.message.includes('NetworkError') || error.message.includes('Failed to fetch')) {
|
|
1254
|
+
errorMessage = 'Network error: Please check your connection';
|
|
1255
|
+
}
|
|
1256
|
+
showNotification(errorMessage, 'error');
|
|
1257
|
+
});
|
|
1258
|
+
});
|
|
1259
|
+
}
|
|
1260
|
+
|
|
1261
|
+
if (cancelNewFileBtn) {
|
|
1262
|
+
cancelNewFileBtn.addEventListener('click', function() {
|
|
1263
|
+
const currentUrl = new URL(window.location.href);
|
|
1264
|
+
const currentPath = currentUrl.searchParams.get('path') || '';
|
|
1265
|
+
const redirectPath = currentPath ? `/?path=${encodeURIComponent(currentPath)}` : '/';
|
|
1266
|
+
window.location.href = redirectPath;
|
|
1267
|
+
});
|
|
1268
|
+
}
|
|
1269
|
+
|
|
1270
|
+
async function copyToClipboard(text, button) {
|
|
1271
|
+
try {
|
|
1272
|
+
await navigator.clipboard.writeText(text);
|
|
1273
|
+
|
|
1274
|
+
// Show success feedback
|
|
1275
|
+
const originalIcon = button.innerHTML;
|
|
1276
|
+
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>';
|
|
1277
|
+
|
|
1278
|
+
button.innerHTML = checkIcon;
|
|
1279
|
+
button.style.color = '#28a745';
|
|
1280
|
+
|
|
1281
|
+
setTimeout(() => {
|
|
1282
|
+
button.innerHTML = originalIcon;
|
|
1283
|
+
button.style.color = '';
|
|
1284
|
+
}, 1000);
|
|
1285
|
+
} catch (err) {
|
|
1286
|
+
// Fallback for older browsers
|
|
1287
|
+
const textArea = document.createElement('textarea');
|
|
1288
|
+
textArea.value = text;
|
|
1289
|
+
textArea.style.position = 'fixed';
|
|
1290
|
+
textArea.style.left = '-999999px';
|
|
1291
|
+
textArea.style.top = '-999999px';
|
|
1292
|
+
document.body.appendChild(textArea);
|
|
1293
|
+
textArea.focus();
|
|
1294
|
+
textArea.select();
|
|
1295
|
+
|
|
1296
|
+
try {
|
|
1297
|
+
document.execCommand('copy');
|
|
1298
|
+
// Show success feedback (same as above)
|
|
1299
|
+
const originalIcon = button.innerHTML;
|
|
1300
|
+
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>';
|
|
1301
|
+
|
|
1302
|
+
button.innerHTML = checkIcon;
|
|
1303
|
+
button.style.color = '#28a745';
|
|
1304
|
+
|
|
1305
|
+
setTimeout(() => {
|
|
1306
|
+
button.innerHTML = originalIcon;
|
|
1307
|
+
button.style.color = '';
|
|
1308
|
+
}, 1000);
|
|
1309
|
+
} catch (fallbackErr) {
|
|
1310
|
+
console.error('Could not copy text: ', fallbackErr);
|
|
1311
|
+
}
|
|
1312
|
+
|
|
1313
|
+
document.body.removeChild(textArea);
|
|
1314
|
+
}
|
|
1315
|
+
}
|
|
1316
|
+
});
|