diff --git a/app/db.py b/app/db.py index 96ef9c0..483e998 100644 --- a/app/db.py +++ b/app/db.py @@ -296,6 +296,14 @@ def init_db(): conn.execute("ALTER TABLE pages ADD COLUMN deleted_at TEXT") except sqlite3.OperationalError: pass + try: + conn.execute("ALTER TABLE pages ADD COLUMN share_mode TEXT DEFAULT 'private'") + except sqlite3.OperationalError: + pass + try: + conn.execute("ALTER TABLE pages ADD COLUMN published INTEGER DEFAULT 0") + except sqlite3.OperationalError: + pass conn.commit() diff --git a/app/routers/board.py b/app/routers/board.py index 36d7f41..81071d4 100644 --- a/app/routers/board.py +++ b/app/routers/board.py @@ -383,6 +383,24 @@ async def remove_favorite(request: Request, page_id: int): conn.commit() return {"status": "removed", "page_id": page_id} +# ═══════════ Share API ═══════════ + +@router.post("/api/share/{page_id:int}") +async def update_share(request: Request, page_id: int): + """Save share settings for a page.""" + import json + body = await request.json() + mode = body.get("mode", "private") + published = body.get("published", False) + with get_conn() as conn: + conn.execute( + "UPDATE pages SET share_mode=?, published=? WHERE id=?", + (mode, 1 if published else 0, page_id), + ) + conn.commit() + return {"status": "ok", "share_mode": mode, "published": published} + + # ═══════════ Trash (must be before /{owner}/{repo} catch-all) ═══════════ @router.get("/api/trash") @@ -732,7 +750,9 @@ async def view_page(request: Request, page_id: int): fav = conn.execute( "SELECT id FROM favorites WHERE user_id=? AND page_id=?", (uid, page_id) ).fetchone() - ctx = {**sidebar, "page": page, "page_favorited": fav is not None} + ctx = {**sidebar, "page": page, "page_favorited": fav is not None, + "page_share_mode": page.get("share_mode", "private"), + "page_published": bool(page.get("published", 0))} template = env.get_template("page_editor.html") return template.render(**ctx) diff --git a/app/templates/page_editor.html b/app/templates/page_editor.html index f2b7d62..40eb8f9 100644 --- a/app/templates/page_editor.html +++ b/app/templates/page_editor.html @@ -334,8 +334,9 @@ function editor(pid,title,fmt,raw,favorited){ pid,pageTitle:title,blocks:[],dirty:false,saving:false,lastSaved:'',st:null, fmt:{open:false,top:0,left:0,idx:-1}, shareOpen:false,moreOpen:false,gsMoreOpen:false,accessMenuOpen:false, - shareTab:'share',favorited:!!favorited,pagePublished:false, - generalAccess:'invited',publicPerm:'viewer', + shareTab:'share',favorited:!!favorited, + pagePublished:{{ page_published | tojson }}, + generalAccess:'{{ page_share_mode }}',publicPerm:'viewer', pubAllowEdit:false,pubAllowComments:true, pageUrl:window.location.href,publishedUrl:'', inviteEmail:'',invitePermission:'editor',accessList:[], @@ -374,15 +375,30 @@ function editor(pid,title,fmt,raw,favorited){ const m=this.favorited?'DELETE':'POST'; fetch(`/board/api/favorites/${this.pid}`,{method:m,headers:{'X-CSRF-Token':csrf?csrf[1]:''}}) .then(r=>r.json()).then(()=>{this.favorited=!this.favorited;}).catch(()=>{}); + togglePublish(){this.pagePublished=!this.pagePublished;this.saveShare();}, + saveShare(){ + const csrf=document.cookie.match(/csrf_token=([^;]+)/); + fetch(`/board/api/share/${this.pid}`,{method:'POST',headers:{'Content-Type':'application/json','X-CSRF-Token':csrf?csrf[1]:''},body:JSON.stringify({mode:this.generalAccess,published:this.pagePublished})}).catch(()=>{}); }, copyPageLink(){ - navigator.clipboard.writeText(this.pageUrl).then(()=>{ - this.toastMsg='Link copied to clipboard'; - this.toastVisible=true; - setTimeout(()=>{this.toastVisible=false;},2500); - }).catch(()=>{}); + const url = this.pageUrl; + const fallback = () => { + const ta = document.createElement('textarea'); + ta.value = url; ta.style.position='fixed'; ta.style.opacity='0'; + document.body.appendChild(ta); ta.select(); + document.execCommand('copy'); document.body.removeChild(ta); + }; + try { + navigator.clipboard.writeText(url).then(() => { + this.showToast('Link copied to clipboard'); + }).catch(() => { fallback(); this.showToast('Link copied to clipboard'); }); + } catch(e) { fallback(); this.showToast('Link copied to clipboard'); } + }, + showToast(msg){ + this.toastMsg = msg; this.toastVisible = true; + clearTimeout(this._toastTimer); + this._toastTimer = setTimeout(() => { this.toastVisible = false; }, 2500); }, - togglePublish(){this.pagePublished=!this.pagePublished;}, invitePerson(){if(!this.inviteEmail.trim())return;this.accessList.push({email:this.inviteEmail,permission:this.invitePermission,permOpen:false});this.inviteEmail='';}, removeAccess(email){this.accessList=this.accessList.filter(a=>a.email!==email);}, exportPage(){this.moreOpen=false;},