@@ -12,7 +12,7 @@
< span class = "page-title-topbar" x-text = "owner + '/' + repo" > < / span >
< / div >
< div class = "topbar-right" >
< button class = "page-action-btn" @ click = "refresh()" title = "Refresh" > 🔄< / button >
< button class = "page-action-btn" @ click = "refreshTree ()" title = "Refresh" > 🔄< / button >
< button class = "page-action-btn" onclick = "window.location='/accounts/settings'" title = "Settings" > ⚙< / button >
< / div >
< / header >
@@ -20,16 +20,7 @@
{% block content %}
< style >
.gw-layout {display:flex;height:calc(100vh - 48px);}
.gw-tree{width:280px;min-width:280px;background:var(--bg-sidebar);border-right:1px solid var(--border);overflow-y:auto;padding:12px 0;}
.gw-tree-item{display:flex;align-items:center;gap:6px;padding:4px 16px;font-size:13px;color:var(--text-secondary);cursor:pointer;transition:background 100ms;white-space:nowrap;overflow:hidden;text-overflow:ellipsis;}
.gw-tree-item:hover{background:var(--bg-tertiary);color:var(--text);}
.gw-tree-item.selected{background:var(--accent);color:#fff;}
.gw-tree-icon{font-size:14px;flex-shrink:0;}
.gw-tree-indent{padding-left:32px !important;}
.gw-tree-indent2{padding-left:48px !important;}
.gw-tree-section-title{font-size:11px;font-weight:600;color:var(--text-dim);text-transform:uppercase;letter-spacing:.5px;padding:8px 16px 4px;margin-top:8px;border-top:1px solid var(--border);}
.gw-main{flex:1;display:flex;flex-direction:column;overflow:hidden;}
.gw-main {display:flex;flex-direction:column; height:calc(100vh - 48px);overflow:hidden; }
.gw-content{padding:24px;overflow-y:auto;flex:1;}
.gw-content h3{font-size:16px;margin-bottom:12px;color:var(--text-dim);}
.gw-content pre{background:var(--bg-tertiary);border:1px solid var(--border);border-radius:8px;padding:16px;overflow-x:auto;font-size:13px;line-height:1.5;white-space:pre-wrap;word-break:break-word;max-height:70vh;overflow-y:auto;}
@@ -37,93 +28,26 @@
.gw-content .empty-state h2{font-size:18px;margin-bottom:8px;color:var(--text);}
.gw-content textarea{width:100%;min-height:50vh;background:var(--bg-tertiary);border:1px solid var(--border);border-radius:8px;color:var(--text);font-size:13px;padding:12px;font-family:monospace;resize:vertical;outline:none;}
.gw-content textarea:focus{border-color:var(--accent);}
.gw-file-toolbar{display:flex;align-items:center;gap:8px;padding:8px 24px;background:var(--bg-secondary);border-bottom:1px solid var(--border);font-size:13px;color:var(--text-dim);}
.gw-file-toolbar strong{color:var(--text);margin-right:12px;}
.gw-commit-bar{display:flex;align-items:center;gap:8px;padding:12px 24px;background:var(--bg-secondary);border-top:1px solid var(--border);flex-wrap:wrap;}
.gw-commit-bar input{flex:1;min-width:180px;padding:8px 12px;background:var(--bg-tertiary);border:1px solid var(--border);border-radius:8px;color:var(--text);font-size:13px;outline:none;}
.gw-commit-bar input:focus{border-color:var(--accent);}
.gw-file-toolbar{display:flex;align-items:center;gap:8px;padding:8px 24px;background:var(--bg-secondary);border-bottom:1px solid var(--border);font-size:13px;color:var(--text-dim);}
.gw-file-toolbar strong{color:var(--text);margin-right:12px;}
.btn-primary{padding:8px 16px;border-radius:8px;font-size:13px;cursor:pointer;border:none;font-weight:500;background:var(--accent);color:#fff;}
.btn-primary:hover{background:var(--accent-hover);}
.btn-danger{background:rgba(255,80,80,.15);color:var(--danger);}
.btn-danger{background:rgba(255,80,80,.15);color:var(--danger);border:none;padding:8px 16px;border-radius:8px;font-size:13px;cursor:pointer;font-weight:500; }
.btn-danger:hover{background:rgba(255,80,80,.25);}
.btn{border:none;padding:8px 16px;border-radius:8px;font-size:13px;cursor:pointer;font-weight:500;border:1px solid var(--border);background:transparent;color:var(--text-dim);}
.btn:hover{background:var(--bg-tertiary);color:var(--text);}
.loading-spinner{text-align:center;padding:20px;color:var(--text-dim);}
< / style >
< div class = "gw-layout " x-data = "giteaWorkspace()" >
<!-- Tree sidebar -->
< div class = "gw-tree" >
< div class = "gw-tree-section-title" > 📁 GITEA REPO< / div >
<!-- Loading -->
< div class = "loading-spinner" x-show = "loading" > Loading…< / div >
<!-- Tree items (recursive rendering) -->
< template x-for = "item in treeItems" :key = "item.path" >
< div >
< div class = "gw-tree-item"
:class="{selected: item.path === selectedPath}"
:style="'padding-left:' + (16 + item.depth * 16) + 'px'"
@click="item.type==='folder' ? toggleFolder(item) : openFile(item)">
< span class = "gw-tree-icon" x-text = "item.type==='folder' ? (item.expanded ? '📂' : '📁') : '📄'" > < / span >
< span x-text = "item.name" style = "overflow:hidden;text-overflow:ellipsis;" > < / span >
< / div >
<!-- children -->
< template x-if = "item.expanded" >
< template x-for = "child in item.children" :key = "child.path" >
< div class = "gw-tree-item"
:class="{selected: child.path === selectedPath}"
:style="'padding-left:' + (16 + child.depth * 16) + 'px'"
@click="child.type==='folder' ? toggleFolder(child) : openFile(child)">
< span class = "gw-tree-icon" x-text = "child.type==='folder' ? (child.expanded ? '📂' : '📁') : '📄'" > < / span >
< span x-text = "child.name" style = "overflow:hidden;text-overflow:ellipsis;" > < / span >
< / div >
< / template >
< / template >
< / div >
< / template >
<!-- Private section -->
< div class = "gw-tree-section-title" > 🔒 PRIVATE PAGES< / div >
< div class = "gw-tree-item" @ click = "selectedPath='--private--'; showEditor=false; showFile=false;" >
< span class = "gw-tree-icon" > 📝< / span >
< span > My notes< / span >
< / div >
< div class = "gw-tree-item" @ click = "newPrivatePage()" style = "color:var(--accent);" >
< span class = "gw-tree-icon" > ➕ < / span >
< span > New page< / span >
< / div >
< / div >
<!-- Main content -->
< div class = "gw-main" >
<!-- Private pages view -->
< template x-if = "selectedPath==='--private--'" >
< div class = "gw-content" >
< template x-if = "privatePages.length === 0" >
< div class = "empty-state" >
< h2 > 🔒 Private Pages< / h2 >
< p > Create FlowDeck pages attached to this Gitea project. These are stored locally, not in the repo.< / p >
< button class = "btn btn-primary" @ click = "newPrivatePage()" style = "margin-top:12px;" > + New Page< / button >
< / div >
< / template >
< template x-for = "pp in privatePages" :key = "pp.id" >
< div style = "background:var(--bg-secondary);border:1px solid var(--border);border-radius:8px;padding:16px;margin-bottom:8px;cursor:pointer;"
@click="openPrivatePage(pp)">
< div style = "font-weight:500;margin-bottom:4px;" x-text = "pp.title" > < / div >
< div style = "font-size:12px;color:var(--text-dim);" x-text = "pp.updated_at ? 'Updated ' + new Date(pp.updated_at).toLocaleDateString() : ''" > < / div >
< / div >
< / template >
< / div >
< / template >
< div class = "gw-main " x-data = "giteaWorkspace()" >
<!-- File view -->
< template x-if = "showFile && !showEditor" >
< div >
< div class = "gw-file-toolbar" >
< strong x-text = "currentFile?.name || ''" > < / strong >
< span x-text = "formatSize(currentFile?.size || 0 )" > < / span >
< strong x-text = "filePath || ''" > < / strong >
< span x-text = "formatSize(fileSize )" > < / span >
< span style = "flex:1;" > < / span >
< button class = "btn" @ click = "openEditor()" > ✏️ Edit< / button >
< button class = "btn btn-danger" @ click = "deleteCurrentFile()" > 🗑 Delete< / button >
@@ -138,7 +62,7 @@
< template x-if = "showEditor" >
< div >
< div class = "gw-file-toolbar" >
< strong > Editing: < span x-text = "currentFile?.name || 'new file'" > < / span > < / strong >
< strong > Editing: < span x-text = "filePath || 'new file'" > < / span > < / strong >
< span style = "flex:1;" > < / span >
< button class = "btn" @ click = "showEditor=false" > Cancel< / button >
< / div >
@@ -158,16 +82,15 @@
< / template >
<!-- Empty state -->
< template x-if = "!showFile && !showEditor && selectedPath!=='--private--' " >
< template x-if = "!showFile && !showEditor" >
< div class = "gw-content" >
< div class = "empty-state" >
< h2 > 📁 < span x-text = "owner + '/' + repo" > < / span > < / h2 >
< p > Select a file from the tree to view it, or create a private page .< / p >
< p > Select a file from the sidebar to view it.< / p >
< / div >
< / div >
< / template >
< / div >
< / div >
< script >
function giteaWorkspace() {
@@ -177,81 +100,81 @@ function giteaWorkspace() {
return {
owner, repo,
treeItems: [],
selectedPath: '',
currentFile: null,
fileContent: '',
showFile: false,
showEditor: false,
filePath: '',
fileContent: '',
fileSize: 0,
fileSha: '',
editContent: '',
commitMessage: 'Update via FlowDeck',
commitHistory: JSON.parse(localStorage.getItem('fd_commit_msgs') || '[]'),
loading: true,
privatePages: [],
async init() {
await this.loadRoot();
await this.loadPrivatePages() ;
init() {
// Set cookie for sidebar
document.cookie = 'flowdeck_workspace=gitea:' + this.owner + ':' + this.repo + ';path=/;SameSite=Lax' ;
// Listen for sidebar clicks on Gitea items
this.setupSidebarClicks();
},
async refresh () {
this.treeItems = [] ;
this.loading = true;
await this.loadRoot( );
},
async loadRoot() {
this.loading = true ;
try {
const r = await fetch(`/api/gitea/projects/${this.owner}/${this.repo}/tree`);
if (r.ok) {
const d = await r.json();
this.treeItems = (d.tree || []).map(item => ({
...item,
depth: 0,
expanded: false,
children: [],
}));
setupSidebarClicks () {
var self = this ;
document.addEventListener('click', function(e) {
var el = e.target.closest('.sidebar-item[data-gitea-path]' );
if (!el) return;
e.preventDefault();
e.stopPropagation();
var path = el.getAttribute('data-gitea-path') ;
var type = el.getAttribute('data-gitea-type');
if (type === 'folder') {
self.loadSubdir(path, el);
} else {
self.openFile(path, el.getAttribute('data-gitea-sha'));
}
} catch(e) {}
this.loading = false;
});
},
async toggleFolder(item ) {
if (item.expanded) {
item.expanded = false ;
async loadSubdir(path, el ) {
// Check if already loaded
var ul = el.querySelector('ul') ;
if (ul) {
ul.style.display = ul.style.display === 'none' ? '' : 'none';
return;
}
if (item.children.length > 0) {
item.expanded = true;
return;
}
// Lazy load
try {
const r = await fetch(` /api/gitea/projects/${ this.owner}/${ this.repo} /tree?path=${ encodeURIComponent(item. path)}` );
if (r.ok) {
const d = await r.json();
item. children = ( d.tree || []).map(child => ({
...child,
depth: item.depth + 1,
expanded: false,
children: [],
})) ;
}
item.expanded = true ;
var r = await fetch(' /api/gitea/projects/' + this.owner + '/' + this.repo + ' /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.paddingLeft = '20px';
children.forEach(function(child) {
var icon = child.type === 'folder' ? '📁' : '📄' ;
var li = document.createElement('li');
li.className = 'sidebar-item' ;
li.setAttribute('data-gitea-path', child.path);
li.setAttribute('data-gitea-type', child.type === 'folder' ? 'folder' : 'file');
li.setAttribute('data-gitea-sha', child.sha || '');
li.style.cursor = 'pointer';
li.innerHTML = '< span class = "sidebar-icon" > ' + icon + '< / span > ' + child.name;
ul.appendChild(li);
});
el.appendChild(ul);
} catch(e) {}
},
async openFile(item ) {
this.selected Path = item. path;
this.currentFile = item ;
async openFile(path, sha ) {
this.file Path = path;
this.fileSha = sha || '' ;
this.showEditor = false;
this.showFile = true;
try {
const r = await fetch(` /api/gitea/projects/${ this.owner}/${ this.repo} /file?path=${ encodeURIComponent(item. path)}` );
var r = await fetch(' /api/gitea/projects/' + this.owner + '/' + this.repo + ' /file?path=' + encodeURIComponent(path));
if (r.ok) {
const d = await r.json();
var d = await r.json();
this.fileContent = d.content || '';
this.fileSize = d.content ? d.content.length : 0;
} else {
this.fileContent = '[Error loading file]';
}
@@ -266,74 +189,57 @@ function giteaWorkspace() {
},
async saveFile() {
if (!this.currentFile ) return;
const path = this.currentFile.path;
if (!this.filePath ) return;
try {
const r = await fetch(` /api/gitea/projects/${ this.owner}/${ this.repo} /file` , {
var r = await fetch(' /api/gitea/projects/' + this.owner + '/' + this.repo + ' /file' , {
method: 'PUT',
headers: {'Content-Type': 'application/json'},
body: JSON.stringify({
path, content: this.editContent,
message: this.commitMessage,
sha: this.currentFile.sha,
path: this.filePath , content: this.editContent,
message: this.commitMessage, sha: this.fileSha,
})
});
if (r.ok) {
this.fileContent = this.editContent;
this.showEditor = false;
// Save commit message history
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 {
const d = await r.json();
var d = await r.json();
alert('Failed to save: ' + (d.error || 'Unknown error'));
}
} catch(e) {
alert('Network error');
}
} catch(e) { alert('Network error'); }
},
async deleteCurrentFile() {
if (!this.currentFile ) return;
if (!confirm(` Delete ${ this.currentF ile.p ath} from ${ this.owner}/${ this.repo}?` )) return;
if (!this.filePath ) return;
if (!confirm(' Delete ' + this.f ileP ath + ' from ' + this.owner + '/' + this.repo + '?' )) return;
try {
const r = await fetch(` /api/gitea/projects/${ this.owner}/${ this.repo} /file?path=${ encodeURIComponent(this.currentF ile.p ath)} & sha=${ encodeURIComponent(this.currentF ile.s ha)} & message=${ encodeURIComponent('Delete via FlowDeck')}` , {
var r = await fetch(' /api/gitea/projects/' + this.owner + '/' + this.repo + ' /file?path=' + encodeURIComponent(this.f ileP ath) + ' & sha=' + encodeURIComponent(this.f ileS ha) + ' & message=' + encodeURIComponent('Delete via FlowDeck'), {
method: 'DELETE',
});
if (r.ok) {
this.showFile = false;
this.selected Path = '';
await this.refresh();
this.file Path = '';
await this.refreshTree ();
}
} catch(e) {}
},
async refreshTree() {
// Reload page to refresh sidebar tree
window.location.reload();
},
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() {
// Pages stored in FlowDeck DB, linked to this Gitea project
// For now, store them as local workspace pages with gitea_owner/gitea_repo
// TODO: Create a dedicated API endpoint for gitea private pages
},
newPrivatePage() {
// TODO: Create a new FlowDeck page linked to this Gitea project
alert('Private pages will be available in a future update. Stay tuned!');
},
openPrivatePage(pp) {
// TODO: Open FlowDeck page editor
},
};
}
< / script >