1
0
Files
medical-notes/stroma/file/templates/file/explorer.html
Johan Dahlin 50366b9b9c
All checks were successful
Deploy Quartz site to GitHub Pages / build (push) Successful in 2m29s
vault backup: 2025-12-26 02:09:22
2025-12-26 02:09:22 +01:00

342 lines
12 KiB
HTML
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Stroma File Explorer</title>
{% load static %}
<script src="https://cdn.tailwindcss.com"></script>
<link rel="stylesheet" href="{% static 'file/editor_dist/assets/editor.css' %}">
<style>
body {
margin: 0;
height: 100vh;
overflow: hidden;
}
.folder-icon::before {
content: '';
display: inline-block;
margin-right: 0.5rem;
font-size: 1.25rem;
font-weight: 600;
transition: transform 0.2s ease;
}
.folder-icon.open::before {
transform: rotate(90deg);
}
.tree-item {
cursor: pointer;
padding: 0.375rem 0.5rem;
border-radius: 0.25rem;
transition: background-color 0.15s;
}
.tree-item:hover {
background-color: rgba(0, 0, 0, 0.05);
}
.tree-item.selected {
background-color: rgba(59, 130, 246, 0.1);
}
.tree-children {
padding-left: 1rem;
display: none;
}
.tree-children.open {
display: block;
}
/* Editor Styles */
.ProseMirror {
outline: none;
padding: 1rem 2rem;
min-height: 100%;
}
.ProseMirror p.is-editor-empty:first-child::before {
color: #adb5bd;
content: attr(data-placeholder);
float: left;
height: 0;
pointer-events: none;
}
/* Bubble Menu Styles */
.bubble-menu {
background-color: #fff;
border: 1px solid #e2e8f0;
border-radius: 0.5rem;
box-shadow: 0 4px 6px -1px rgba(0, 0, 0, 0.1);
display: flex;
padding: 0.25rem;
gap: 0.25rem;
}
.bubble-menu button {
border: none;
background: none;
padding: 0.25rem 0.5rem;
border-radius: 0.25rem;
cursor: pointer;
color: #4a5568;
}
.bubble-menu button:hover,
.bubble-menu button.is-active {
background-color: #edf2f7;
color: #2d3748;
}
</style>
</head>
<body class="bg-gray-50">
<div class="flex h-full">
<!-- Sidebar -->
<div class="w-64 bg-white border-r border-gray-200 overflow-y-auto">
<div class="p-4 border-b border-gray-200 flex justify-between items-center">
<h2 class="text-lg font-semibold text-gray-800">Explorer</h2>
<a href="/upload/" class="text-blue-500 text-xs hover:underline">Upload</a>
</div>
<div class="p-2" id="file-tree">
<div class="p-4 text-gray-400 text-sm">Loading structure...</div>
</div>
</div>
<!-- Content Area -->
<div class="flex-1 flex flex-col bg-white">
<!-- Empty State -->
<div id="empty-state" class="flex flex-col items-center justify-center h-full text-gray-400">
<svg class="w-16 h-16 mb-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2"
d="M9 12h6m-6 4h6m2 5H7a2 2 0 01-2-2V5a2 2 0 012-2h5.586a1 1 0 01.707.293l5.414 5.414a1 1 0 01.293.707V19a2 2 0 01-2 2z">
</path>
</svg>
<p>Select a file to view or edit</p>
</div>
<!-- Markdown Editor -->
<div id="markdown-container" class="flex-1 flex-col hidden">
<!-- Header -->
<div class="bg-white border-b border-gray-200 px-4 py-3 flex justify-between items-center shrink-0 z-10">
<h3 id="markdown-title" class="text-sm font-medium text-gray-700 truncate mr-4"></h3>
<button id="save-btn"
class="bg-blue-600 hover:bg-blue-700 text-white text-xs font-semibold py-1.5 px-4 rounded transition">
Save Changes
</button>
</div>
<!-- Bubble Menu -->
<div id="bubble-menu" class="bubble-menu hidden">
<button id="btn-bold" class="font-bold">B</button>
<button id="btn-italic" class="italic">i</button>
<button id="btn-strike" class="line-through">S</button>
<button id="btn-code" class="font-mono">&lt;&gt;</button>
</div>
<!-- Editor Container -->
<div class="flex-1 overflow-auto relative cursor-text" id="editor-container">
<div id="editor" class="prose prose-sm sm:prose lg:prose-lg max-w-none h-full"></div>
</div>
</div>
<!-- PDF Viewer -->
<div id="pdf-container" class="flex-1 hidden">
<iframe id="pdf-iframe" class="w-full h-full border-none"></iframe>
</div>
</div>
</div>
<script>
const fileTree = document.getElementById('file-tree');
const emptyState = document.getElementById('empty-state');
const markdownContainer = document.getElementById('markdown-container');
const pdfContainer = document.getElementById('pdf-container');
const pdfIframe = document.getElementById('pdf-iframe');
const markdownTitle = document.getElementById('markdown-title');
let selectedFileId = null;
let selectedElement = null;
let currentFileType = null;
function getCookie(name) {
let cookieValue = null;
if (document.cookie && document.cookie !== '') {
const cookies = document.cookie.split(';');
for (let i = 0; i < cookies.length; i++) {
const cookie = cookies[i].trim();
if (cookie.substring(0, name.length + 1) === (name + '=')) {
cookieValue = decodeURIComponent(cookie.substring(name.length + 1));
break;
}
}
}
return cookieValue;
}
async function loadTree() {
try {
const response = await fetch('{% url "file:tree_api" %}');
const structure = await response.json();
fileTree.innerHTML = '';
structure.forEach(item => createTreeItem(item, fileTree));
} catch (err) {
fileTree.innerHTML = '<div class="p-4 text-red-500 text-sm">Failed to load structure</div>';
}
}
function createTreeItem(item, parentElement) {
const itemDiv = document.createElement('div');
if (item.type === 'folder') {
const folderDiv = document.createElement('div');
folderDiv.className = 'tree-item folder-icon flex items-center';
folderDiv.innerHTML = `<span class="truncate">${item.name}</span>`;
const childrenDiv = document.createElement('div');
childrenDiv.className = 'tree-children';
folderDiv.addEventListener('click', (e) => {
e.stopPropagation();
folderDiv.classList.toggle('open');
childrenDiv.classList.toggle('open');
});
itemDiv.appendChild(folderDiv);
itemDiv.appendChild(childrenDiv);
item.children.forEach(child => createTreeItem(child, childrenDiv));
} else {
const fileDiv = document.createElement('div');
fileDiv.className = 'tree-item truncate';
fileDiv.textContent = item.name;
fileDiv.dataset.fileId = item.id;
fileDiv.dataset.filePath = item.path;
fileDiv.dataset.fileName = item.name;
fileDiv.dataset.mimeType = item.mime_type;
fileDiv.addEventListener('click', async (e) => {
e.stopPropagation();
if (selectedElement) selectedElement.classList.remove('selected');
fileDiv.classList.add('selected');
selectedElement = fileDiv;
loadFile(item.id, item.name, item.mime_type, item.path);
});
itemDiv.appendChild(fileDiv);
}
parentElement.appendChild(itemDiv);
}
async function loadFile(id, name, mimeType, path) {
selectedFileId = id;
// Update URL hash with the file path, replacing spaces with underscores
const hashPath = path.replace(/ /g, '_');
window.location.hash = `#${hashPath}`;
// Hide all containers
emptyState.classList.add('hidden');
markdownContainer.classList.add('hidden');
pdfContainer.classList.add('hidden');
if (mimeType === 'application/pdf') {
// Show PDF viewer
currentFileType = 'pdf';
pdfIframe.src = `{% url "file:pdf_viewer" 0 %}`.replace('0', id);
pdfContainer.classList.remove('hidden');
} else {
// Show markdown editor and load content
currentFileType = 'markdown';
markdownTitle.textContent = name;
markdownContainer.classList.remove('hidden');
// Set current file ID for editor
if (window.setEditorFileId) {
window.setEditorFileId(id);
}
// Load content via API
try {
const response = await fetch(`{% url "file:get_content" 0 %}`.replace('0', id));
const data = await response.json();
// Update editor content if editor is initialized
if (window.editorInstance) {
window.editorInstance.commands.setContent(data.content || '');
}
} catch (err) {
console.error('Failed to load file content:', err);
}
}
}
function loadFileFromHash() {
const hash = window.location.hash.substring(1); // Remove #
if (hash) {
// Convert underscores back to spaces to match the actual file path
const filePath = hash.replace(/_/g, ' ');
// Find the file in the tree and load it
const fileElement = findFileElement(filePath);
if (fileElement) {
// Expand all parent folders
expandParentFolders(fileElement);
// Click the file to load it
fileElement.click();
}
}
}
function findFileElement(filePath) {
// Search through all file tree items
const allItems = fileTree.querySelectorAll('.tree-item');
for (let item of allItems) {
// Skip folder items
if (item.classList.contains('folder-icon')) continue;
// Check if this item has the matching file path
if (item.dataset.filePath === filePath) {
return item;
}
}
return null;
}
function expandParentFolders(element) {
let parent = element.parentElement;
while (parent && parent !== fileTree) {
if (parent.classList.contains('tree-children')) {
parent.classList.add('open');
// Find the folder div and add open class to it
const folderDiv = parent.previousElementSibling;
if (folderDiv && folderDiv.classList.contains('folder-icon')) {
folderDiv.classList.add('open');
}
}
parent = parent.parentElement;
}
}
// Initialize
loadTree().then(() => {
// After tree is loaded, check for hash and load file if present
loadFileFromHash();
});
// Listen for hash changes (e.g., browser back/forward)
window.addEventListener('hashchange', loadFileFromHash);
</script>
<!-- Load editor module after DOM is ready -->
<script type="module" src="{% static 'file/editor_dist/assets/editor.js' %}"></script>
</body>
</html>