Files
flowdeck/static/js/gitea_workspace.js
T
bruno 1fedafa808
FlowDeck CI / lint (push) Successful in 1m37s
FlowDeck CI / docker (push) Canceled after 0s
FlowDeck CI / test (push) Canceled after 1h9m58s
fix: sidebar Repository et pages forge — erreurs console, cache assets (v7.70.1)
2026-10-09 23:33:48 -04:00

799 lines
31 KiB
JavaScript

var _regGiteaWs = () => {
Alpine.data('giteaWorkspace', () => {
const params = new URLSearchParams(window.location.search);
const owner = params.get('owner') || '';
const repo = params.get('repo') || '';
// Forge is carried by the page route (/gitea-workspace vs /github-workspace);
// the sidebar cookie prefix follows it so refresh APIs resolve the mirror.
const forge = (window.location.pathname || '').indexOf('/github-workspace') === 0 ? 'github' : 'gitea';
return {
owner, repo, forge,
apiBase() {
return '/api/' + this.forge + '/projects/' + this.owner + '/' + this.repo;
},
// ── Library-style toolbar (same as /local-workspace) ──
viewMode: localStorage.getItem('fd_default_view') || 'tree',
viewMenuOpen: false,
sortOpen: false,
filterOpen: false,
searchOpen: false,
sortBy: '',
filterType: '',
searchQuery: '',
showFile: false,
showEditor: false,
showPrivate: false,
privatePages: [],
editingPrivate: null,
editingPrivateTitle: '',
editingPrivateContent: '',
filePath: '',
fileContent: '',
fileSize: 0,
fileSha: '',
fileLoading: false,
fileLanguage: 'plaintext',
fileLanguageLabel: 'Plain Text',
editContent: '',
commitMessage: 'Update via FlowDeck',
commitHistory: JSON.parse(localStorage.getItem('fd_commit_msgs') || '[]'),
// File tree browser
treeItems: [],
sortedTreeItems: [],
filteredTreeItems: [],
treeLoading: false,
treeError: false,
treeErrorText: '',
folderStack: [],
currentPath: '',
// Context menu
gwCtx: { visible: false, x: 0, y: 0, item: null },
_sortTree() {
var items = [...this.treeItems];
var by = this.sortBy || '';
var typeRank = function(t) { return (t === 'folder' || t === 'dir') ? 0 : 1; };
items.sort(function(a, b) {
if (by === 'name-desc') return (b.name || '').localeCompare(a.name || '');
if (by === 'type') {
var d = typeRank(a.type) - typeRank(b.type);
return d !== 0 ? d : (a.name || '').localeCompare(b.name || '');
}
// default name-asc: folders first
var td = typeRank(a.type) - typeRank(b.type);
if (td !== 0) return td;
return (a.name || '').localeCompare(b.name || '');
});
this.sortedTreeItems = items;
this.doFilter();
},
toggleViewMenu() { this.viewMenuOpen = !this.viewMenuOpen; this.sortOpen = false; this.filterOpen = false; },
toggleSortMenu() { this.sortOpen = !this.sortOpen; this.viewMenuOpen = false; this.filterOpen = false; },
toggleFilterMenu() { this.filterOpen = !this.filterOpen; this.viewMenuOpen = false; this.sortOpen = false; },
toggleSearch() {
this.searchOpen = !this.searchOpen;
if (!this.searchOpen) { this.searchQuery = ''; this.doFilter(); }
},
doSort() { this._sortTree(); },
filterByType(t) { this.filterType = (this.filterType === t ? '' : t); this.doFilter(); },
doFilter() {
var q = (this.searchQuery || '').toLowerCase();
var ft = this.filterType || '';
this.filteredTreeItems = (this.sortedTreeItems || []).filter(function(it) {
if (ft === 'folder' && it.type !== 'folder') return false;
if (ft && ft !== 'folder') {
var ext = (it.name || '').split('.').pop().toLowerCase();
var kind = (/^(png|jpe?g|gif|svg|webp|ico|bmp)$/.test(ext)) ? 'image'
: (/^(pdf)$/.test(ext)) ? 'pdf'
: (/^(js|jsx|ts|tsx|py|rb|rs|go|java|kt|c|cpp|h|cs|php|swift|r|lua|dart|sh|sql)$/.test(ext)) ? 'code'
: (/^(md|txt|json|ya?ml|toml|ini|csv)$/.test(ext)) ? 'text'
: 'page';
if (kind !== ft && !(ft === 'page' && it.type === 'file' && kind === 'page')) {
// 'page' filter = generic files; keep kind match only
if (ft === 'page' && it.type !== 'file') return false;
else if (ft !== 'page') return false;
}
}
if (q && (it.name || '').toLowerCase().indexOf(q) < 0 && (it.path || '').toLowerCase().indexOf(q) < 0) return false;
return true;
});
},
createFolderAtRoot() {
var name = prompt('Folder name:');
if (!name || !name.trim()) return;
this.createNewFolder(name.trim(), this.currentPath || '');
},
async createNewFolder(name, basePath) {
var path = (basePath ? basePath.replace(/\/$/, '') + '/' : '') + name + '/.gitkeep';
try {
var r = await fetch(this.apiBase() + '/file', {
method: 'PUT',
headers: { 'Content-Type': 'application/json', 'X-CSRF-Token': getCsrf() },
body: JSON.stringify({ path: path, content: '', message: 'Create folder ' + name + ' via FlowDeck' })
});
if (r.ok) await this.refreshTree();
else window.showToast('Create folder failed', 'error');
} catch (e) { window.showToast('Create folder failed', 'error'); }
},
init() {
// Expose globally for header to access
window._gwData = this;
// Set cookie for sidebar (forge-prefixed so Workspace mirror resolves)
document.cookie = 'flowdeck_workspace=' + this.forge + ':' + this.owner + ':' + this.repo + ';path=/;SameSite=Lax';
// Load sidebar tree (into gitea section)
this.loadSidebarTree();
// Load main content tree
this.loadMainTree('');
// Listen for sidebar clicks on Gitea items
this.setupSidebarClicks();
},
async loadMainTree(path) {
this.treeLoading = true;
this.treeError = false;
this.treeErrorText = '';
this.currentPath = path;
var forgeLabel = this.forge === 'github' ? 'GitHub' : 'Gitea';
try {
var url = this.apiBase() + '/tree';
if (path) url += '?path=' + encodeURIComponent(path);
var r = await fetch(url);
if (!r.ok) {
this.treeItems = []; this.sortedTreeItems = []; this.filteredTreeItems = [];
this.treeError = true;
this.treeErrorText = r.status === 401
? 'Connect your ' + forgeLabel + ' account (Settings → Integrations) to browse this repository.'
: 'Repository API answered HTTP ' + r.status + '.';
return;
}
var d = await r.json();
this.treeItems = d.tree || [];
this._sortTree();
} catch {
this.treeItems = []; this.sortedTreeItems = []; this.filteredTreeItems = [];
this.treeError = true;
this.treeErrorText = 'Could not reach the repository — the server may be restarting. Retry in a moment.';
}
finally { this.treeLoading = false; }
},
drillDown(path) {
this.folderStack.push(path.split('/').pop());
this.loadMainTree(path);
},
navigateToFolder(idx) {
// Truncate stack and rebuild path
this.folderStack = this.folderStack.slice(0, idx + 1);
var path = this.folderStack.join('/');
this.loadMainTree(path);
},
navigateToRoot() {
this.folderStack = [];
this.loadMainTree('');
},
openGwContext(ev, item) {
this.gwCtx = { visible: true, x: ev.clientX, y: ev.clientY, item: item };
},
gwRename() {
this.gwCtx.visible = false;
var item = this.gwCtx.item;
if (!item) return;
var newName = prompt('Rename:', item.name);
if (!newName || !newName.trim() || newName.trim() === item.name) return;
var self = this;
var oldPath = item.path;
var newPath = oldPath.replace(/[^/]+$/, newName.trim());
fetch(this.apiBase() + '/file', {
method: 'PUT',
headers: {'X-CSRF-Token': getCsrf(), 'Content-Type': 'application/json' },
body: JSON.stringify({ path: oldPath, new_path: newPath, message: 'Rename ' + oldPath + ' to ' + newPath })
}).then(function(r){ return r.json(); })
.then(function(d){
if (d.status === 'ok') { self.refreshTree(); }
else { window.showToast('Rename failed', 'error'); }
})
.catch(function(){ window.showToast('Rename failed', 'error'); });
},
gwDelete() {
this.gwCtx.visible = false;
var item = this.gwCtx.item;
if (!item) return;
if (!confirm('Delete ' + item.name + '?')) return;
var self = this;
fetch(this.apiBase() + '/file?path=' + encodeURIComponent(item.path) + '&sha=' + (item.sha || '') + '&message=Delete ' + item.path, {headers: {'X-CSRF-Token': getCsrf()},
method: 'DELETE'
}).then(function(r){
if (r.ok) { self.refreshTree(); }
else { window.showToast('Delete failed', 'error'); }
}).catch(function(){ window.showToast('Delete failed', 'error'); });
},
_sidebarHint(msg) {
var container = document.getElementById('sidebar-gitea-items');
if (container) container.innerHTML = '<li class="sidebar-item empty-hint"><span class="page-icon">'+getSvgIcon('link',14)+'</span><span class="page-name text-dim">' + msg + '</span></li>';
},
async loadSidebarTree() {
var forgeLabel = this.forge === 'github' ? 'GitHub' : 'Gitea';
try {
var r = await fetch(this.apiBase() + '/tree');
if (!r.ok) {
// API error (e.g. forge account not linked) — hint instead of
// leaving "Loading..." stuck in the Repository section.
this._sidebarHint(r.status === 401 ? 'Connect ' + forgeLabel + ' to browse files' : 'Could not load repository tree (HTTP ' + r.status + ')');
return;
}
var d = await r.json();
var items = d.tree || [];
var container = document.getElementById('sidebar-gitea-items');
if (!container) return;
// Ensure gitea section is visible
if (window.appState && window.appState.sectionsOpen && !window.appState.sectionsOpen.gitea) {
window.appState.sectionsOpen.gitea = true;
}
container.innerHTML = '';
// Same DOM contract as Workspace-local (_workspace_tree_macro):
// .sidebar-item.ws-tree-item + chevron + .page-icon/.page-name.
var html = '';
var sorted = [...items].sort(function(a, b) {
var at = (a.type === 'folder' || a.type === 'dir') ? 0 : 1;
var bt = (b.type === 'folder' || b.type === 'dir') ? 0 : 1;
if (at !== bt) return at - bt;
return (a.name || '').localeCompare(b.name || '');
});
for (var i = 0; i < sorted.length; i++) {
var item = sorted[i];
var isF = item.type === 'folder' || item.type === 'dir';
var safe = (item.name || '').replace(/'/g, "\\'");
var chev = isF
? '<button class="tree-chevron" data-repo-toggle="' + item.path + '" title="Toggle folder">▶</button>'
: '<span style="width:12px;flex-shrink:0;"></span>';
var icon = isF ? getSvgIcon('folder',14) : getSvgIcon('file',14);
var label = isF
? '<span class="page-name tree-folder-link" title="Open folder">' + item.name + '</span>'
: '<span class="page-name">' + item.name + '</span>';
html += '<li class="sidebar-item ws-tree-item" data-gitea-path="' + item.path
+ '" data-gitea-type="' + (isF ? 'folder' : 'file') + '" data-gitea-sha="' + (item.sha || '')
+ '" data-is-folder="' + (isF ? 'true' : 'false') + '" data-depth="0" data-name="' + safe + '"'
+ ' style="padding-left:4px;">' + chev
+ '<span class="page-icon">' + icon + '</span>' + label + '</li>';
}
// Add Private Pages link
html += '<li style="border-top:1px solid var(--border);margin:8px 0;"></li>';
html += '<li class="sidebar-item" id="gitea-private-link" style="padding-left:12px;cursor:pointer;display:flex;align-items:center;gap:4px;">';
html += '<span class="page-icon">'+getSvgIcon('lock',14)+'</span><span class="page-name">Private Pages</span></li>';
container.innerHTML = html;
// Attach sidebar clicks once (loadSidebarTree runs on every refresh —
// re-attaching would stack duplicate global listeners).
this.setupSidebarClicks();
} catch(e) {
// Network failure (server restarting, offline…) — hint in place of
// "Loading..." so the Repository section never looks broken.
this._sidebarHint('Could not reach the repository — retry in a moment');
}
},
setupSidebarClicks() {
if (this._sidebarClicksBound) return;
this._sidebarClicksBound = true;
var self = this;
document.addEventListener('click', function(e) {
var el = e.target.closest('.sidebar-item[data-gitea-path]');
if (!el) return;
var path = el.getAttribute('data-gitea-path');
var type = el.getAttribute('data-gitea-type');
// If clicking the checkbox, let its own handler deal with selection
if (e.target.classList.contains('gitea-checkbox')) return;
// If clicking the inline rename input, don't navigate
if (e.target.classList.contains('inline-rename-input')) return;
// If Shift or Ctrl/Meta is pressed, use multi-selection
if (e.shiftKey || e.ctrlKey || e.metaKey) {
e.preventDefault();
e.stopPropagation();
self.selectItem(path, el, e);
return;
}
// Normal click: navigate (open file/folder)
e.preventDefault();
e.stopPropagation();
// Update visual "active" state
document.querySelectorAll('.sidebar-item.active').forEach(function(si) {
si.classList.remove('active');
});
el.classList.add('active');
if (type === 'folder') {
self.loadSubdir(path, el);
} else {
self.openFile(path, el.getAttribute('data-gitea-sha'));
}
});
// Listen for custom delete event on gitea sidebar items
document.addEventListener('gitea-delete', function(e) {
var el = e.target;
var path = el.getAttribute('data-gitea-path');
if (!path) return;
if (!confirm('Delete ' + path + ' from ' + self.owner + '/' + self.repo + '?')) return;
var sha = el.getAttribute('data-gitea-sha') || '';
fetch(self.apiBase() + '/file?path=' + encodeURIComponent(path) + '&sha=' + encodeURIComponent(sha) + '&message=' + encodeURIComponent('Delete via FlowDeck'), {headers: {'X-CSRF-Token': getCsrf()},
method: 'DELETE',
}).then(function(r) {
if (r.ok) self.refreshTree();
}).catch(function() {});
});
},
async loadSubdir(path, el) {
var self = this;
// Check if already loaded
var ul = el.querySelector('ul');
if (ul) {
ul.style.display = ul.style.display === 'none' ? '' : 'none';
return;
}
try {
var r = await fetch(this.apiBase() + '/tree?path=' + encodeURIComponent(path));
if (!r.ok) return;
var d = await r.json();
var children = d.tree || [];
ul = document.createElement('ul');
ul.className = 'sidebar-items';
ul.style.cssText = 'list-style:none;margin:0;padding:0;';
var depth = parseInt(el.getAttribute('data-depth') || '0', 10) + 1;
children.forEach(function(child) {
var isF = child.type === 'folder' || child.type === 'dir';
var icon = isF ? getSvgIcon('folder',14) : getSvgIcon('file',14);
var li = document.createElement('li');
li.className = 'sidebar-item ws-tree-item';
li.setAttribute('data-gitea-path', child.path);
li.setAttribute('data-gitea-type', isF ? 'folder' : 'file');
li.setAttribute('data-gitea-sha', child.sha || '');
li.setAttribute('data-is-folder', isF ? 'true' : 'false');
li.setAttribute('data-depth', String(depth));
li.setAttribute('data-name', (child.name || '').replace(/'/g, "\\'"));
li.style.cssText = 'padding-left:' + (4 + depth * 12) + 'px;';
// Checkbox
var cb = document.createElement('input');
cb.type = 'checkbox';
cb.className = 'gitea-checkbox';
cb.style.cssText = 'flex-shrink:0;margin:0;display:none;';
cb.addEventListener('click', function(ev) {
ev.stopPropagation();
self.selectItem(child.path, li, ev);
});
li.appendChild(cb);
// Chevron (folders) — same as workspace-local
if (isF) {
var chev = document.createElement('button');
chev.className = 'tree-chevron';
chev.setAttribute('data-repo-toggle', child.path);
chev.title = 'Toggle folder';
chev.textContent = '▶';
li.appendChild(chev);
} else {
var sp = document.createElement('span');
sp.style.cssText = 'width:12px;flex-shrink:0;';
li.appendChild(sp);
}
// Icon
var iconSpan = document.createElement('span');
iconSpan.className = 'page-icon';
iconSpan.innerHTML = icon;
li.appendChild(iconSpan);
// Name
var nameSpan = document.createElement('span');
nameSpan.className = 'page-name' + (isF ? ' tree-folder-link' : '');
nameSpan.textContent = child.name;
if (isF) nameSpan.title = 'Open folder';
nameSpan.addEventListener('dblclick', function(ev) {
ev.preventDefault();
ev.stopPropagation();
self.startInlineRename(nameSpan, child.path, li);
});
li.appendChild(nameSpan);
ul.appendChild(li);
});
el.appendChild(ul);
var chevBtn = el.querySelector('[data-repo-toggle]');
if (chevBtn) chevBtn.classList.add('open');
} catch { /* volontaire */ }
},
async openFile(path, sha) {
this.filePath = path;
this.fileSha = sha || '';
this.showEditor = false;
this.showFile = true;
this.fileLoading = true;
this.fileLanguage = this.getLanguage(path);
try {
var r = await fetch(this.apiBase() + '/file?path=' + encodeURIComponent(path));
if (r.ok) {
var d = await r.json();
this.fileContent = d.content || '';
this.fileSize = d.content ? d.content.length : 0;
} else {
this.fileContent = '[Error loading file]';
}
} catch {
this.fileContent = '[Error loading file]';
}
this.fileLoading = false;
// Highlight after Alpine renders (FDHighlight covers ~230 file types;
// legacy Prism kept as a last-resort fallback).
var self = this;
this.$nextTick(function() {
var block = self.$refs.codeBlock;
if (!block || !block.textContent) return;
if (window.FDHighlight) {
try {
FDHighlight.highlightBlock(block, self.fileLanguage);
self.fileLanguageLabel = FDHighlight.labelFor(self.fileLanguage);
} catch (e) { /* plain text */ }
} else if (typeof Prism !== 'undefined') {
Prism.highlightElement(block);
}
});
},
getLanguage(path) {
if (window.FDHighlight) {
try { return FDHighlight.langForFile(path); } catch (e) { /* fall through */ }
}
var ext = (path || '').split('.').pop().toLowerCase();
var map = {
js:'javascript', jsx:'javascript', ts:'typescript', tsx:'typescript',
py:'python', rb:'ruby', rs:'rust', go:'go', java:'java', kt:'kotlin',
c:'c', cpp:'c', h:'c', hpp:'c', cs:'csharp',
html:'markup', htm:'markup', xml:'markup', svg:'markup', css:'css', scss:'css',
json:'json', yaml:'yaml', yml:'yaml', toml:'toml', ini:'ini',
md:'markdown', sql:'sql', sh:'bash', bash:'bash', zsh:'bash',
php:'php', swift:'swift', r:'r', lua:'lua', dart:'dart',
dockerfile:'docker', makefile:'makefile', cmake:'cmake',
};
return map[ext] || 'text';
},
openEditor() {
this.editContent = this.fileContent;
this.showEditor = true;
},
async saveFile() {
if (!this.filePath) return;
try {
var r = await fetch(this.apiBase() + '/file', {
method: 'PUT',
headers: {'Content-Type': 'application/json', 'X-CSRF-Token': this.getCsrfToken()},
body: JSON.stringify({
path: this.filePath, content: this.editContent,
message: this.commitMessage, sha: this.fileSha,
})
});
if (r.ok) {
this.fileContent = this.editContent;
this.showEditor = false;
if (!this.commitHistory.includes(this.commitMessage)) {
this.commitHistory.unshift(this.commitMessage);
if (this.commitHistory.length > 10) this.commitHistory.pop();
localStorage.setItem('fd_commit_msgs', JSON.stringify(this.commitHistory));
}
} else {
var d = await r.json();
window.showToast('Failed to save: ' + (d.error || 'Unknown error'), 'error');
}
} catch { window.showToast('Network error', 'error'); }
},
async deleteCurrentFile() {
if (!this.filePath) return;
if (!confirm('Delete ' + this.filePath + ' from ' + this.owner + '/' + this.repo + '?')) return;
try {
var r = await fetch(this.apiBase() + '/file?path=' + encodeURIComponent(this.filePath) + '&sha=' + encodeURIComponent(this.fileSha) + '&message=' + encodeURIComponent('Delete via FlowDeck'), {headers: {'X-CSRF-Token': getCsrf()},
method: 'DELETE',
});
if (r.ok) {
this.showFile = false;
this.filePath = '';
await this.refreshTree();
}
} catch { /* volontaire */ }
},
async refreshTree() {
// Reload main tree and sidebar tree without full page reload
this.loadMainTree(this.currentPath);
this.loadSidebarTree();
},
// ── A20 ph3 : new Date / x-html interdits sous build CSP ──
closePrivateEdit() {
this.editingPrivate = null;
this.editingPrivateTitle = '';
this.editingPrivateContent = '';
},
fmtGwDate(pp) {
return pp.updated_at ? new Date(pp.updated_at + 'Z').toLocaleString() : '';
},
bindGwIcon(el, item) {
Alpine.effect(() => {
el.innerHTML = item.type === 'folder' ? getSvgIcon('folder', 16) : getSvgIcon('file', 16);
});
},
formatSize(bytes) {
if (!bytes) return '';
if (bytes < 1024) return bytes + ' B';
if (bytes < 1048576) return (bytes/1024).toFixed(1) + ' KB';
return (bytes/1048576).toFixed(1) + ' MB';
},
// ── Private Pages ──
async loadPrivatePages() {
try {
var r = await fetch(this.apiBase() + '/private-pages');
if (r.ok) { var d = await r.json(); this.privatePages = d.pages || []; }
} catch { /* volontaire */ }
},
newPrivate() {
this.editingPrivate = 'new';
this.editingPrivateTitle = '';
this.editingPrivateContent = '';
},
async openPrivate(id) {
try {
var r = await fetch(this.apiBase() + '/private-pages/' + id);
if (r.ok) {
var d = await r.json();
this.editingPrivate = id;
this.editingPrivateTitle = d.page.title;
this.editingPrivateContent = d.page.content;
}
} catch { /* volontaire */ }
},
async savePrivate() {
var url = this.apiBase() + '/private-pages';
var method = 'POST';
if (this.editingPrivate !== 'new') {
url += '/' + this.editingPrivate;
method = 'PUT';
}
try {
var r = await fetch(url, {
method, headers: {'Content-Type':'application/json','X-CSRF-Token': this.getCsrfToken ? this.getCsrfToken() : ''},
body: JSON.stringify({title: this.editingPrivateTitle, content: this.editingPrivateContent})
});
if (r.ok) {
this.editingPrivate = null;
this.editingPrivateTitle = '';
this.editingPrivateContent = '';
await this.loadPrivatePages();
}
} catch { /* volontaire */ }
},
// Create new file
showNewFile: false,
newFilePath: '',
newFileContent: '',
newFileMsg: 'Create via FlowDeck',
async createNewFile() {
if (!this.newFilePath.trim()) return;
try {
var r = await fetch(this.apiBase() + '/file', {
method: 'PUT',
headers: {'Content-Type':'application/json', 'X-CSRF-Token': this.getCsrfToken()},
body: JSON.stringify({path: this.newFilePath.trim(), content: this.newFileContent, message: this.newFileMsg || 'Create via FlowDeck'})
});
if (r.ok) {
this.showNewFile = false;
this.newFilePath = '';
this.newFileContent = '';
this.newFileMsg = 'Create via FlowDeck';
await this.loadSidebarTree();
} else {
var d = await r.json();
window.showToast('Failed: ' + (d.error || d.detail || 'Unknown error'), 'error');
}
} catch(e) { window.showToast('Error: ' + e.message, 'error'); }
},
// Upload files
triggerUpload() { document.getElementById('gitea-upload-input').click(); },
async doUpload(ev) {
var files = ev.target.files;
if (!files.length) return;
for (var i = 0; i < files.length; i++) {
var form = new FormData();
form.append('file', files[i]);
form.append('message', 'Upload ' + files[i].name + ' via FlowDeck');
try {
var r = await fetch(this.apiBase() + '/upload', {
method: 'POST',
headers: {'X-CSRF-Token': this.getCsrfToken()},
body: form
});
if (!r.ok) { var d = await r.json(); console.error('Upload failed:', d.error); }
} catch(e) { console.error('Upload error:', e); }
}
await this.loadSidebarTree();
ev.target.value = '';
},
async deletePrivate(id) {
if (!confirm('Delete this private page?')) return;
try {
var r = await fetch(this.apiBase() + '/private-pages/' + id, {
method: 'DELETE', headers: {'X-CSRF-Token': this.getCsrfToken ? this.getCsrfToken() : ''}
});
if (r.ok) await this.loadPrivatePages();
} catch { /* volontaire */ }
},
getCsrfToken() {
return document.cookie.split('; ').find(function(c) { return c.startsWith('csrf_token='); })?.split('=')[1] || '';
},
// ── Multi-selection ──
multiSelect: false,
selectedPaths: [],
lastClickedPath: null,
toggleMultiSelect() {
this.multiSelect = !this.multiSelect;
var cbs = document.querySelectorAll('.gitea-checkbox');
var self = this;
cbs.forEach(function(cb) { cb.style.display = self.multiSelect ? '' : 'none'; });
if (!this.multiSelect) {
// Clear selection when leaving multi-select mode
cbs.forEach(function(cb) { cb.checked = false; });
this.selectedPaths = [];
this.lastClickedPath = null;
// Remove selected class
document.querySelectorAll('.sidebar-item.gitea-selected').forEach(function(el) {
el.classList.remove('gitea-selected');
});
}
},
selectItem(path, li, ev) {
if (ev.shiftKey && this.lastClickedPath) {
// Range select
var all = Array.from(document.querySelectorAll('.sidebar-item[data-gitea-path]'));
var startIdx = all.findIndex(function(el) { return el.getAttribute('data-gitea-path') === this.lastClickedPath; }.bind(this));
var endIdx = all.findIndex(function(el) { return el.getAttribute('data-gitea-path') === path; }.bind(this));
if (startIdx >= 0 && endIdx >= 0) {
var lo = Math.min(startIdx, endIdx);
var hi = Math.max(startIdx, endIdx);
this.selectedPaths = [];
for (var i = lo; i <= hi; i++) {
this.selectedPaths.push(all[i].getAttribute('data-gitea-path'));
all[i].classList.add('gitea-selected');
var cb = all[i].querySelector('.gitea-checkbox');
if (cb) cb.checked = true;
}
}
this.multiSelect = true;
this.toggleMultiSelect(); // ensure checkboxes are visible
} else if (ev.ctrlKey || ev.metaKey) {
// Toggle single
var idx = this.selectedPaths.indexOf(path);
if (idx >= 0) {
this.selectedPaths.splice(idx, 1);
li.classList.remove('gitea-selected');
var cb = li.querySelector('.gitea-checkbox');
if (cb) cb.checked = false;
} else {
this.selectedPaths.push(path);
li.classList.add('gitea-selected');
var cb = li.querySelector('.gitea-checkbox');
if (cb) cb.checked = true;
}
this.multiSelect = this.selectedPaths.length > 0;
this.toggleMultiSelect();
} else {
// Normal select — clear multi-selection
this.selectedPaths = [path];
this.lastClickedPath = path;
document.querySelectorAll('.sidebar-item.gitea-selected').forEach(function(el) {
el.classList.remove('gitea-selected');
});
li.classList.add('gitea-selected', 'active');
var cb = li.querySelector('.gitea-checkbox');
if (cb) cb.checked = true;
}
},
isSelected(path) {
return this.selectedPaths.indexOf(path) >= 0;
},
// ── Inline rename ──
startInlineRename(nameSpan, path, li) {
var originalName = nameSpan.textContent;
var input = document.createElement('input');
input.type = 'text';
input.value = originalName;
input.className = 'inline-rename-input';
input.style.cssText = 'flex:1;min-width:0;padding:2px 6px;background:var(--bg-tertiary);border:1px solid var(--accent);border-radius:4px;color:var(--text);font-size:13px;outline:none;';
nameSpan.replaceWith(input);
input.focus();
input.select();
var self = this;
var finish = function() { self.finishInlineRename(input, originalName, path, li); };
var cancel = function() {
var span = document.createElement('span');
span.className = 'page-name';
span.textContent = originalName;
input.replaceWith(span);
};
input.addEventListener('blur', finish);
input.addEventListener('keydown', function(e) {
if (e.key === 'Enter') finish();
if (e.key === 'Escape') { e.stopPropagation(); cancel(); }
});
},
finishInlineRename(input, originalName, path, li) {
if (input._renaming) return; // guard against double-fire
input._renaming = true;
var newName = input.value.trim();
if (!newName || newName === originalName) {
var span = document.createElement('span');
span.className = 'page-name';
span.textContent = originalName;
input.replaceWith(span);
return;
}
var self = this;
// Construct new path by replacing the last segment
var parts = path.split('/');
parts.pop();
var newPath = (parts.length > 0 ? parts.join('/') + '/' : '') + newName;
fetch(this.apiBase() + '/rename', {
method: 'POST',
headers: {'Content-Type': 'application/json', 'X-CSRF-Token': this.getCsrfToken()},
body: JSON.stringify({old_path: path, new_path: newPath, new_name: newName})
}).then(function(r) {
if (r.ok) {
self.refreshTree();
} else {
var span = document.createElement('span');
span.className = 'page-name';
span.textContent = originalName;
input.replaceWith(span);
window.showToast('Rename failed', 'error');
}
}).catch(function() {
var span = document.createElement('span');
span.className = 'page-name';
span.textContent = originalName;
input.replaceWith(span);
});
},
};
});
};
if (window.Alpine) {
_regGiteaWs();
fdRefreshXData('giteaWorkspace');
} else {
document.addEventListener('alpine:init', _regGiteaWs);
}