This commit is contained in:
nahakubuilde
2025-08-25 08:48:52 +01:00
commit bfa0eaf68a
26 changed files with 4388 additions and 0 deletions

405
web/templates/base.html Normal file
View File

@@ -0,0 +1,405 @@
<!DOCTYPE html>
<html lang="en" class="dark">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>{{block "title" .}}{{.app_name}}{{end}}</title>
<script src="https://cdn.tailwindcss.com"></script>
<script>
tailwind.config = {
darkMode: 'class',
theme: {
extend: {
colors: {
'obsidian': {
'50': '#f8fafc',
'100': '#f1f5f9',
'200': '#e2e8f0',
'300': '#cbd5e1',
'400': '#94a3b8',
'500': '#64748b',
'600': '#475569',
'700': '#334155',
'800': '#1e293b',
'900': '#0f172a',
}
}
}
}
}
</script>
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/highlight.js/11.9.0/styles/github-dark.min.css">
<script src="https://cdnjs.cloudflare.com/ajax/libs/highlight.js/11.9.0/highlight.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/highlight.js/11.9.0/languages/go.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/highlight.js/11.9.0/languages/python.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/highlight.js/11.9.0/languages/javascript.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/highlight.js/11.9.0/languages/markdown.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/highlight.js/11.9.0/languages/json.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/highlight.js/11.9.0/languages/yaml.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/highlight.js/11.9.0/languages/xml.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/highlight.js/11.9.0/languages/sql.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/highlight.js/11.9.0/languages/bash.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/highlight.js/11.9.0/languages/css.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.4.0/js/all.min.js"></script>
<style>
/* Custom scrollbar for dark theme */
::-webkit-scrollbar {
width: 8px;
height: 8px;
}
::-webkit-scrollbar-track {
background: #1e293b;
}
::-webkit-scrollbar-thumb {
background: #475569;
border-radius: 4px;
}
::-webkit-scrollbar-thumb:hover {
background: #64748b;
}
/* Content markdown styles */
.prose-dark {
color: #d1d5db;
}
.prose-dark h1, .prose-dark h2, .prose-dark h3, .prose-dark h4, .prose-dark h5, .prose-dark h6 {
color: white;
}
.prose-dark a {
color: #60a5fa;
}
.prose-dark a:hover {
color: #93c5fd;
}
.prose-dark code {
background-color: #1f2937;
color: #10b981;
padding: 0.125rem 0.25rem;
border-radius: 0.25rem;
}
.prose-dark pre {
background-color: #111827;
border: 1px solid #374151;
}
.prose-dark blockquote {
border-left: 4px solid #3b82f6;
background-color: #1f2937;
padding-left: 1rem;
padding-top: 0.5rem;
padding-bottom: 0.5rem;
margin: 1rem 0;
font-style: italic;
}
.prose-dark table {
border-collapse: collapse;
border: 1px solid #4b5563;
}
.prose-dark th, .prose-dark td {
border: 1px solid #4b5563;
padding: 0.75rem;
}
.prose-dark th {
background-color: #1f2937;
font-weight: 600;
}
.prose-dark img {
max-width: 100%;
height: auto;
border-radius: 0.5rem;
box-shadow: 0 10px 15px -3px rgba(0, 0, 0, 0.1);
cursor: pointer;
}
/* Sidebar styles */
.sidebar-item {
display: flex;
align-items: center;
padding: 0.5rem 0.75rem;
border-radius: 0.5rem;
transition: background-color 0.2s;
cursor: pointer;
}
.sidebar-item:hover {
background-color: #374151;
}
.sidebar-item.active {
background-color: #2563eb;
color: white;
}
/* Custom button styles */
.btn-primary {
background-color: #2563eb;
color: white;
font-weight: 500;
padding: 0.5rem 1rem;
border-radius: 0.5rem;
transition: background-color 0.2s;
text-decoration: none;
display: inline-block;
}
.btn-primary:hover {
background-color: #1d4ed8;
}
.btn-secondary {
background-color: #4b5563;
color: white;
font-weight: 500;
padding: 0.5rem 1rem;
border-radius: 0.5rem;
transition: background-color 0.2s;
text-decoration: none;
display: inline-block;
}
.btn-secondary:hover {
background-color: #374151;
}
.btn-danger {
background-color: #dc2626;
color: white;
font-weight: 500;
padding: 0.5rem 1rem;
border-radius: 0.5rem;
transition: background-color 0.2s;
text-decoration: none;
display: inline-block;
}
.btn-danger:hover {
background-color: #b91c1c;
}
/* Modal styles */
.modal-overlay {
position: fixed;
top: 0;
left: 0;
right: 0;
bottom: 0;
background-color: rgba(0, 0, 0, 0.5);
display: flex;
align-items: center;
justify-content: center;
z-index: 50;
}
.modal-content {
background-color: #1f2937;
border-radius: 0.5rem;
padding: 1.5rem;
max-width: 32rem;
width: 100%;
margin: 1rem;
max-height: 24rem;
overflow-y: auto;
}
/* Editor styles */
.editor-textarea {
width: 100%;
min-height: 24rem;
background-color: #1f2937;
color: #d1d5db;
border: 1px solid #4b5563;
border-radius: 0.5rem;
padding: 1rem;
font-family: monospace;
font-size: 0.875rem;
resize: vertical;
}
.editor-textarea:focus {
outline: none;
border-color: #3b82f6;
box-shadow: 0 0 0 2px rgba(59, 130, 246, 0.5);
}
/* Form input styles */
.form-input, .form-textarea {
width: 100%;
background-color: #374151;
border: 1px solid #4b5563;
border-radius: 0.5rem;
padding: 0.5rem 0.75rem;
color: white;
}
.form-input:focus, .form-textarea:focus {
outline: none;
border-color: #3b82f6;
box-shadow: 0 0 0 2px rgba(59, 130, 246, 0.5);
}
.hidden {
display: none;
}
.rotate-90 {
transform: rotate(90deg);
}
</style>
</head>
<body class="bg-slate-900 text-gray-300 min-h-screen">
<div class="flex h-screen">
<!-- Sidebar -->
<div id="sidebar" class="w-80 bg-slate-800 border-r border-gray-700 flex flex-col">
<!-- Header -->
<div class="p-4 border-b border-gray-700">
<div class="flex items-center justify-between">
<h1 class="text-xl font-bold text-white">{{.app_name}}</h1>
<div class="flex items-center space-x-2">
<a href="/settings" class="text-gray-400 hover:text-white transition-colors" title="Settings">
<i class="fas fa-cog"></i>
</a>
</div>
</div>
</div>
<!-- Navigation -->
<div class="px-4 py-4">
<a href="/create" class="btn-primary text-sm w-full text-center">
<i class="fas fa-plus mr-2"></i>New Note
</a>
</div>
<!-- File Tree -->
<div class="flex-1 overflow-y-auto px-4 pb-4">
{{if .notes_tree}}
{{template "tree_node" dict "node" .notes_tree "active_path" .active_path "current_note" .current_note}}
{{end}}
</div>
</div>
<!-- Main Content -->
<div class="flex-1 flex flex-col overflow-hidden">
<!-- Breadcrumbs -->
{{if .breadcrumbs}}
<div class="bg-slate-800 border-b border-gray-700 px-6 py-3">
<nav class="flex items-center space-x-2 text-sm">
{{range $i, $crumb := .breadcrumbs}}
{{if $i}}<i class="fas fa-chevron-right text-gray-500 text-xs"></i>{{end}}
{{if $crumb.URL}}
<a href="{{$crumb.URL}}" class="text-blue-400 hover:text-blue-300 transition-colors">{{$crumb.Name}}</a>
{{else}}
<span class="text-gray-300">{{$crumb.Name}}</span>
{{end}}
{{end}}
</nav>
</div>
{{end}}
<!-- Content Area -->
<div class="flex-1 overflow-y-auto">
{{block "content" .}}{{end}}
</div>
</div>
</div>
<!-- Scripts -->
<script>
// Initialize syntax highlighting
hljs.highlightAll();
// Tree functionality
document.addEventListener('click', function(e) {
if (e.target.closest('.tree-toggle')) {
const toggle = e.target.closest('.tree-toggle');
const children = toggle.nextElementSibling;
const chevron = toggle.querySelector('.tree-chevron');
if (children && children.classList.contains('tree-children')) {
children.classList.toggle('hidden');
if (chevron) {
chevron.classList.toggle('rotate-90');
}
}
}
});
// Auto-expand active path
function expandActivePath() {
const activeItem = document.querySelector('.sidebar-item.active');
if (activeItem) {
let parent = activeItem.parentElement;
while (parent) {
if (parent.classList.contains('tree-children')) {
parent.classList.remove('hidden');
const toggle = parent.previousElementSibling;
if (toggle && toggle.classList.contains('tree-toggle')) {
const chevron = toggle.querySelector('.tree-chevron');
if (chevron) {
chevron.classList.add('rotate-90');
}
}
}
parent = parent.parentElement;
}
}
}
// Notification system
function showNotification(message, type = 'info', duration = 3000) {
const notification = document.createElement('div');
notification.className = `fixed top-4 right-4 z-50 px-6 py-3 rounded-lg shadow-lg max-w-sm`;
if (type === 'success') {
notification.className += ' bg-green-600 text-white';
} else if (type === 'error') {
notification.className += ' bg-red-600 text-white';
} else {
notification.className += ' bg-blue-600 text-white';
}
notification.innerHTML = `
<div class="flex items-center justify-between">
<span>${message}</span>
<button class="ml-4 text-white hover:text-gray-300" onclick="this.closest('div').remove()">
<i class="fas fa-times"></i>
</button>
</div>
`;
document.body.appendChild(notification);
setTimeout(() => {
if (notification.parentElement) {
notification.remove();
}
}, duration);
}
// Initialize on page load
document.addEventListener('DOMContentLoaded', function() {
expandActivePath();
});
</script>
{{block "scripts" .}}{{end}}
</body>
</html>
<!-- Tree Node Template -->
{{define "tree_node"}}
<div class="tree-node">
{{if .node.Children}}
<div class="tree-toggle flex items-center py-1 hover:bg-gray-700 rounded px-2 cursor-pointer" data-path="{{.node.Path}}">
<i class="fas fa-chevron-right transform transition-transform duration-200 mr-2 text-xs tree-chevron"></i>
<span class="mr-2">📁</span>
<span class="flex-1">{{.node.Name}}</span>
</div>
<div class="tree-children ml-4 hidden">
{{range .node.Children}}
{{template "tree_node" dict "node" . "active_path" $.active_path "current_note" $.current_note}}
{{end}}
</div>
{{else}}
{{if eq .node.Type "md"}}
<a href="/note/{{.node.Path}}" class="sidebar-item {{if eq .current_note .node.Path}}active{{end}}">
<span class="mr-2">📝</span>
<span>{{.node.Name}}</span>
</a>
{{else}}
<a href="/view_text/{{.node.Path}}" class="sidebar-item">
<span class="mr-2">📄</span>
<span>{{.node.Name}}</span>
</a>
{{end}}
{{end}}
</div>
{{end}}

