Vier Customization-Iterationen (v0.2 bis v0.5.1) haben auf Mobile (iOS Safari) und Chrome Desktop jeweils neue Probleme verursacht statt sie zu lösen. Rollback entfernt: - vault.html: Script-Tag für vault-custom.js entfernt (upstream-original) - server.py: ?file= query-Patch entfernt (upstream-original) - vault-custom.js: leerer Stub mit Comment (Customizations deaktiviert) - hermes-custom.css: leerer Stub (CSS deaktiviert) Was bleibt: - Repo-Struktur (README, CHANGELOG, ARCHITECTURE, CUSTOMIZATIONS, scripts/) - Documented history der Customization-Iterationen für späteres Re-Audit - Backup-Dateien: *.lukas (alle Original-Patches vor diesem Commit) Begründung: Lukas braucht ein verlässlich funktionierendes Wiki, nicht endlose Problemlösungs-Schleifen. Basis muss stimmen, bevor neue Features ausprobiert werden. 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <noreply@anthropic.com>
454 lines
18 KiB
Plaintext
454 lines
18 KiB
Plaintext
/* HermesCustom — Lukas' additions to obsidian-web-viewer
|
||
*
|
||
* Loaded by vault.html via <script> tag (one-line patch).
|
||
*
|
||
* v0.5 — Mobile/Responsive overhaul:
|
||
* - Graph is NEVER a permanent bottom panel (waste of vertical space on phones)
|
||
* - Graph shown via full-screen modal toggle on Mobile + Tablet
|
||
* - Tree as hamburger on Mobile, full sidebar on Tablet
|
||
* - Desktop unchanged
|
||
*
|
||
* Companion: hermes-custom.css
|
||
*
|
||
* Layout decisions per viewport:
|
||
* Desktop (>=1025px): 3-panel, obv default, both toggles hidden
|
||
* Compact (601-1024px): 2-panel (tree+content), graph hidden by default,
|
||
* "Show Graph" button visible, "Tree" toggle visible
|
||
* Mobile (<=600px): 1-panel (content only), tree hamburger, graph modal
|
||
*/
|
||
(function() {
|
||
'use strict';
|
||
|
||
window.HermesCustom = window.HermesCustom || {};
|
||
const HermesCustom = window.HermesCustom;
|
||
|
||
// ============================================================
|
||
// Config
|
||
// ============================================================
|
||
|
||
const BREAKPOINTS = {
|
||
mobile: 600,
|
||
compact: 1024,
|
||
};
|
||
|
||
// ============================================================
|
||
// 1. UI elements (injected into header)
|
||
// ============================================================
|
||
|
||
/**
|
||
* Inject toggle buttons into header.
|
||
* - Tree toggle: only visible when tree is hidden (Compact mode hides tree by default)
|
||
* - Graph toggle: only visible when graph is hidden (Compact + Mobile)
|
||
* - Search input is already in obv's header; we don't duplicate
|
||
*/
|
||
HermesCustom.injectUI = function() {
|
||
const header = document.querySelector('.vault-header');
|
||
if (!header) {
|
||
setTimeout(HermesCustom.injectUI, 100);
|
||
return;
|
||
}
|
||
|
||
// Graph toggle button (top-right of header, before search)
|
||
if (!document.querySelector('.hermes-graph-toggle')) {
|
||
const graphBtn = document.createElement('button');
|
||
graphBtn.className = 'hermes-graph-toggle hermes-ui-btn';
|
||
graphBtn.setAttribute('aria-label', 'Toggle 3D graph');
|
||
graphBtn.innerHTML = '⊕ Graph';
|
||
graphBtn.onclick = () => HermesCustom.toggleGraphModal();
|
||
header.insertBefore(graphBtn, header.querySelector('.search-box') || header.lastChild);
|
||
}
|
||
|
||
// Tree toggle button (left-most in header)
|
||
if (!document.querySelector('.hermes-tree-toggle')) {
|
||
const treeBtn = document.createElement('button');
|
||
treeBtn.className = 'hermes-tree-toggle hermes-ui-btn';
|
||
treeBtn.setAttribute('aria-label', 'Toggle file tree');
|
||
treeBtn.innerHTML = '☰';
|
||
treeBtn.onclick = () => HermesCustom.toggleTreeSidebar();
|
||
header.insertBefore(treeBtn, header.firstChild);
|
||
}
|
||
|
||
// Backdrop for tree sidebar
|
||
if (!document.querySelector('.hermes-sidebar-backdrop')) {
|
||
const bd = document.createElement('div');
|
||
bd.className = 'hermes-sidebar-backdrop';
|
||
bd.onclick = () => HermesCustom.closeTreeSidebar();
|
||
document.body.appendChild(bd);
|
||
}
|
||
|
||
// Graph modal (hidden by default)
|
||
if (!document.querySelector('.hermes-graph-modal')) {
|
||
HermesCustom.buildGraphModal();
|
||
}
|
||
};
|
||
|
||
/**
|
||
* Build the graph modal: full-screen overlay that re-uses the
|
||
* existing canvas from obv. Strategy: clone the canvas into our modal,
|
||
* or move the existing canvas in/out. Simpler approach: create a NEW
|
||
* canvas inside the modal, and re-run the graph render there.
|
||
*/
|
||
HermesCustom.buildGraphModal = function() {
|
||
const modal = document.createElement('div');
|
||
modal.className = 'hermes-graph-modal';
|
||
|
||
const modalHeader = document.createElement('div');
|
||
modalHeader.className = 'hermes-graph-modal-header';
|
||
modalHeader.innerHTML = `
|
||
<span class="hermes-graph-modal-title">GRAPH VIEW · Tap a node to navigate</span>
|
||
<button class="hermes-graph-modal-close">× Close</button>
|
||
`;
|
||
modal.appendChild(modalHeader);
|
||
|
||
const canvasContainer = document.createElement('div');
|
||
canvasContainer.className = 'hermes-graph-modal-canvas';
|
||
canvasContainer.id = 'hermes-graph-modal-canvas';
|
||
modal.appendChild(canvasContainer);
|
||
|
||
document.body.appendChild(modal);
|
||
|
||
modal.querySelector('.hermes-graph-modal-close').onclick = () => {
|
||
HermesCustom.closeGraphModal();
|
||
};
|
||
// ESC to close
|
||
document.addEventListener('keydown', (e) => {
|
||
if (e.key === 'Escape' && modal.classList.contains('hermes-modal-open')) {
|
||
HermesCustom.closeGraphModal();
|
||
}
|
||
});
|
||
};
|
||
|
||
// ============================================================
|
||
// 2. Graph modal open/close
|
||
// ============================================================
|
||
|
||
HermesCustom.toggleGraphModal = function() {
|
||
const modal = document.querySelector('.hermes-graph-modal');
|
||
if (!modal) return;
|
||
if (modal.classList.contains('hermes-modal-open')) {
|
||
HermesCustom.closeGraphModal();
|
||
} else {
|
||
HermesCustom.openGraphModal();
|
||
}
|
||
};
|
||
|
||
HermesCustom.openGraphModal = function() {
|
||
const modal = document.querySelector('.hermes-graph-modal');
|
||
const canvasContainer = document.getElementById('hermes-graph-modal-canvas');
|
||
if (!modal || !canvasContainer) return;
|
||
|
||
// Move the existing obv canvas into our modal
|
||
const existingCanvas = document.querySelector('.vault-graph canvas');
|
||
if (existingCanvas && existingCanvas.parentElement !== canvasContainer) {
|
||
// Clone the canvas instead of moving it (keeps obv's state intact)
|
||
canvasContainer.innerHTML = '';
|
||
const clonedCanvas = document.createElement('canvas');
|
||
clonedCanvas.width = window.innerWidth;
|
||
clonedCanvas.height = window.innerHeight - 48 - 44; // viewport minus header+modal-header
|
||
canvasContainer.appendChild(clonedCanvas);
|
||
|
||
// Re-render the graph into our cloned canvas by re-running loadGraph logic
|
||
HermesCustom.renderGraphIntoCanvas(clonedCanvas);
|
||
}
|
||
|
||
modal.classList.add('hermes-modal-open');
|
||
document.body.style.overflow = 'hidden'; // prevent background scroll
|
||
console.log('[HermesCustom] Graph modal opened');
|
||
};
|
||
|
||
HermesCustom.closeGraphModal = function() {
|
||
const modal = document.querySelector('.hermes-graph-modal');
|
||
if (!modal) return;
|
||
modal.classList.remove('hermes-modal-open');
|
||
document.body.style.overflow = '';
|
||
console.log('[HermesCustom] Graph modal closed');
|
||
};
|
||
|
||
/**
|
||
* Re-render obv's graph into a different canvas.
|
||
* This is a hack: we re-fetch graph data and re-create a Three.js scene
|
||
* because obv's `loadGraph()` creates the scene with a specific canvas.
|
||
*
|
||
* Note: this duplicates Three.js scene. For mobile-only graph viewing,
|
||
* this is acceptable (small perf cost, but isolated to user action).
|
||
*/
|
||
HermesCustom.renderGraphIntoCanvas = function(canvas) {
|
||
if (!window.THREE || !HermesCustom._graphData) {
|
||
console.warn('[HermesCustom] Three.js or graph data not ready');
|
||
return;
|
||
}
|
||
|
||
const ctx = {
|
||
scene: new THREE.Scene(),
|
||
camera: null,
|
||
renderer: null,
|
||
nodeObjects: new Map(),
|
||
};
|
||
|
||
const w = canvas.clientWidth || canvas.width;
|
||
const h = canvas.clientHeight || canvas.height;
|
||
ctx.camera = new THREE.PerspectiveCamera(60, w / h, 1, 5000);
|
||
ctx.camera.position.set(0, 0, 800);
|
||
|
||
ctx.renderer = new THREE.WebGLRenderer({ canvas, antialias: true });
|
||
ctx.renderer.setSize(w, h);
|
||
ctx.renderer.setPixelRatio(window.devicePixelRatio || 1);
|
||
|
||
// Build nodes
|
||
const nodes = HermesCustom._graphData.nodes;
|
||
const nodeGeometry = new THREE.SphereGeometry(4, 16, 16);
|
||
const edges = HermesCustom._graphData.edges;
|
||
|
||
nodes.forEach(node => {
|
||
const mat = new THREE.MeshBasicMaterial({ color: 0x6c7086 });
|
||
const mesh = new THREE.Mesh(nodeGeometry, mat);
|
||
mesh.userData.nodeId = node.id;
|
||
// Random-ish positions (deterministic based on label hash)
|
||
const hash = node.label.split('').reduce((a, c) => a + c.charCodeAt(0), 0);
|
||
mesh.position.set(
|
||
(hash % 100) * 6 - 300,
|
||
((hash * 7) % 100) * 4 - 200,
|
||
((hash * 13) % 100) * 4 - 200
|
||
);
|
||
ctx.scene.add(mesh);
|
||
ctx.nodeObjects.set(node.id, mesh);
|
||
});
|
||
|
||
// Build edges
|
||
edges.forEach(edge => {
|
||
const src = ctx.nodeObjects.get(edge.source);
|
||
const tgt = ctx.nodeObjects.get(edge.target);
|
||
if (!src || !tgt) return;
|
||
const lineGeo = new THREE.BufferGeometry().setFromPoints([
|
||
src.position, tgt.position
|
||
]);
|
||
const line = new THREE.Line(lineGeo, new THREE.LineBasicMaterial({
|
||
color: 0x45475a, transparent: true, opacity: 0.6
|
||
}));
|
||
ctx.scene.add(line);
|
||
});
|
||
|
||
// Slow rotation
|
||
function animate() {
|
||
requestAnimationFrame(animate);
|
||
ctx.scene.rotation.y += 0.001;
|
||
ctx.renderer.render(ctx.scene, ctx.camera);
|
||
}
|
||
animate();
|
||
|
||
// Click handler — re-use the same navigation logic
|
||
HermesCustom.setupClickForCanvas(canvas, ctx.camera, ctx.nodeObjects);
|
||
};
|
||
|
||
HermesCustom.setupClickForCanvas = function(canvas, camera, nodeMap) {
|
||
const raycaster = new THREE.Raycaster();
|
||
const mouse = new THREE.Vector2();
|
||
canvas.addEventListener('click', (event) => {
|
||
const rect = canvas.getBoundingClientRect();
|
||
mouse.x = ((event.clientX - rect.left) / rect.width) * 2 - 1;
|
||
mouse.y = -((event.clientY - rect.top) / rect.height) * 2 + 1;
|
||
raycaster.setFromCamera(mouse, camera);
|
||
const intersects = raycaster.intersectObjects(Array.from(nodeMap.values()));
|
||
if (intersects.length > 0) {
|
||
const nodeId = intersects[0].object.userData.nodeId;
|
||
if (typeof window.loadFile === 'function') {
|
||
HermesCustom.closeGraphModal();
|
||
window.loadFile(nodeId);
|
||
window.history.pushState({}, '', '?file=' + encodeURIComponent(nodeId));
|
||
}
|
||
}
|
||
});
|
||
};
|
||
|
||
// ============================================================
|
||
// 3. Tree sidebar toggle
|
||
// ============================================================
|
||
|
||
HermesCustom.toggleTreeSidebar = function() {
|
||
const sidebar = document.querySelector('.vault-sidebar');
|
||
const backdrop = document.querySelector('.hermes-sidebar-backdrop');
|
||
if (!sidebar) return;
|
||
const opening = !sidebar.classList.contains('hermes-sidebar-open');
|
||
sidebar.classList.toggle('hermes-sidebar-open', opening);
|
||
if (backdrop) backdrop.classList.toggle('hermes-backdrop-visible', opening);
|
||
};
|
||
|
||
HermesCustom.closeTreeSidebar = function() {
|
||
const sidebar = document.querySelector('.vault-sidebar');
|
||
const backdrop = document.querySelector('.hermes-sidebar-backdrop');
|
||
if (sidebar) sidebar.classList.remove('hermes-sidebar-open');
|
||
if (backdrop) backdrop.classList.remove('hermes-backdrop-visible');
|
||
};
|
||
|
||
// ============================================================
|
||
// 4. Layout decision
|
||
// ============================================================
|
||
|
||
HermesCustom.applyLayout = function() {
|
||
const app = document.querySelector('.vault-app');
|
||
if (!app) {
|
||
setTimeout(HermesCustom.applyLayout, 100);
|
||
return;
|
||
}
|
||
|
||
// Marker class
|
||
if (!app.classList.contains('hermes-rc')) {
|
||
app.classList.add('hermes-rc');
|
||
}
|
||
|
||
const width = window.innerWidth;
|
||
app.classList.remove('hermes-mobile', 'hermes-compact', 'hermes-desktop');
|
||
|
||
if (width <= BREAKPOINTS.mobile) {
|
||
app.classList.add('hermes-mobile');
|
||
} else if (width <= BREAKPOINTS.compact) {
|
||
app.classList.add('hermes-compact');
|
||
} else {
|
||
app.classList.add('hermes-desktop');
|
||
}
|
||
|
||
// Auto-close sidebar on resize to desktop
|
||
if (width >= BREAKPOINTS.compact) {
|
||
HermesCustom.closeTreeSidebar();
|
||
}
|
||
};
|
||
|
||
HermesCustom.setupResizeHandler = function() {
|
||
let resizeTimer;
|
||
let lastWidth = window.innerWidth;
|
||
window.addEventListener('resize', () => {
|
||
clearTimeout(resizeTimer);
|
||
resizeTimer = setTimeout(() => {
|
||
if (window.innerWidth !== lastWidth) {
|
||
lastWidth = window.innerWidth;
|
||
HermesCustom.applyLayout();
|
||
}
|
||
}, 150);
|
||
});
|
||
};
|
||
|
||
// ============================================================
|
||
// 5. Current-page highlighting (desktop)
|
||
// ============================================================
|
||
|
||
HermesCustom.getCurrentFile = function() {
|
||
const params = new URLSearchParams(window.location.search);
|
||
const fileParam = params.get('file');
|
||
if (fileParam) return fileParam;
|
||
return HermesCustom._lastLoadedFile || null;
|
||
};
|
||
|
||
HermesCustom.getConnectedFiles = function(currentFile) {
|
||
if (!currentFile) return [];
|
||
const graphData = HermesCustom._graphData;
|
||
if (!graphData) return [];
|
||
return graphData.edges.filter(e => e.source === currentFile).map(e => e.target);
|
||
};
|
||
|
||
HermesCustom.highlightCurrentNode = function() {
|
||
const currentFile = HermesCustom.getCurrentFile();
|
||
if (!currentFile) return;
|
||
if (typeof window.graphNodeObjects === 'undefined' || !window.graphNodeObjects) {
|
||
setTimeout(HermesCustom.highlightCurrentNode, 500);
|
||
return;
|
||
}
|
||
|
||
const connected = new Set(HermesCustom.getConnectedFiles(currentFile));
|
||
connected.add(currentFile);
|
||
|
||
window.graphNodeObjects.forEach((mesh, nodeId) => {
|
||
const isCurrent = (nodeId === currentFile);
|
||
const isConnected = connected.has(nodeId) && !isCurrent;
|
||
|
||
if (isCurrent) {
|
||
mesh.material.color.setHex(0xFFD700);
|
||
mesh.material.emissive.setHex(0xFFD700);
|
||
mesh.material.emissiveIntensity = 0.6;
|
||
mesh.material.opacity = 1.0;
|
||
mesh.scale.set(1.5, 1.5, 1.5);
|
||
} else if (isConnected) {
|
||
mesh.material.color.setHex(0x89b4fa);
|
||
mesh.material.emissive.setHex(0x89b4fa);
|
||
mesh.material.emissiveIntensity = 0.3;
|
||
mesh.material.opacity = 1.0;
|
||
mesh.scale.set(1.1, 1.1, 1.1);
|
||
} else {
|
||
mesh.material.color.setHex(0x6c7086);
|
||
mesh.material.emissiveIntensity = 0;
|
||
mesh.material.opacity = 0.25;
|
||
mesh.material.transparent = true;
|
||
}
|
||
});
|
||
};
|
||
|
||
// ============================================================
|
||
// 6. URL navigation
|
||
// ============================================================
|
||
|
||
HermesCustom.updateURL = function(path) {
|
||
const newURL = '?file=' + encodeURIComponent(path);
|
||
window.history.pushState({}, '', newURL);
|
||
};
|
||
|
||
// ============================================================
|
||
// 7. loadFile hook
|
||
// ============================================================
|
||
|
||
HermesCustom.hookLoadFile = function() {
|
||
if (typeof window.loadFile !== 'function') {
|
||
setTimeout(HermesCustom.hookLoadFile, 200);
|
||
return;
|
||
}
|
||
if (window.loadFile._hermesPatched) return;
|
||
|
||
const originalLoadFile = window.loadFile;
|
||
window.loadFile = function(path) {
|
||
HermesCustom._lastLoadedFile = path;
|
||
HermesCustom.updateURL(path);
|
||
// Close tree sidebar after file selection (mobile UX)
|
||
if (window.innerWidth <= BREAKPOINTS.mobile) {
|
||
HermesCustom.closeTreeSidebar();
|
||
}
|
||
const result = originalLoadFile.apply(this, arguments);
|
||
setTimeout(HermesCustom.highlightCurrentNode, 300);
|
||
return result;
|
||
};
|
||
window.loadFile._hermesPatched = true;
|
||
};
|
||
|
||
// ============================================================
|
||
// 8. Graph data fetch
|
||
// ============================================================
|
||
|
||
HermesCustom.loadGraphData = function() {
|
||
fetch('/api/vault/graph')
|
||
.then(r => r.json())
|
||
.then(data => {
|
||
HermesCustom._graphData = data;
|
||
console.log('[HermesCustom] Graph data: ' + data.nodes.length + ' nodes, ' + data.edges.length + ' edges');
|
||
HermesCustom.highlightCurrentNode();
|
||
})
|
||
.catch(err => console.warn('[HermesCustom] Graph fetch failed:', err));
|
||
};
|
||
|
||
// ============================================================
|
||
// Init
|
||
// ============================================================
|
||
|
||
HermesCustom.init = function() {
|
||
console.log('[HermesCustom] init v5 (mobile-toggle-pattern)');
|
||
HermesCustom.injectUI();
|
||
HermesCustom.hookLoadFile();
|
||
HermesCustom.applyLayout();
|
||
HermesCustom.setupResizeHandler();
|
||
HermesCustom.loadGraphData();
|
||
};
|
||
|
||
if (document.readyState === 'loading') {
|
||
document.addEventListener('DOMContentLoaded', HermesCustom.init);
|
||
} else {
|
||
setTimeout(HermesCustom.init, 50);
|
||
}
|
||
|
||
})();
|