All checks were successful
Deploy Quartz site to GitHub Pages / build (push) Successful in 2m29s
342 lines
12 KiB
HTML
342 lines
12 KiB
HTML
<!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"><></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> |