feat: tab navigation in modal (Index/Transkript/Zusammenfassung)
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
+5
-4
@@ -136,7 +136,7 @@ async def get_transcripts(user: dict = Depends(current_user)):
|
|||||||
return list_transcripts(user_dir)
|
return list_transcripts(user_dir)
|
||||||
|
|
||||||
|
|
||||||
@router.get("/transcripts/{filename}")
|
@router.get("/transcripts/{filename:path}")
|
||||||
async def get_transcript(filename: str, user: dict = Depends(current_user)):
|
async def get_transcript(filename: str, user: dict = Depends(current_user)):
|
||||||
from fastapi.responses import PlainTextResponse
|
from fastapi.responses import PlainTextResponse
|
||||||
user_dir = os.path.join(user["output_dir"], user["username"])
|
user_dir = os.path.join(user["output_dir"], user["username"])
|
||||||
@@ -146,7 +146,7 @@ async def get_transcript(filename: str, user: dict = Depends(current_user)):
|
|||||||
return PlainTextResponse(content)
|
return PlainTextResponse(content)
|
||||||
|
|
||||||
|
|
||||||
@router.post("/transcripts/{filename}/reprocess")
|
@router.post("/transcripts/{filename:path}/reprocess")
|
||||||
async def reprocess_transcript(filename: str, body: dict, user: dict = Depends(current_user)):
|
async def reprocess_transcript(filename: str, body: dict, user: dict = Depends(current_user)):
|
||||||
from output import read_transcript
|
from output import read_transcript
|
||||||
from fastapi.responses import PlainTextResponse
|
from fastapi.responses import PlainTextResponse
|
||||||
@@ -175,10 +175,11 @@ async def reprocess_transcript(filename: str, body: dict, user: dict = Depends(c
|
|||||||
return PlainTextResponse(refined)
|
return PlainTextResponse(refined)
|
||||||
|
|
||||||
|
|
||||||
@router.delete("/transcripts/{filename}")
|
@router.delete("/transcripts/{filename:path}")
|
||||||
async def delete_transcript(filename: str, user: dict = Depends(current_user)):
|
async def delete_transcript(filename: str, user: dict = Depends(current_user)):
|
||||||
user_dir = os.path.join(user["output_dir"], user["username"])
|
user_dir = os.path.join(user["output_dir"], user["username"])
|
||||||
if os.path.basename(filename) != filename or not filename.endswith(".md"):
|
parts = filename.split("/")
|
||||||
|
if len(parts) > 2 or any(p in (".", "..") or not p for p in parts) or not filename.endswith(".md"):
|
||||||
raise HTTPException(status_code=404, detail="Nicht gefunden")
|
raise HTTPException(status_code=404, detail="Nicht gefunden")
|
||||||
path = os.path.join(user_dir, filename)
|
path = os.path.join(user_dir, filename)
|
||||||
if not os.path.exists(path):
|
if not os.path.exists(path):
|
||||||
|
|||||||
+40
-8
@@ -12,9 +12,11 @@ const modalObsidianBtn = document.getElementById('modal-obsidian-btn');
|
|||||||
const modalFolderBtn = document.getElementById('modal-folder-btn');
|
const modalFolderBtn = document.getElementById('modal-folder-btn');
|
||||||
const modalOpenBtn = document.getElementById('modal-open-btn');
|
const modalOpenBtn = document.getElementById('modal-open-btn');
|
||||||
const modalCloseBtn = document.getElementById('modal-close-btn');
|
const modalCloseBtn = document.getElementById('modal-close-btn');
|
||||||
|
const modalTabs = document.getElementById('modal-tabs');
|
||||||
let _modalPath = null;
|
let _modalPath = null;
|
||||||
let _modalPaths = null;
|
let _modalPaths = null;
|
||||||
let _modalFilename = null;
|
let _modalFilename = null;
|
||||||
|
let _modalRelated = null;
|
||||||
|
|
||||||
const speakerCard = document.getElementById('speaker-card');
|
const speakerCard = document.getElementById('speaker-card');
|
||||||
const speakerRows = document.getElementById('speaker-rows');
|
const speakerRows = document.getElementById('speaker-rows');
|
||||||
@@ -54,18 +56,47 @@ logoutBtn.addEventListener('click', () => {
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
function openModal(filename, path, paths) {
|
function _loadModalContent(filename, activeTab) {
|
||||||
|
modalBody.innerHTML = '';
|
||||||
|
apiFetch(`/transcripts/${filename.split('/').map(encodeURIComponent).join('/')}`)
|
||||||
|
.then(r => r.text())
|
||||||
|
.then(md => { modalBody.innerHTML = DOMPurify.sanitize(marked.parse(md)); });
|
||||||
|
// update active tab
|
||||||
|
modalTabs.querySelectorAll('.modal-tab').forEach(t => {
|
||||||
|
t.classList.toggle('active', t.dataset.file === filename);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function openModal(filename, path, paths, related) {
|
||||||
_modalPath = path;
|
_modalPath = path;
|
||||||
_modalPaths = paths || null;
|
_modalPaths = paths || null;
|
||||||
_modalFilename = filename;
|
_modalFilename = filename;
|
||||||
modalTitle.textContent = filename.replace(/\.md$/, '').replace(/^\d{4}-\d{2}-\d{2}-\d{4}-/, '');
|
_modalRelated = related || null;
|
||||||
modalBody.innerHTML = '';
|
modalTitle.textContent = filename.replace(/\.md$/, '').replace(/^\d{4}-\d{2}-\d{2}-\d{4}-/, '').replace(/-index$/, '');
|
||||||
modal.classList.remove('hidden');
|
modal.classList.remove('hidden');
|
||||||
apiFetch(`/transcripts/${encodeURIComponent(filename)}`)
|
|
||||||
.then(r => r.text())
|
// Build tabs if there are related files
|
||||||
.then(md => {
|
modalTabs.innerHTML = '';
|
||||||
modalBody.innerHTML = DOMPurify.sanitize(marked.parse(md));
|
if (related && (related.transkript || related.zusammenfassung)) {
|
||||||
|
modalTabs.style.display = 'flex';
|
||||||
|
const tabDefs = [
|
||||||
|
{ label: 'Index', file: filename },
|
||||||
|
{ label: 'Transkript', file: related.transkript },
|
||||||
|
{ label: 'Zusammenfassung', file: related.zusammenfassung },
|
||||||
|
].filter(t => t.file);
|
||||||
|
tabDefs.forEach(({ label, file }) => {
|
||||||
|
const btn = document.createElement('button');
|
||||||
|
btn.className = 'modal-tab';
|
||||||
|
btn.textContent = label;
|
||||||
|
btn.dataset.file = file;
|
||||||
|
btn.addEventListener('click', () => _loadModalContent(file, file));
|
||||||
|
modalTabs.appendChild(btn);
|
||||||
});
|
});
|
||||||
|
} else {
|
||||||
|
modalTabs.style.display = 'none';
|
||||||
|
}
|
||||||
|
|
||||||
|
_loadModalContent(filename, filename);
|
||||||
}
|
}
|
||||||
|
|
||||||
function closeModal() {
|
function closeModal() {
|
||||||
@@ -73,6 +104,7 @@ function closeModal() {
|
|||||||
_modalPath = null;
|
_modalPath = null;
|
||||||
_modalPaths = null;
|
_modalPaths = null;
|
||||||
_modalFilename = null;
|
_modalFilename = null;
|
||||||
|
_modalRelated = null;
|
||||||
}
|
}
|
||||||
|
|
||||||
modalCloseBtn.addEventListener('click', closeModal);
|
modalCloseBtn.addEventListener('click', closeModal);
|
||||||
@@ -239,7 +271,7 @@ async function loadTranscripts() {
|
|||||||
meta.className = 'meta';
|
meta.className = 'meta';
|
||||||
meta.textContent = `${Math.round(t.size / 1024 * 10) / 10} KB`;
|
meta.textContent = `${Math.round(t.size / 1024 * 10) / 10} KB`;
|
||||||
|
|
||||||
div.addEventListener('click', () => openModal(t.filename, t.path));
|
div.addEventListener('click', () => openModal(t.filename, t.path, null, t.related || null));
|
||||||
|
|
||||||
const reprocessBtn = document.createElement('button');
|
const reprocessBtn = document.createElement('button');
|
||||||
reprocessBtn.className = 'del-btn';
|
reprocessBtn.className = 'del-btn';
|
||||||
|
|||||||
@@ -163,6 +163,10 @@
|
|||||||
.modal-body pre { background: var(--surface2); padding: 12px; border-radius: 6px; overflow-x: auto; margin: 0 0 0.8em; }
|
.modal-body pre { background: var(--surface2); padding: 12px; border-radius: 6px; overflow-x: auto; margin: 0 0 0.8em; }
|
||||||
.modal-body pre code { background: none; padding: 0; }
|
.modal-body pre code { background: none; padding: 0; }
|
||||||
.modal-body hr { border: none; border-top: 1px solid var(--border); margin: 1em 0; }
|
.modal-body hr { border: none; border-top: 1px solid var(--border); margin: 1em 0; }
|
||||||
|
.modal-tabs { display: flex; gap: 4px; padding: 10px 18px 0; border-bottom: 1px solid var(--border); flex-shrink: 0; }
|
||||||
|
.modal-tab { background: none; border: 1px solid transparent; border-bottom: none; border-radius: 6px 6px 0 0; padding: 5px 12px; font-size: 0.78rem; font-family: inherit; color: var(--muted); cursor: pointer; transition: color 0.15s, border-color 0.15s; margin-bottom: -1px; }
|
||||||
|
.modal-tab:hover { color: var(--text); }
|
||||||
|
.modal-tab.active { color: var(--text); border-color: var(--border); background: var(--surface); }
|
||||||
.del-btn {
|
.del-btn {
|
||||||
background: none; border: none; color: var(--muted); cursor: pointer;
|
background: none; border: none; color: var(--muted); cursor: pointer;
|
||||||
padding: 4px; border-radius: 4px; display: flex; align-items: center;
|
padding: 4px; border-radius: 4px; display: flex; align-items: center;
|
||||||
@@ -274,6 +278,7 @@
|
|||||||
<button id="modal-close-btn" class="modal-btn" title="Schließen">✕</button>
|
<button id="modal-close-btn" class="modal-btn" title="Schließen">✕</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
<div id="modal-tabs" class="modal-tabs" style="display:none"></div>
|
||||||
<div id="modal-body" class="modal-body"></div>
|
<div id="modal-body" class="modal-body"></div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -36,10 +36,15 @@ def save_transcript(
|
|||||||
|
|
||||||
|
|
||||||
def read_transcript(output_dir: str, filename: str) -> str | None:
|
def read_transcript(output_dir: str, filename: str) -> str | None:
|
||||||
"""Return file content if filename is a plain .md file inside output_dir."""
|
"""Return file content for a .md file inside output_dir (flat or one level deep)."""
|
||||||
if os.path.basename(filename) != filename or not filename.endswith(".md"):
|
if not filename.endswith(".md"):
|
||||||
|
return None
|
||||||
|
parts = filename.split("/")
|
||||||
|
if len(parts) > 2 or any(p in (".", "..") or not p for p in parts):
|
||||||
return None
|
return None
|
||||||
path = os.path.join(output_dir, filename)
|
path = os.path.join(output_dir, filename)
|
||||||
|
if not os.path.abspath(path).startswith(os.path.abspath(output_dir) + os.sep):
|
||||||
|
return None
|
||||||
if not os.path.exists(path):
|
if not os.path.exists(path):
|
||||||
return None
|
return None
|
||||||
with open(path, encoding="utf-8") as f:
|
with open(path, encoding="utf-8") as f:
|
||||||
@@ -57,7 +62,17 @@ def list_transcripts(output_dir: str, limit: int = 20) -> list[dict]:
|
|||||||
for f in files:
|
for f in files:
|
||||||
full = os.path.join(output_dir, f)
|
full = os.path.join(output_dir, f)
|
||||||
stat = os.stat(full)
|
stat = os.stat(full)
|
||||||
result.append({"filename": f, "path": full, "size": stat.st_size, "mtime": stat.st_mtime})
|
item: dict = {"filename": f, "path": full, "size": stat.st_size, "mtime": stat.st_mtime}
|
||||||
|
if f.endswith("-index.md"):
|
||||||
|
base = f[: -len("-index.md")]
|
||||||
|
related: dict[str, str] = {}
|
||||||
|
for suffix, key in [("-transkript.md", "transkript"), ("-zusammenfassung.md", "zusammenfassung")]:
|
||||||
|
rel_filename = f"{base}/{base}{suffix}"
|
||||||
|
if os.path.exists(os.path.join(output_dir, rel_filename)):
|
||||||
|
related[key] = rel_filename
|
||||||
|
if related:
|
||||||
|
item["related"] = related
|
||||||
|
result.append(item)
|
||||||
return result
|
return result
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user