325
web/templates/base_new.html Normal file
View File

@@ -0,0 +1,325 @@
{{define "base.html"}}
<!DOCTYPE html>
<html lang="en" class="dark">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>{{block "title" .}}{{.app_name}}{{end}}</title>
<script src="https://cdn.tailwindcss.com"></script>
<script>
tailwind.config = {
darkMode: 'class',
theme: {
extend: {
colors: {
'obsidian': {
'50': '#f8fafc',
'100': '#f1f5f9',
'200': '#e2e8f0',
'300': '#cbd5e1',
'400': '#94a3b8',
'500': '#64748b',
'600': '#475569',
'700': '#334155',
'800': '#1e293b',
'900': '#0f172a',
}
}
}
}
}
</script>
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/highlight.js/11.9.0/styles/github-dark.min.css">
<script src="https://cdnjs.cloudflare.com/ajax/libs/highlight.js/11.9.0/highlight.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.4.0/js/all.min.js"></script>
<style>
/* Custom scrollbar for dark theme */
::-webkit-scrollbar {
width: 8px;
height: 8px;
}
::-webkit-scrollbar-track {
background: #1e293b;
}
::-webkit-scrollbar-thumb {
background: #475569;
border-radius: 4px;
}
::-webkit-scrollbar-thumb:hover {
background: #64748b;
}
/* Content markdown styles */
.prose-dark {
color: #d1d5db;
}
.prose-dark h1, .prose-dark h2, .prose-dark h3, .prose-dark h4, .prose-dark h5, .prose-dark h6 {
color: white;
}
.prose-dark a {
color: #60a5fa;
}
.prose-dark a:hover {
color: #93c5fd;
}
.prose-dark code {
background-color: #1f2937;
color: #10b981;
padding: 0.125rem 0.25rem;
border-radius: 0.25rem;
}
.prose-dark pre {
background-color: #111827;
border: 1px solid #374151;
}
.prose-dark blockquote {
border-left: 4px solid #3b82f6;
background-color: #1f2937;
padding-left: 1rem;
padding-top: 0.5rem;
padding-bottom: 0.5rem;
margin: 1rem 0;
font-style: italic;
}
/* Custom button styles */
.btn-primary {
background-color: #2563eb;
color: white;
font-weight: 500;
padding: 0.5rem 1rem;
border-radius: 0.5rem;
transition: background-color 0.2s;
}
.btn-primary:hover {
background-color: #1d4ed8;
}
.btn-secondary {
background-color: #4b5563;
color: white;
font-weight: 500;
padding: 0.5rem 1rem;
border-radius: 0.5rem;
transition: background-color 0.2s;
}
.btn-secondary:hover {
background-color: #374151;
}
.btn-danger {
background-color: #dc2626;
color: white;
font-weight: 500;
padding: 0.5rem 1rem;
border-radius: 0.5rem;
transition: background-color 0.2s;
}
.btn-danger:hover {
background-color: #b91c1c;
}
/* Sidebar styles */
.sidebar-item {
display: flex;
align-items: center;
padding: 0.5rem 0.75rem;
border-radius: 0.5rem;
transition: background-color 0.2s;
cursor: pointer;
}
.sidebar-item:hover {
background-color: #374151;
}
.sidebar-item.active {
background-color: #2563eb;
color: white;
}
/* Modal styles */
.modal-overlay {
position: fixed;
top: 0;
left: 0;
right: 0;
bottom: 0;
background-color: rgba(0, 0, 0, 0.5);
display: flex;
align-items: center;
justify-content: center;
z-index: 50;
}
.modal-content {
background-color: #1f2937;
border-radius: 0.5rem;
padding: 1.5rem;
max-width: 32rem;
width: 100%;
margin: 1rem;
max-height: 24rem;
overflow-y: auto;
}
/* Editor styles */
.editor-textarea {
width: 100%;
min-height: 24rem;
background-color: #1f2937;
color: #d1d5db;
border: 1px solid #4b5563;
border-radius: 0.5rem;
padding: 1rem;
font-family: monospace;
font-size: 0.875rem;
resize: vertical;
}
.editor-textarea:focus {
outline: none;
border-color: #3b82f6;
box-shadow: 0 0 0 2px rgba(59, 130, 246, 0.5);
}
/* Form input styles */
.form-input, .form-textarea {
width: 100%;
background-color: #374151;
border: 1px solid #4b5563;
border-radius: 0.5rem;
padding: 0.5rem 0.75rem;
color: white;
}
.form-input:focus, .form-textarea:focus {
outline: none;
border-color: #3b82f6;
box-shadow: 0 0 0 2px rgba(59, 130, 246, 0.5);
}
.hidden {
display: none;
}
</style>
</head>
<body class="bg-slate-900 text-gray-300 min-h-screen">
<div class="flex h-screen">
<!-- Sidebar -->
<div id="sidebar" class="w-80 bg-slate-800 border-r border-gray-700 flex flex-col">
<!-- Header -->
<div class="p-4 border-b border-gray-700">
<div class="flex items-center justify-between">
<h1 class="text-xl font-bold text-white">{{.app_name}}</h1>
<div class="flex items-center space-x-2">
<a href="/settings" class="text-gray-400 hover:text-white transition-colors" title="Settings">
<i class="fas fa-cog"></i>
</a>
</div>
</div>
</div>
<!-- Search -->
<div class="p-4">
<input type="text" id="search-input" placeholder="Search notes..."
class="form-input text-sm">
</div>
<!-- Navigation -->
<div class="px-4 pb-4">
<a href="/create" class="btn-primary text-sm w-full text-center block">
<i class="fas fa-plus mr-2"></i>New Note
</a>
</div>
<!-- File Tree -->
<div class="flex-1 overflow-y-auto px-4 pb-4">
{{if .notes_tree}}
{{template "tree_node" dict "node" .notes_tree "active_path" .active_path "current_note" .current_note}}
{{end}}
</div>
</div>
<!-- Main Content -->
<div class="flex-1 flex flex-col overflow-hidden">
<!-- Breadcrumbs -->
{{if .breadcrumbs}}
<div class="bg-slate-800 border-b border-gray-700 px-6 py-3">
<nav class="flex items-center space-x-2 text-sm">
{{range $i, $crumb := .breadcrumbs}}
{{if $i}}<i class="fas fa-chevron-right text-gray-500 text-xs"></i>{{end}}
{{if $crumb.URL}}
<a href="{{$crumb.URL}}" class="text-blue-400 hover:text-blue-300 transition-colors">{{$crumb.Name}}</a>
{{else}}
<span class="text-gray-300">{{$crumb.Name}}</span>
{{end}}
{{end}}
</nav>
</div>
{{end}}
<!-- Content Area -->
<div class="flex-1 overflow-y-auto">
{{block "content" .}}{{end}}
</div>
</div>
</div>
<!-- Scripts -->
<script>
// Initialize syntax highlighting
hljs.highlightAll();
// Search functionality
const searchInput = document.getElementById('search-input');
if (searchInput) {
searchInput.addEventListener('input', function() {
// TODO: Implement search functionality
});
}
// Tree functionality
document.querySelectorAll('.tree-toggle').forEach(toggle => {
toggle.addEventListener('click', function() {
const children = this.nextElementSibling;
if (children && children.classList.contains('tree-children')) {
children.classList.toggle('hidden');
}
});
});
// Notification system
function showNotification(message, type = 'info', duration = 3000) {
// Simple notification for now
alert(message);
}
</script>
{{block "scripts" .}}{{end}}
</body>
</html>
{{end}}
<!-- Tree Node Template -->
{{define "tree_node"}}
<div class="tree-node">
{{if .node.Children}}
<div class="tree-toggle flex items-center py-1 hover:bg-gray-700 rounded px-2 cursor-pointer" data-path="{{.node.Path}}">
<i class="fas fa-chevron-right transform transition-transform duration-200 mr-2 text-xs"></i>
<span class="mr-2">📁</span>
<span class="flex-1">{{.node.Name}}</span>
</div>
<div class="tree-children ml-4 hidden">
{{range .node.Children}}
{{template "tree_node" dict "node" . "active_path" $.active_path "current_note" $.current_note}}
{{end}}
</div>
{{else}}
{{if eq .node.Type "md"}}
<a href="/note/{{.node.Path}}" class="sidebar-item {{if eq .current_note .node.Path}}active{{end}}">
<span class="mr-2">📝</span>
<span>{{.node.Name}}</span>
</a>
{{else}}
<a href="/view_text/{{.node.Path}}" class="sidebar-item">
<span class="mr-2">📄</span>
<span>{{.node.Name}}</span>
</a>
{{end}}
{{end}}
</div>
{{end}}

145
web/templates/create.html Normal file
View File

@@ -0,0 +1,145 @@
{{template "base.html" .}}
{{define "content"}}
<div class="max-w-4xl mx-auto p-6">
<!-- Header -->
<div class="mb-6">
<h1 class="text-3xl font-bold text-white mb-4">Create New Note</h1>
{{if .folder_path}}
<p class="text-gray-400">
<i class="fas fa-folder mr-2"></i>
Creating in: <span class="text-blue-400">{{.folder_path}}</span>
</p>
{{end}}
</div>
<!-- Create Form -->
<form id="create-form" class="space-y-6">
<div class="bg-gray-800 rounded-lg p-6">
<!-- Title Input -->
<div class="mb-6">
<label for="title" class="block text-sm font-medium text-gray-300 mb-2">
Note Title
</label>
<input type="text" id="title" name="title" required
class="w-full bg-gray-700 border border-gray-600 rounded-lg px-4 py-2 text-white focus:ring-2 focus:ring-blue-500 focus:border-transparent"
placeholder="Enter note title (e.g., My New Note)">
<p class="text-xs text-gray-500 mt-1">The .md extension will be added automatically</p>
</div>
<!-- Content Editor -->
<div class="mb-6">
<label for="content" class="block text-sm font-medium text-gray-300 mb-2">
Content
</label>
<textarea id="content" name="content" rows="20"
class="editor-textarea"
placeholder="# Your Note Title
Start writing your note here using Markdown syntax...
## Examples
- **Bold text**
- *Italic text*
- [Links](https://example.com)
- ![Images](image.png)
- `Code snippets`
```javascript
// Code blocks
console.log('Hello, World!');
```
> Blockquotes
| Tables | Work | Too |
|--------|------|-----|
| Cell 1 | Cell 2 | Cell 3 |
"></textarea>
</div>
<!-- Actions -->
<div class="flex items-center justify-between">
<a href="{{if .folder_path}}/folder/{{.folder_path}}{{else}}/{{end}}" class="btn-secondary">
<i class="fas fa-arrow-left mr-2"></i>Cancel
</a>
<button type="submit" class="btn-primary">
<i class="fas fa-save mr-2"></i>Create Note
</button>
</div>
</div>
</form>
</div>
{{end}}
{{define "scripts"}}
<script>
const createForm = document.getElementById('create-form');
const titleInput = document.getElementById('title');
const contentTextarea = document.getElementById('content');
createForm.addEventListener('submit', function(e) {
e.preventDefault();
const title = titleInput.value.trim();
const content = contentTextarea.value;
const folderPath = '{{.folder_path}}';
if (!title) {
showNotification('Please enter a note title', 'error');
return;
}
const formData = new FormData();
formData.append('title', title);
formData.append('content', content);
formData.append('folder_path', folderPath);
fetch('/create', {
method: 'POST',
body: formData
})
.then(response => response.json())
.then(data => {
if (data.success) {
showNotification('Note created successfully', 'success');
if (data.redirect) {
window.location.href = data.redirect;
}
} else {
throw new Error(data.error || 'Failed to create note');
}
})
.catch(error => {
showNotification('Error: ' + error.message, 'error');
});
});
// Auto-resize textarea
contentTextarea.addEventListener('input', function() {
this.style.height = 'auto';
this.style.height = (this.scrollHeight) + 'px';
});
// Keyboard shortcuts
contentTextarea.addEventListener('keydown', function(e) {
// Ctrl+S or Cmd+S to save
if ((e.ctrlKey || e.metaKey) && e.key === 's') {
e.preventDefault();
createForm.dispatchEvent(new Event('submit'));
}
// Tab for indentation
if (e.key === 'Tab') {
e.preventDefault();
const start = this.selectionStart;
const end = this.selectionEnd;
const value = this.value;
this.value = value.substring(0, start) + '\t' + value.substring(end);
this.selectionStart = this.selectionEnd = start + 1;
}
});
</script>
{{end}}

190
web/templates/edit.html Normal file
View File

@@ -0,0 +1,190 @@
{{template "base.html" .}}
{{define "content"}}
<div class="max-w-4xl mx-auto p-6">
<!-- Header -->
<div class="mb-6">
<div class="flex items-center justify-between mb-4">
<h1 class="text-3xl font-bold text-white">Edit: {{.title}}</h1>
<div class="flex items-center space-x-3">
<a href="/note/{{.note_path}}" class="btn-secondary">
<i class="fas fa-eye mr-2"></i>Preview
</a>
<button type="submit" form="edit-form" class="btn-primary">
<i class="fas fa-save mr-2"></i>Save
</button>
</div>
</div>
{{if .folder_path}}
<p class="text-gray-400">
<i class="fas fa-folder mr-2"></i>
<a href="/folder/{{.folder_path}}" class="text-blue-400 hover:text-blue-300">{{.folder_path}}</a>
</p>
{{end}}
</div>
<!-- Edit Form -->
<form id="edit-form" class="space-y-6">
<div class="bg-gray-800 rounded-lg p-6">
<!-- Content Editor -->
<div class="mb-6">
<label for="content" class="block text-sm font-medium text-gray-300 mb-2">
Content
</label>
<textarea id="content" name="content" rows="25"
class="editor-textarea">{{.content}}</textarea>
</div>
<!-- Editor Toolbar -->
<div class="flex items-center justify-between border-t border-gray-700 pt-4">
<div class="flex items-center space-x-2">
<button type="button" class="btn-secondary text-sm" onclick="insertMarkdown('**', '**')" title="Bold">
<i class="fas fa-bold"></i>
</button>
<button type="button" class="btn-secondary text-sm" onclick="insertMarkdown('*', '*')" title="Italic">
<i class="fas fa-italic"></i>
</button>
<button type="button" class="btn-secondary text-sm" onclick="insertMarkdown('`', '`')" title="Code">
<i class="fas fa-code"></i>
</button>
<button type="button" class="btn-secondary text-sm" onclick="insertMarkdown('[', '](url)')" title="Link">
<i class="fas fa-link"></i>
</button>
<button type="button" class="btn-secondary text-sm" onclick="insertMarkdown('![', '](image.png)')" title="Image">
<i class="fas fa-image"></i>
</button>
<button type="button" class="btn-secondary text-sm" onclick="insertHeading()" title="Heading">
<i class="fas fa-heading"></i>
</button>
<button type="button" class="btn-secondary text-sm" onclick="insertList()" title="List">
<i class="fas fa-list"></i>
</button>
</div>
<div class="text-xs text-gray-500">
Press Ctrl+S to save
</div>
</div>
</div>
</form>
</div>
{{end}}
{{define "scripts"}}
<script>
const editForm = document.getElementById('edit-form');
const contentTextarea = document.getElementById('content');
editForm.addEventListener('submit', function(e) {
e.preventDefault();
const content = contentTextarea.value;
const notePath = '{{.note_path}}';
const formData = new FormData();
formData.append('content', content);
fetch('/edit/' + notePath, {
method: 'POST',
body: formData
})
.then(response => response.json())
.then(data => {
if (data.success) {
showNotification('Note saved successfully', 'success');
if (data.redirect) {
setTimeout(() => {
window.location.href = data.redirect;
}, 1000);
}
} else {
throw new Error(data.error || 'Failed to save note');
}
})
.catch(error => {
showNotification('Error: ' + error.message, 'error');
});
});
// Auto-resize textarea
function autoResize() {
contentTextarea.style.height = 'auto';
contentTextarea.style.height = (contentTextarea.scrollHeight) + 'px';
}
contentTextarea.addEventListener('input', autoResize);
// Initial resize
autoResize();
// Keyboard shortcuts
contentTextarea.addEventListener('keydown', function(e) {
// Ctrl+S or Cmd+S to save
if ((e.ctrlKey || e.metaKey) && e.key === 's') {
e.preventDefault();
editForm.dispatchEvent(new Event('submit'));
}
// Tab for indentation
if (e.key === 'Tab') {
e.preventDefault();
const start = this.selectionStart;
const end = this.selectionEnd;
const value = this.value;
this.value = value.substring(0, start) + '\t' + value.substring(end);
this.selectionStart = this.selectionEnd = start + 1;
}
});
// Markdown insertion functions
function insertMarkdown(before, after) {
const start = contentTextarea.selectionStart;
const end = contentTextarea.selectionEnd;
const text = contentTextarea.value;
const selectedText = text.substring(start, end);
const newText = text.substring(0, start) + before + selectedText + after + text.substring(end);
contentTextarea.value = newText;
// Position cursor
const newPos = start + before.length + selectedText.length;
contentTextarea.focus();
contentTextarea.setSelectionRange(newPos, newPos);
autoResize();
}
function insertHeading() {
const start = contentTextarea.selectionStart;
const text = contentTextarea.value;
const lineStart = text.lastIndexOf('\n', start - 1) + 1;
const lineText = text.substring(lineStart, start);
if (lineText.startsWith('# ')) {
return; // Already a heading
}
const newText = text.substring(0, lineStart) + '# ' + text.substring(lineStart);
contentTextarea.value = newText;
contentTextarea.focus();
contentTextarea.setSelectionRange(start + 2, start + 2);
autoResize();
}
function insertList() {
const start = contentTextarea.selectionStart;
const text = contentTextarea.value;
const lineStart = text.lastIndexOf('\n', start - 1) + 1;
const newText = text.substring(0, lineStart) + '- ' + text.substring(lineStart);
contentTextarea.value = newText;
contentTextarea.focus();
contentTextarea.setSelectionRange(start + 2, start + 2);
autoResize();
}
</script>
{{end}}

38
web/templates/error.html Normal file
View File

@@ -0,0 +1,38 @@
{{template "base.html" .}}
{{define "content"}}
<div class="flex items-center justify-center min-h-screen">
<div class="max-w-md w-full mx-4">
<div class="bg-gray-800 rounded-lg p-8 text-center">
<div class="mb-6">
<i class="fas fa-exclamation-triangle text-6xl text-red-500 mb-4"></i>
<h1 class="text-2xl font-bold text-white mb-2">Error</h1>
{{if .error}}
<h2 class="text-lg text-gray-300 mb-4">{{.error}}</h2>
{{end}}
</div>
{{if .message}}
<div class="bg-gray-700 rounded-lg p-4 mb-6">
<p class="text-gray-300 text-sm">{{.message}}</p>
</div>
{{end}}
<div class="space-y-3">
<a href="/" class="block btn-primary">
<i class="fas fa-home mr-2"></i>Go Home
</a>
<button onclick="history.back()" class="block btn-secondary w-full">
<i class="fas fa-arrow-left mr-2"></i>Go Back
</button>
</div>
</div>
</div>
</div>
{{end}}
{{define "error_scripts"}}
<script>
// No additional scripts needed for error page
</script>
{{end}}

256
web/templates/folder.html Normal file
View File

@@ -0,0 +1,256 @@
{{template "base.html" .}}
{{define "content"}}
<div class="p-6">
<!-- Header with upload button -->
<div class="flex items-center justify-between mb-6">
<div>
<h1 class="text-2xl font-bold text-white mb-2">
{{if .folder_path}}
{{.folder_path}}
{{else}}
Welcome to {{.app_name}}
{{end}}
</h1>
<p class="text-gray-400">
{{if .folder_contents}}
{{len .folder_contents}} items
{{else}}
No items found
{{end}}
</p>
</div>
<div class="flex items-center space-x-3">
<button id="upload-btn" class="btn-primary">
<i class="fas fa-upload mr-2"></i>Upload File
</button>
<a href="/create?folder={{.folder_path}}" class="btn-secondary">
<i class="fas fa-plus mr-2"></i>New Note
</a>
</div>
</div>
<!-- Upload Area (hidden by default) -->
<div id="upload-area" class="upload-area mb-6 hidden">
<div class="flex flex-col items-center">
<i class="fas fa-cloud-upload text-4xl text-gray-500 mb-4"></i>
<p class="text-gray-400 mb-2">Drag and drop files here or click to select</p>
<input type="file" id="file-input" multiple class="hidden">
<button id="select-files" class="btn-secondary">Select Files</button>
</div>
<div id="upload-progress" class="mt-4 hidden">
<div class="w-full bg-gray-700 rounded-full h-2">
<div id="progress-bar" class="bg-blue-600 h-2 rounded-full transition-all duration-300" style="width: 0%"></div>
</div>
<p id="upload-status" class="text-sm text-gray-400 mt-2"></p>
</div>
</div>
<!-- Content Grid -->
<div class="grid gap-4">
{{if .folder_contents}}
{{range .folder_contents}}
<div class="bg-gray-800 rounded-lg p-4 hover:bg-gray-700 transition-colors duration-200 cursor-pointer item-card"
data-path="{{.Path}}" data-type="{{.Type}}">
<div class="flex items-center justify-between">
<div class="flex items-center space-x-3">
<span class="text-2xl">{{fileTypeIcon .Type}}</span>
<div>
<h3 class="font-medium text-white">{{.DisplayName}}</h3>
<p class="text-sm text-gray-400">
{{if eq .Type "dir"}}
Folder
{{else}}
{{formatSize .Size}} • {{formatTime .ModTime}}
{{end}}
</p>
</div>
</div>
<div class="flex items-center space-x-2">
{{if eq .Type "md"}}
<a href="/edit/{{.Path}}" class="text-blue-400 hover:text-blue-300 p-2" title="Edit">
<i class="fas fa-edit"></i>
</a>
{{end}}
{{if ne .Type "dir"}}
<a href="/download/{{.Path}}" class="text-green-400 hover:text-green-300 p-2" title="Download">
<i class="fas fa-download"></i>
</a>
{{end}}
<button class="text-red-400 hover:text-red-300 p-2 delete-btn" data-path="{{.Path}}" title="Delete">
<i class="fas fa-trash"></i>
</button>
</div>
</div>
</div>
{{end}}
{{else}}
<div class="text-center py-12">
<i class="fas fa-folder-open text-6xl text-gray-600 mb-4"></i>
<p class="text-xl text-gray-400 mb-2">This folder is empty</p>
<p class="text-gray-500">Create a new note or upload files to get started</p>
</div>
{{end}}
</div>
</div>
<!-- Delete Confirmation Modal -->
<div id="delete-modal" class="modal-overlay hidden">
<div class="modal-content">
<h3 class="text-lg font-medium text-white mb-4">Confirm Delete</h3>
<p class="text-gray-300 mb-6">Are you sure you want to delete this item? This action cannot be undone.</p>
<div class="flex justify-end space-x-3">
<button id="cancel-delete" class="btn-secondary">Cancel</button>
<button id="confirm-delete" class="btn-danger">Delete</button>
</div>
</div>
</div>
{{end}}
{{define "scripts"}}
<script>
let uploadArea = document.getElementById('upload-area');
let fileInput = document.getElementById('file-input');
let uploadBtn = document.getElementById('upload-btn');
let selectFilesBtn = document.getElementById('select-files');
let uploadProgress = document.getElementById('upload-progress');
let progressBar = document.getElementById('progress-bar');
let uploadStatus = document.getElementById('upload-status');
let deleteModal = document.getElementById('delete-modal');
let deleteTarget = null;
// Toggle upload area
uploadBtn.addEventListener('click', function() {
uploadArea.classList.toggle('hidden');
});
// File selection
selectFilesBtn.addEventListener('click', function() {
fileInput.click();
});
fileInput.addEventListener('change', function() {
if (this.files.length > 0) {
uploadFiles(this.files);
}
});
// Drag and drop
uploadArea.addEventListener('dragover', function(e) {
e.preventDefault();
this.classList.add('dragover');
});
uploadArea.addEventListener('dragleave', function(e) {
e.preventDefault();
this.classList.remove('dragover');
});
uploadArea.addEventListener('drop', function(e) {
e.preventDefault();
this.classList.remove('dragover');
if (e.dataTransfer.files.length > 0) {
uploadFiles(e.dataTransfer.files);
}
});
// Upload files function
function uploadFiles(files) {
uploadProgress.classList.remove('hidden');
progressBar.style.width = '0%';
uploadStatus.textContent = 'Preparing upload...';
const formData = new FormData();
formData.append('path', '{{.folder_path}}');
for (let file of files) {
formData.append('file', file);
}
fetch('/upload', {
method: 'POST',
body: formData
})
.then(response => response.json())
.then(data => {
if (data.success) {
progressBar.style.width = '100%';
uploadStatus.textContent = 'Upload complete!';
showNotification('Files uploaded successfully', 'success');
setTimeout(() => {
window.location.reload();
}, 1000);
} else {
throw new Error(data.error || 'Upload failed');
}
})
.catch(error => {
uploadStatus.textContent = 'Upload failed: ' + error.message;
showNotification('Upload failed: ' + error.message, 'error');
});
}
// Item click handlers
document.addEventListener('click', function(e) {
const itemCard = e.target.closest('.item-card');
if (itemCard && !e.target.closest('a') && !e.target.closest('button')) {
const path = itemCard.dataset.path;
const type = itemCard.dataset.type;
if (type === 'dir') {
window.location.href = '/folder/' + path;
} else if (type === 'md') {
window.location.href = '/note/' + path;
} else if (type === 'image') {
window.open('/serve_attached_image/' + path, '_blank');
} else {
window.location.href = '/view_text/' + path;
}
}
});
// Delete functionality
document.addEventListener('click', function(e) {
if (e.target.closest('.delete-btn')) {
e.stopPropagation();
deleteTarget = e.target.closest('.delete-btn').dataset.path;
deleteModal.classList.remove('hidden');
}
});
document.getElementById('cancel-delete').addEventListener('click', function() {
deleteModal.classList.add('hidden');
deleteTarget = null;
});
document.getElementById('confirm-delete').addEventListener('click', function() {
if (deleteTarget) {
fetch('/delete/' + deleteTarget, {
method: 'DELETE'
})
.then(response => response.json())
.then(data => {
if (data.success) {
showNotification('Item deleted successfully', 'success');
window.location.reload();
} else {
throw new Error(data.error || 'Delete failed');
}
})
.catch(error => {
showNotification('Delete failed: ' + error.message, 'error');
});
}
deleteModal.classList.add('hidden');
deleteTarget = null;
});
// Close modal when clicking outside
deleteModal.addEventListener('click', function(e) {
if (e.target === this) {
this.classList.add('hidden');
deleteTarget = null;
}
});
</script>
{{end}}

93
web/templates/note.html Normal file
View File

@@ -0,0 +1,93 @@
{{define "content"}}
<div class="max-w-4xl mx-auto p-6">
<!-- Note Header -->
<div class="mb-6">
<div class="flex items-center justify-between mb-4">
<h1 class="text-3xl font-bold text-white">{{.title}}</h1>
<div class="flex items-center space-x-3">
<a href="/edit/{{.note_path}}" class="btn-primary">
<i class="fas fa-edit mr-2"></i>Edit
</a>
<a href="/download/{{.note_path}}" class="btn-secondary">
<i class="fas fa-download mr-2"></i>Download
</a>
<button class="btn-danger delete-note-btn" data-path="{{.note_path}}">
<i class="fas fa-trash mr-2"></i>Delete
</button>
</div>
</div>
{{if .folder_path}}
<p class="text-gray-400">
<i class="fas fa-folder mr-2"></i>
<a href="/folder/{{.folder_path}}" class="text-blue-400 hover:text-blue-300">{{.folder_path}}</a>
</p>
{{end}}
</div>
<!-- Note Content -->
<div class="bg-gray-800 rounded-lg p-6">
<div class="prose prose-dark max-w-none">
{{.content | safeHTML}}
</div>
</div>
</div>
<!-- Delete Confirmation Modal -->
<div id="delete-modal" class="modal-overlay hidden">
<div class="modal-content">
<h3 class="text-lg font-medium text-white mb-4">Confirm Delete</h3>
<p class="text-gray-300 mb-6">Are you sure you want to delete this note? This action cannot be undone.</p>
<div class="flex justify-end space-x-3">
<button id="cancel-delete" class="btn-secondary">Cancel</button>
<button id="confirm-delete" class="btn-danger">Delete</button>
</div>
</div>
</div>
{{end}}
{{define "scripts"}}
<script>
document.addEventListener('DOMContentLoaded', function() {
const deleteBtn = document.querySelector('.delete-note-btn');
const modal = document.getElementById('delete-modal');
const cancelBtn = document.getElementById('cancel-delete');
const confirmBtn = document.getElementById('confirm-delete');
if (deleteBtn) {
deleteBtn.addEventListener('click', function() {
modal.classList.remove('hidden');
});
}
if (cancelBtn) {
cancelBtn.addEventListener('click', function() {
modal.classList.add('hidden');
});
}
if (confirmBtn) {
confirmBtn.addEventListener('click', function() {
const path = deleteBtn.dataset.path;
fetch(`/delete/${path}`, {
method: 'DELETE'
})
.then(response => {
if (response.ok) {
window.location.href = '/';
} else {
alert('Error deleting note');
}
})
.catch(error => {
console.error('Error:', error);
alert('Error deleting note');
});
});
}
// Re-highlight code blocks that might have been added dynamically
hljs.highlightAll();
});
</script>
{{end}}

289
web/templates/settings.html Normal file
View File

@@ -0,0 +1,289 @@
{{template "base.html" .}}
{{define "content"}}
<div class="max-w-6xl mx-auto p-6">
<!-- Header -->
<div class="mb-8">
<h1 class="text-3xl font-bold text-white mb-2">Settings</h1>
<p class="text-gray-400">Configure your {{.app_name}} instance</p>
</div>
<!-- Settings Sections -->
<div class="space-y-8">
<!-- Image Storage Settings -->
<div class="bg-gray-800 rounded-lg p-6">
<h2 class="text-xl font-semibold text-white mb-4">
<i class="fas fa-images mr-2"></i>Image Storage
</h2>
<p class="text-gray-400 mb-6">Configure how images are stored and referenced in your notes</p>
<form id="image-storage-form" class="space-y-6">
<!-- Storage Mode -->
<div>
<label class="block text-sm font-medium text-gray-300 mb-3">Storage Mode</label>
<div class="space-y-3">
<label class="flex items-start space-x-3">
<input type="radio" name="storage_mode" value="1" class="mt-1 text-blue-600">
<div>
<div class="text-white font-medium">Root Directory</div>
<div class="text-sm text-gray-400">Store images directly in the notes root directory</div>
</div>
</label>
<label class="flex items-start space-x-3">
<input type="radio" name="storage_mode" value="2" class="mt-1 text-blue-600">
<div>
<div class="text-white font-medium">Specific Folder</div>
<div class="text-sm text-gray-400">Store all images in a specific folder</div>
</div>
</label>
<label class="flex items-start space-x-3">
<input type="radio" name="storage_mode" value="3" class="mt-1 text-blue-600">
<div>
<div class="text-white font-medium">Same as Note</div>
<div class="text-sm text-gray-400">Store images in the same directory as the note</div>
</div>
</label>
<label class="flex items-start space-x-3">
<input type="radio" name="storage_mode" value="4" class="mt-1 text-blue-600">
<div>
<div class="text-white font-medium">Subfolder of Note</div>
<div class="text-sm text-gray-400">Store images in a subfolder within the note's directory</div>
</div>
</label>
</div>
</div>
<!-- Storage Path (for mode 2) -->
<div id="storage-path-section" class="hidden">
<label for="storage_path" class="block text-sm font-medium text-gray-300 mb-2">
Storage Path
</label>
<input type="text" id="storage_path" name="storage_path"
class="w-full bg-gray-700 border border-gray-600 rounded-lg px-4 py-2 text-white focus:ring-2 focus:ring-blue-500 focus:border-transparent"
placeholder="e.g., images">
</div>
<!-- Subfolder Name (for mode 4) -->
<div id="subfolder-section" class="hidden">
<label for="subfolder_name" class="block text-sm font-medium text-gray-300 mb-2">
Subfolder Name
</label>
<input type="text" id="subfolder_name" name="subfolder_name"
class="w-full bg-gray-700 border border-gray-600 rounded-lg px-4 py-2 text-white focus:ring-2 focus:ring-blue-500 focus:border-transparent"
placeholder="e.g., attached">
</div>
<div class="flex justify-end">
<button type="submit" class="btn-primary">
<i class="fas fa-save mr-2"></i>Save Image Settings
</button>
</div>
</form>
</div>
<!-- Notes Directory Settings -->
<div class="bg-gray-800 rounded-lg p-6">
<h2 class="text-xl font-semibold text-white mb-4">
<i class="fas fa-folder mr-2"></i>Notes Directory
</h2>
<p class="text-gray-400 mb-6">Set the root directory where your notes are stored</p>
<form id="notes-dir-form" class="space-y-6">
<div>
<label for="notes_dir" class="block text-sm font-medium text-gray-300 mb-2">
Notes Directory Path
</label>
<input type="text" id="notes_dir" name="notes_dir"
class="w-full bg-gray-700 border border-gray-600 rounded-lg px-4 py-2 text-white focus:ring-2 focus:ring-blue-500 focus:border-transparent"
placeholder="/path/to/your/notes">
<p class="text-xs text-gray-500 mt-1">Provide the absolute path to your notes directory</p>
</div>
<div class="flex justify-end">
<button type="submit" class="btn-primary">
<i class="fas fa-save mr-2"></i>Save Directory Settings
</button>
</div>
</form>
</div>
<!-- File Extensions Settings -->
<div class="bg-gray-800 rounded-lg p-6">
<h2 class="text-xl font-semibold text-white mb-4">
<i class="fas fa-file mr-2"></i>File Extensions
</h2>
<p class="text-gray-400 mb-6">Configure which file types are allowed and visible</p>
<form id="file-extensions-form" class="space-y-6">
<div>
<label for="allowed_image_extensions" class="block text-sm font-medium text-gray-300 mb-2">
Allowed Image Extensions
</label>
<input type="text" id="allowed_image_extensions" name="allowed_image_extensions"
class="w-full bg-gray-700 border border-gray-600 rounded-lg px-4 py-2 text-white focus:ring-2 focus:ring-blue-500 focus:border-transparent"
placeholder="jpg, jpeg, png, webp, gif">
<p class="text-xs text-gray-500 mt-1">Comma-separated list of image file extensions</p>
</div>
<div>
<label for="allowed_file_extensions" class="block text-sm font-medium text-gray-300 mb-2">
Allowed File Extensions
</label>
<input type="text" id="allowed_file_extensions" name="allowed_file_extensions"
class="w-full bg-gray-700 border border-gray-600 rounded-lg px-4 py-2 text-white focus:ring-2 focus:ring-blue-500 focus:border-transparent"
placeholder="txt, pdf, html, json, yaml, yml, conf, csv">
<p class="text-xs text-gray-500 mt-1">Comma-separated list of viewable file extensions</p>
</div>
<div class="flex items-center">
<input type="checkbox" id="images_hide" name="images_hide"
class="h-4 w-4 text-blue-600 rounded border-gray-600 bg-gray-700">
<label for="images_hide" class="ml-2 text-sm text-gray-300">
Hide images from main folder view
</label>
</div>
<div class="flex justify-end">
<button type="submit" class="btn-primary">
<i class="fas fa-save mr-2"></i>Save Extension Settings
</button>
</div>
</form>
</div>
</div>
</div>
{{end}}
{{define "scripts"}}
<script>
// Load current settings
function loadSettings() {
// Load image storage settings
fetch('/settings/image_storage')
.then(response => response.json())
.then(data => {
document.querySelector(`input[name="storage_mode"][value="${data.mode}"]`).checked = true;
document.getElementById('storage_path').value = data.path || '';
document.getElementById('subfolder_name').value = data.subfolder || '';
toggleStorageOptions();
})
.catch(error => console.error('Error loading image storage settings:', error));
// Load notes directory settings
fetch('/settings/notes_dir')
.then(response => response.json())
.then(data => {
document.getElementById('notes_dir').value = data.notes_dir || '';
})
.catch(error => console.error('Error loading notes directory settings:', error));
// Load file extensions settings
fetch('/settings/file_extensions')
.then(response => response.json())
.then(data => {
document.getElementById('allowed_image_extensions').value = data.allowed_image_extensions || '';
document.getElementById('allowed_file_extensions').value = data.allowed_file_extensions || '';
document.getElementById('images_hide').checked = data.images_hide || false;
})
.catch(error => console.error('Error loading file extensions settings:', error));
}
// Toggle storage mode options
function toggleStorageOptions() {
const mode = document.querySelector('input[name="storage_mode"]:checked')?.value;
const pathSection = document.getElementById('storage-path-section');
const subfolderSection = document.getElementById('subfolder-section');
pathSection.classList.toggle('hidden', mode !== '2');
subfolderSection.classList.toggle('hidden', mode !== '4');
}
// Event listeners
document.addEventListener('change', function(e) {
if (e.target.name === 'storage_mode') {
toggleStorageOptions();
}
});
// Image storage form
document.getElementById('image-storage-form').addEventListener('submit', function(e) {
e.preventDefault();
const formData = new FormData(this);
fetch('/settings/image_storage', {
method: 'POST',
body: formData
})
.then(response => response.json())
.then(data => {
if (data.success) {
showNotification('Image storage settings saved successfully', 'success');
if (data.reload_required) {
setTimeout(() => window.location.reload(), 1500);
}
} else {
throw new Error(data.error || 'Failed to save settings');
}
})
.catch(error => {
showNotification('Error: ' + error.message, 'error');
});
});
// Notes directory form
document.getElementById('notes-dir-form').addEventListener('submit', function(e) {
e.preventDefault();
const formData = new FormData(this);
fetch('/settings/notes_dir', {
method: 'POST',
body: formData
})
.then(response => response.json())
.then(data => {
if (data.success) {
showNotification('Notes directory settings saved successfully', 'success');
if (data.reload_required) {
setTimeout(() => window.location.reload(), 1500);
}
} else {
throw new Error(data.error || 'Failed to save settings');
}
})
.catch(error => {
showNotification('Error: ' + error.message, 'error');
});
});
// File extensions form
document.getElementById('file-extensions-form').addEventListener('submit', function(e) {
e.preventDefault();
const formData = new FormData(this);
fetch('/settings/file_extensions', {
method: 'POST',
body: formData
})
.then(response => response.json())
.then(data => {
if (data.success) {
showNotification('File extension settings saved successfully', 'success');
if (data.reload_required) {
setTimeout(() => window.location.reload(), 1500);
}
} else {
throw new Error(data.error || 'Failed to save settings');
}
})
.catch(error => {
showNotification('Error: ' + error.message, 'error');
});
});
// Load settings on page load
document.addEventListener('DOMContentLoaded', loadSettings);
</script>
{{end}}

18
web/templates/test.html Normal file
View File

@@ -0,0 +1,18 @@
<!DOCTYPE html>
<html>
<head>
<title>Test Page</title>
</head>
<body>
<h1>Test Page Works!</h1>
<p>App Name: {{.app_name}}</p>
<p>Folder Contents Count: {{len .folder_contents}}</p>
{{if .folder_contents}}
<ul>
{{range .folder_contents}}
<li>{{.DisplayName}} ({{.Type}})</li>
{{end}}
</ul>
{{end}}
</body>
</html>

View File

@@ -0,0 +1,103 @@
{{template "base.html" .}}
{{define "content"}}
<div class="max-w-4xl mx-auto p-6">
<!-- Header -->
<div class="mb-6">
<div class="flex items-center justify-between mb-4">
<h1 class="text-3xl font-bold text-white">{{.file_name}}</h1>
<div class="flex items-center space-x-3">
<a href="/download/{{.file_path}}" class="btn-secondary">
<i class="fas fa-download mr-2"></i>Download
</a>
<button class="btn-danger delete-file-btn" data-path="{{.file_path}}">
<i class="fas fa-trash mr-2"></i>Delete
</button>
</div>
</div>
{{if .folder_path}}
<p class="text-gray-400">
<i class="fas fa-folder mr-2"></i>
<a href="/folder/{{.folder_path}}" class="text-blue-400 hover:text-blue-300">{{.folder_path}}</a>
</p>
{{end}}
</div>
<!-- File Content -->
<div class="bg-gray-800 rounded-lg p-6">
<pre class="text-sm text-gray-300 whitespace-pre-wrap overflow-x-auto"><code>{{.content}}</code></pre>
</div>
</div>
<!-- Delete Confirmation Modal -->
<div id="delete-modal" class="modal-overlay hidden">
<div class="modal-content">
<h3 class="text-lg font-medium text-white mb-4">Confirm Delete</h3>
<p class="text-gray-300 mb-6">Are you sure you want to delete this file? This action cannot be undone.</p>
<div class="flex justify-end space-x-3">
<button id="cancel-delete" class="btn-secondary">Cancel</button>
<button id="confirm-delete" class="btn-danger">Delete</button>
</div>
</div>
</div>
{{end}}
{{define "scripts"}}
<script>
let deleteModal = document.getElementById('delete-modal');
let deleteTarget = null;
// Delete functionality
document.addEventListener('click', function(e) {
if (e.target.closest('.delete-file-btn')) {
deleteTarget = e.target.closest('.delete-file-btn').dataset.path;
deleteModal.classList.remove('hidden');
}
});
document.getElementById('cancel-delete').addEventListener('click', function() {
deleteModal.classList.add('hidden');
deleteTarget = null;
});
document.getElementById('confirm-delete').addEventListener('click', function() {
if (deleteTarget) {
fetch('/delete/' + deleteTarget, {
method: 'DELETE'
})
.then(response => response.json())
.then(data => {
if (data.success) {
showNotification('File deleted successfully', 'success');
// Redirect to folder or root
const folderPath = '{{.folder_path}}';
if (folderPath) {
window.location.href = '/folder/' + folderPath;
} else {
window.location.href = '/';
}
} else {
throw new Error(data.error || 'Delete failed');
}
})
.catch(error => {
showNotification('Delete failed: ' + error.message, 'error');
});
}
deleteModal.classList.add('hidden');
deleteTarget = null;
});
// Close modal when clicking outside
deleteModal.addEventListener('click', function(e) {
if (e.target === this) {
this.classList.add('hidden');
deleteTarget = null;
}
});
// Highlight code if possible
hljs.highlightAll();
</script>
{{end}}