cleanup and refactor
This commit is contained in:
1 parent
5f9db690a4
commit
73da8fb849
10 files changed
+91
-97
No files matched your search
@@ -190,10 +190,8 @@ body::after {
|
|||||||
border: 2px solid var(--t-border);
|
border: 2px solid var(--t-border);
|
||||||
box-shadow: 0 2px 8px rgba(0,0,0,0.3);
|
box-shadow: 0 2px 8px rgba(0,0,0,0.3);
|
||||||
z-index: 2;
|
z-index: 2;
|
||||||
transition: width 0.25s cubic-bezier(0.34, 1.56, 0.64, 1), height 0.25s cubic-bezier(0.34, 1.56, 0.64, 1), margin 0.25s cubic-bezier(0.34, 1.56, 0.64, 1), border-color 0.2s ease, box-shadow 0.25s ease;
|
|
||||||
}
|
|
||||||
.card-thumb {
|
|
||||||
transform-origin: center center;
|
transform-origin: center center;
|
||||||
|
transition: width 0.25s cubic-bezier(0.34, 1.56, 0.64, 1), height 0.25s cubic-bezier(0.34, 1.56, 0.64, 1), margin 0.25s cubic-bezier(0.34, 1.56, 0.64, 1), border-color 0.2s ease, box-shadow 0.25s ease;
|
||||||
}
|
}
|
||||||
.card-thumb:hover {
|
.card-thumb:hover {
|
||||||
width: 80px;
|
width: 80px;
|
||||||
@@ -203,8 +201,6 @@ body::after {
|
|||||||
border-color: var(--t-muted);
|
border-color: var(--t-muted);
|
||||||
box-shadow: 0 4px 16px rgba(0,0,0,0.4);
|
box-shadow: 0 4px 16px rgba(0,0,0,0.4);
|
||||||
z-index: 10;
|
z-index: 10;
|
||||||
background-size: cover;
|
|
||||||
background-repeat: no-repeat;
|
|
||||||
}
|
}
|
||||||
.card-glow:hover { box-shadow: 0 0 16px -3px var(--glow-color, rgba(56, 189, 248, 0.15)); }
|
.card-glow:hover { box-shadow: 0 0 16px -3px var(--glow-color, rgba(56, 189, 248, 0.15)); }
|
||||||
.card-selected { outline: 2px solid var(--t-accent) !important; outline-offset: -2px; }
|
.card-selected { outline: 2px solid var(--t-accent) !important; outline-offset: -2px; }
|
||||||
|
|||||||
+9
-25
@@ -196,7 +196,6 @@ if (str_starts_with($route, '/api/auth')) {
|
|||||||
foreach ($reEncrypted as $ck => $val) $db->prepare("UPDATE settings SET value = :v WHERE key = :k")->execute(['v' => $val, 'k' => $ck]);
|
foreach ($reEncrypted as $ck => $val) $db->prepare("UPDATE settings SET value = :v WHERE key = :k")->execute(['v' => $val, 'k' => $ck]);
|
||||||
$db->prepare("UPDATE settings SET value = :h WHERE key = 'password_hash'")->execute(['h' => $newHash]);
|
$db->prepare("UPDATE settings SET value = :h WHERE key = 'password_hash'")->execute(['h' => $newHash]);
|
||||||
$db->commit();
|
$db->commit();
|
||||||
$db->exec("UPDATE cards SET caldav_etag = NULL, caldav_synced_at = NULL WHERE caldav_uid LIKE 'note:%'");
|
|
||||||
echo json_encode(['ok' => true, 'rekey' => true]);
|
echo json_encode(['ok' => true, 'rekey' => true]);
|
||||||
}
|
}
|
||||||
exit;
|
exit;
|
||||||
@@ -773,10 +772,6 @@ if (str_starts_with($route, '/api/')) {
|
|||||||
if ($route === '/api/caldav/sync' && $method === 'POST') {
|
if ($route === '/api/caldav/sync' && $method === 'POST') {
|
||||||
echo json_encode($caldav->sync()); exit;
|
echo json_encode($caldav->sync()); exit;
|
||||||
}
|
}
|
||||||
if ($route === '/api/caldav/log' && $method === 'GET') {
|
|
||||||
echo json_encode($db->query('SELECT * FROM caldav_sync_log ORDER BY id DESC LIMIT 50')->fetchAll()); exit;
|
|
||||||
}
|
|
||||||
|
|
||||||
// ── Relay ──
|
// ── Relay ──
|
||||||
|
|
||||||
if ($route === '/api/relay/settings' && $method === 'GET') {
|
if ($route === '/api/relay/settings' && $method === 'GET') {
|
||||||
@@ -814,6 +809,13 @@ if (str_starts_with($route, '/api/')) {
|
|||||||
|
|
||||||
$pulled = 0;
|
$pulled = 0;
|
||||||
$encKey = substr(hash('sha256', $relayKey), 0, 32);
|
$encKey = substr(hash('sha256', $relayKey), 0, 32);
|
||||||
|
$decryptField = function($text) use ($encKey) {
|
||||||
|
if (!str_starts_with($text, 'HYPRCRD-RELAY-V1')) return $text;
|
||||||
|
$raw = base64_decode(trim(explode("\n", $text, 2)[1] ?? ''));
|
||||||
|
if (strlen($raw) < 17) return $text;
|
||||||
|
$plain = openssl_decrypt(substr($raw, 16), 'aes-256-cbc', $encKey, OPENSSL_RAW_DATA, substr($raw, 0, 16));
|
||||||
|
return $plain !== false ? $plain : $text;
|
||||||
|
};
|
||||||
|
|
||||||
// Load IDs we pushed ourselves
|
// Load IDs we pushed ourselves
|
||||||
$ownIds = array_column($db->query('SELECT relay_msg_id FROM relay_pushed')->fetchAll(), 'relay_msg_id');
|
$ownIds = array_column($db->query('SELECT relay_msg_id FROM relay_pushed')->fetchAll(), 'relay_msg_id');
|
||||||
@@ -821,30 +823,12 @@ if (str_starts_with($route, '/api/')) {
|
|||||||
foreach ($messages as $msg) {
|
foreach ($messages as $msg) {
|
||||||
if ($msg['fetched'] ?? false) continue;
|
if ($msg['fetched'] ?? false) continue;
|
||||||
if (in_array($msg['id'] ?? 0, $ownIds)) {
|
if (in_array($msg['id'] ?? 0, $ownIds)) {
|
||||||
// Clean up tracking row since relay will delete it after 24h anyway
|
|
||||||
$db->prepare('DELETE FROM relay_pushed WHERE relay_msg_id = ?')->execute([$msg['id']]);
|
$db->prepare('DELETE FROM relay_pushed WHERE relay_msg_id = ?')->execute([$msg['id']]);
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Decrypt
|
$title = $decryptField($msg['title'] ?? '');
|
||||||
$title = $msg['title'] ?? '';
|
$body = $decryptField($msg['body'] ?? '');
|
||||||
$body = $msg['body'] ?? '';
|
|
||||||
if (str_starts_with($body, 'HYPRCRD-RELAY-V1')) {
|
|
||||||
$enc = trim(explode("\n", $body, 2)[1] ?? '');
|
|
||||||
$raw = base64_decode($enc);
|
|
||||||
if (strlen($raw) >= 17) {
|
|
||||||
$plain = openssl_decrypt(substr($raw, 16), 'aes-256-cbc', $encKey, OPENSSL_RAW_DATA, substr($raw, 0, 16));
|
|
||||||
if ($plain !== false) $body = $plain;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if (str_starts_with($title, 'HYPRCRD-RELAY-V1')) {
|
|
||||||
$enc = trim(explode("\n", $title, 2)[1] ?? '');
|
|
||||||
$raw = base64_decode($enc);
|
|
||||||
if (strlen($raw) >= 17) {
|
|
||||||
$plain = openssl_decrypt(substr($raw, 16), 'aes-256-cbc', $encKey, OPENSSL_RAW_DATA, substr($raw, 0, 16));
|
|
||||||
if ($plain !== false) $title = $plain;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!$title) $title = substr(strip_tags($body), 0, 60) ?: 'Relay message';
|
if (!$title) $title = substr(strip_tags($body), 0, 60) ?: 'Relay message';
|
||||||
$body = trim($body);
|
$body = trim($body);
|
||||||
|
|||||||
+2
-2
@@ -136,7 +136,7 @@ importFile?.addEventListener('change', async e => {
|
|||||||
const data = JSON.parse(await res.text());
|
const data = JSON.parse(await res.text());
|
||||||
if (data.error) throw new Error(data.error);
|
if (data.error) throw new Error(data.error);
|
||||||
await loadBuckets(); state.cards = await api('api/cards'); renderBoard();
|
await loadBuckets(); state.cards = await api('api/cards'); renderBoard();
|
||||||
} catch (err) { console.error('Import failed:', err); }
|
} catch {}
|
||||||
e.target.value = '';
|
e.target.value = '';
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -152,7 +152,7 @@ importLogseq?.addEventListener('change', async e => {
|
|||||||
if (data.error) throw new Error(data.error);
|
if (data.error) throw new Error(data.error);
|
||||||
await loadBuckets(); state.cards = await api('api/cards'); renderBoard();
|
await loadBuckets(); state.cards = await api('api/cards'); renderBoard();
|
||||||
await showDialog({ title: 'logseq import', message: `${data.imported} cards, ${data.assets} images imported`, okLabel: 'ok' });
|
await showDialog({ title: 'logseq import', message: `${data.imported} cards, ${data.assets} images imported`, okLabel: 'ok' });
|
||||||
} catch (err) { console.error('Logseq import failed:', err); }
|
} catch {}
|
||||||
e.target.value = '';
|
e.target.value = '';
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
+20
-23
@@ -2,10 +2,9 @@
|
|||||||
|
|
||||||
import { $, esc, api } from './util.js';
|
import { $, esc, api } from './util.js';
|
||||||
import { state, sortedCards, parseTags, tagChipHtml, getChildren, getRecent, getFavorites, isFavorite, SORT_MODES, SORT_ICONS, CARD_COLORS } from './state.js';
|
import { state, sortedCards, parseTags, tagChipHtml, getChildren, getRecent, getFavorites, isFavorite, SORT_MODES, SORT_ICONS, CARD_COLORS } from './state.js';
|
||||||
import { stripMd, getTodos, firstImage, leadingImage } from './markdown.js';
|
import { stripMd, getTodos, firstImage, leadingImage, renderMd, addCodeCopyButtons, formatTodoItems } from './markdown.js';
|
||||||
import { pushReorder, pushCardEdit, pushBatch, snapshotCard } from './undo.js';
|
import { pushReorder, pushCardEdit, pushBatch, snapshotCard } from './undo.js';
|
||||||
import { getCalConfig } from './caldav.js';
|
import { getCalConfig } from './caldav.js';
|
||||||
import { renderMd, addCodeCopyButtons, formatTodoItems } from './markdown.js';
|
|
||||||
import { addProvider as addNotifProvider, render as renderNotifs } from './notifications.js';
|
import { addProvider as addNotifProvider, render as renderNotifs } from './notifications.js';
|
||||||
import { bindCheckboxes, bindDetailImageFullscreen } from './detail.js';
|
import { bindCheckboxes, bindDetailImageFullscreen } from './detail.js';
|
||||||
|
|
||||||
@@ -22,13 +21,11 @@ const boardPreviewTitle = $('board-preview-title');
|
|||||||
const boardPreviewColor = $('board-preview-color');
|
const boardPreviewColor = $('board-preview-color');
|
||||||
const boardPreviewTags = $('board-preview-tags');
|
const boardPreviewTags = $('board-preview-tags');
|
||||||
const boardPreviewMeta = $('board-preview-meta');
|
const boardPreviewMeta = $('board-preview-meta');
|
||||||
let boardSplitOpen = localStorage.getItem('hyprcrd-board-split') === '1';
|
|
||||||
let boardPreviewCardId = null;
|
|
||||||
let activeDragEl = null;
|
let activeDragEl = null;
|
||||||
let dragGhost = null;
|
let dragGhost = null;
|
||||||
|
|
||||||
// ── Multi-select ──
|
// ── Multi-select ──
|
||||||
const selectedIds = new Set();
|
const selectedIds = state.selectedIds;
|
||||||
const selectBar = $('select-bar');
|
const selectBar = $('select-bar');
|
||||||
const selectCount = $('select-count');
|
const selectCount = $('select-count');
|
||||||
|
|
||||||
@@ -263,8 +260,8 @@ export function updateSortUI() {
|
|||||||
|
|
||||||
function showBoardPreview(cardId) {
|
function showBoardPreview(cardId) {
|
||||||
const card = state.cards.find(c => c.id == cardId);
|
const card = state.cards.find(c => c.id == cardId);
|
||||||
if (!card || !boardSplitOpen) return;
|
if (!card || !state.boardSplitOpen) return;
|
||||||
boardPreviewCardId = cardId;
|
state.boardPreviewCardId = cardId;
|
||||||
$('board-preview-empty')?.classList.add('hidden');
|
$('board-preview-empty')?.classList.add('hidden');
|
||||||
$('board-preview-content')?.classList.remove('hidden');
|
$('board-preview-content')?.classList.remove('hidden');
|
||||||
if (boardPreviewColor) boardPreviewColor.style.background = card.color;
|
if (boardPreviewColor) boardPreviewColor.style.background = card.color;
|
||||||
@@ -296,12 +293,12 @@ function showBoardPreview(cardId) {
|
|||||||
|
|
||||||
function closeBoardPreview() {
|
function closeBoardPreview() {
|
||||||
boardPreview?.classList.add('hidden');
|
boardPreview?.classList.add('hidden');
|
||||||
boardSplitOpen = false;
|
state.boardSplitOpen = false;
|
||||||
localStorage.setItem('hyprcrd-board-split', '0');
|
localStorage.setItem('hyprcrd-board-split', '0');
|
||||||
const btn = $('board-split-toggle');
|
const btn = $('board-split-toggle');
|
||||||
if (btn) btn.classList.remove('text-t-accent');
|
if (btn) btn.classList.remove('text-t-accent');
|
||||||
board.querySelectorAll('.board-card').forEach(el => el.classList.remove('preview-active'));
|
board.querySelectorAll('.board-card').forEach(el => el.classList.remove('preview-active'));
|
||||||
boardPreviewCardId = null;
|
state.boardPreviewCardId = null;
|
||||||
resetBoardPreview();
|
resetBoardPreview();
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -313,26 +310,26 @@ function resetBoardPreview() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function toggleBoardSplit() {
|
function toggleBoardSplit() {
|
||||||
boardSplitOpen = !boardSplitOpen;
|
state.boardSplitOpen = !state.boardSplitOpen;
|
||||||
localStorage.setItem('hyprcrd-board-split', boardSplitOpen ? '1' : '0');
|
localStorage.setItem('hyprcrd-board-split', state.boardSplitOpen ? '1' : '0');
|
||||||
boardPreview?.classList.toggle('hidden', !boardSplitOpen);
|
boardPreview?.classList.toggle('hidden', !state.boardSplitOpen);
|
||||||
const btn = $('board-split-toggle');
|
const btn = $('board-split-toggle');
|
||||||
if (btn) btn.classList.toggle('text-t-accent', boardSplitOpen);
|
if (btn) btn.classList.toggle('text-t-accent', state.boardSplitOpen);
|
||||||
if (!boardSplitOpen) {
|
if (!state.boardSplitOpen) {
|
||||||
board.querySelectorAll('.board-card').forEach(el => el.classList.remove('preview-active'));
|
board.querySelectorAll('.board-card').forEach(el => el.classList.remove('preview-active'));
|
||||||
boardPreviewCardId = null;
|
state.boardPreviewCardId = null;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
$('board-split-toggle')?.addEventListener('click', toggleBoardSplit);
|
$('board-split-toggle')?.addEventListener('click', toggleBoardSplit);
|
||||||
$('board-preview-close')?.addEventListener('click', closeBoardPreview);
|
$('board-preview-close')?.addEventListener('click', closeBoardPreview);
|
||||||
$('board-preview-edit')?.addEventListener('click', () => {
|
$('board-preview-edit')?.addEventListener('click', () => {
|
||||||
const card = state.cards.find(c => c.id == boardPreviewCardId);
|
const card = state.cards.find(c => c.id == state.boardPreviewCardId);
|
||||||
if (card) window._openModal(card);
|
if (card) window._openModal(card);
|
||||||
});
|
});
|
||||||
|
|
||||||
// Init split state
|
// Init split state
|
||||||
if (boardSplitOpen) {
|
if (state.boardSplitOpen) {
|
||||||
boardPreview?.classList.remove('hidden');
|
boardPreview?.classList.remove('hidden');
|
||||||
const btn = $('board-split-toggle');
|
const btn = $('board-split-toggle');
|
||||||
if (btn) btn.classList.add('text-t-accent');
|
if (btn) btn.classList.add('text-t-accent');
|
||||||
@@ -559,7 +556,7 @@ export function renderBoard() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
board.querySelectorAll('.superview-item[data-id]').forEach(el => {
|
board.querySelectorAll('.superview-item[data-id]').forEach(el => {
|
||||||
el.addEventListener('click', () => { clearHover(); if (boardSplitOpen) showBoardPreview(el.dataset.id); else window._openDetail(el.dataset.id); });
|
el.addEventListener('click', () => { clearHover(); if (state.boardSplitOpen) showBoardPreview(el.dataset.id); else window._openDetail(el.dataset.id); });
|
||||||
el.addEventListener('dblclick', () => { clearHover(); const card = state.cards.find(c => c.id == el.dataset.id); if (card) window._openModal(card); });
|
el.addEventListener('dblclick', () => { clearHover(); const card = state.cards.find(c => c.id == el.dataset.id); if (card) window._openModal(card); });
|
||||||
el.addEventListener('contextmenu', e => { e.preventDefault(); clearHover(); const card = state.cards.find(c => c.id == el.dataset.id); if (card) window._showContextMenu(e.clientX, e.clientY, card); });
|
el.addEventListener('contextmenu', e => { e.preventDefault(); clearHover(); const card = state.cards.find(c => c.id == el.dataset.id); if (card) window._showContextMenu(e.clientX, e.clientY, card); });
|
||||||
|
|
||||||
@@ -645,10 +642,10 @@ export function renderBoard() {
|
|||||||
if (fc) fc.textContent = `${cards.length} card${cards.length !== 1 ? 's' : ''} · ${state.cards.length} total`;
|
if (fc) fc.textContent = `${cards.length} card${cards.length !== 1 ? 's' : ''} · ${state.cards.length} total`;
|
||||||
|
|
||||||
// Refresh split preview if open
|
// Refresh split preview if open
|
||||||
if (boardSplitOpen && boardPreviewCardId) {
|
if (state.boardSplitOpen && state.boardPreviewCardId) {
|
||||||
const stillExists = state.cards.some(c => c.id == boardPreviewCardId);
|
const stillExists = state.cards.some(c => c.id == state.boardPreviewCardId);
|
||||||
if (stillExists) showBoardPreview(boardPreviewCardId);
|
if (stillExists) showBoardPreview(state.boardPreviewCardId);
|
||||||
else { boardPreviewCardId = null; resetBoardPreview(); }
|
else { state.boardPreviewCardId = null; resetBoardPreview(); }
|
||||||
}
|
}
|
||||||
|
|
||||||
// Notification banner
|
// Notification banner
|
||||||
@@ -912,7 +909,7 @@ function bindCardEvents() {
|
|||||||
if (clickTimer) return;
|
if (clickTimer) return;
|
||||||
clickTimer = setTimeout(() => {
|
clickTimer = setTimeout(() => {
|
||||||
clickTimer = null;
|
clickTimer = null;
|
||||||
if (boardSplitOpen) { showBoardPreview(id); }
|
if (state.boardSplitOpen) { showBoardPreview(id); }
|
||||||
else if (localStorage.getItem('hyprcrd-click-action') === 'edit') {
|
else if (localStorage.getItem('hyprcrd-click-action') === 'edit') {
|
||||||
const card = state.cards.find(c => c.id == id);
|
const card = state.cards.find(c => c.id == id);
|
||||||
if (card) window._openModal(card);
|
if (card) window._openModal(card);
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
// ── CalDAV Sync ──
|
// ── CalDAV Sync ──
|
||||||
|
|
||||||
import { $, esc, api, getCsrf } from './util.js';
|
import { $, esc, api, getCsrf } from './util.js';
|
||||||
|
import { state } from './state.js';
|
||||||
|
|
||||||
const CAL_ICONS = [
|
const CAL_ICONS = [
|
||||||
{ id: 'calendar-dots', label: 'calendar' },
|
{ id: 'calendar-dots', label: 'calendar' },
|
||||||
@@ -259,10 +260,9 @@ function showSyncNotification(stats) {
|
|||||||
setTimeout(() => { note.style.transition = 'opacity 0.3s'; note.style.opacity = '0'; setTimeout(() => note.remove(), 350); }, 4000);
|
setTimeout(() => { note.style.transition = 'opacity 0.3s'; note.style.opacity = '0'; setTimeout(() => note.remove(), 350); }, 4000);
|
||||||
}
|
}
|
||||||
|
|
||||||
let bgSyncRunning = false;
|
|
||||||
async function backgroundSync() {
|
async function backgroundSync() {
|
||||||
if (bgSyncRunning) return;
|
if (state.bgSyncRunning) return;
|
||||||
bgSyncRunning = true;
|
state.bgSyncRunning = true;
|
||||||
try {
|
try {
|
||||||
const headers = { 'X-CSRF-Token': getCsrf() };
|
const headers = { 'X-CSRF-Token': getCsrf() };
|
||||||
const [res, relayRes] = await Promise.all([
|
const [res, relayRes] = await Promise.all([
|
||||||
@@ -277,7 +277,7 @@ async function backgroundSync() {
|
|||||||
showSyncNotification(total);
|
showSyncNotification(total);
|
||||||
}
|
}
|
||||||
} catch {} finally {
|
} catch {} finally {
|
||||||
bgSyncRunning = false;
|
state.bgSyncRunning = false;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -12,7 +12,7 @@ const title = $('cal-title');
|
|||||||
const DAYS = ['Mo', 'Tu', 'We', 'Th', 'Fr', 'Sa', 'Su'];
|
const DAYS = ['Mo', 'Tu', 'We', 'Th', 'Fr', 'Sa', 'Su'];
|
||||||
const MONTHS = ['January', 'February', 'March', 'April', 'May', 'June', 'July', 'August', 'September', 'October', 'November', 'December'];
|
const MONTHS = ['January', 'February', 'March', 'April', 'May', 'June', 'July', 'August', 'September', 'October', 'November', 'December'];
|
||||||
|
|
||||||
let viewYear, viewMonth; // 0-indexed month
|
// Calendar view year/month stored in state.calYear/state.calMonth
|
||||||
|
|
||||||
function pad(n) { return String(n).padStart(2, '0'); }
|
function pad(n) { return String(n).padStart(2, '0'); }
|
||||||
|
|
||||||
@@ -146,12 +146,12 @@ function clearCalHover() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function render() {
|
function render() {
|
||||||
title.textContent = `${MONTHS[viewMonth]} ${viewYear}`;
|
title.textContent = `${MONTHS[state.calMonth]} ${state.calYear}`;
|
||||||
|
|
||||||
const firstDay = new Date(viewYear, viewMonth, 1).getDay(); // 0=Sun
|
const firstDay = new Date(state.calYear, state.calMonth, 1).getDay(); // 0=Sun
|
||||||
const startOffset = (firstDay + 6) % 7; // Mon=0
|
const startOffset = (firstDay + 6) % 7; // Mon=0
|
||||||
const daysInMonth = new Date(viewYear, viewMonth + 1, 0).getDate();
|
const daysInMonth = new Date(state.calYear, state.calMonth + 1, 0).getDate();
|
||||||
const prevMonthDays = new Date(viewYear, viewMonth, 0).getDate();
|
const prevMonthDays = new Date(state.calYear, state.calMonth, 0).getDate();
|
||||||
const today = new Date();
|
const today = new Date();
|
||||||
const todayStr = dateStr(today.getFullYear(), today.getMonth(), today.getDate());
|
const todayStr = dateStr(today.getFullYear(), today.getMonth(), today.getDate());
|
||||||
|
|
||||||
@@ -169,17 +169,17 @@ function render() {
|
|||||||
if (dayNum < 1) {
|
if (dayNum < 1) {
|
||||||
// Previous month
|
// Previous month
|
||||||
displayNum = prevMonthDays + dayNum;
|
displayNum = prevMonthDays + dayNum;
|
||||||
const pm = viewMonth === 0 ? 11 : viewMonth - 1;
|
const pm = state.calMonth === 0 ? 11 : state.calMonth - 1;
|
||||||
const py = viewMonth === 0 ? viewYear - 1 : viewYear;
|
const py = state.calMonth === 0 ? state.calYear - 1 : state.calYear;
|
||||||
ds = dateStr(py, pm, displayNum);
|
ds = dateStr(py, pm, displayNum);
|
||||||
} else if (dayNum > daysInMonth) {
|
} else if (dayNum > daysInMonth) {
|
||||||
// Next month
|
// Next month
|
||||||
displayNum = dayNum - daysInMonth;
|
displayNum = dayNum - daysInMonth;
|
||||||
const nm = viewMonth === 11 ? 0 : viewMonth + 1;
|
const nm = state.calMonth === 11 ? 0 : state.calMonth + 1;
|
||||||
const ny = viewMonth === 11 ? viewYear + 1 : viewYear;
|
const ny = state.calMonth === 11 ? state.calYear + 1 : state.calYear;
|
||||||
ds = dateStr(ny, nm, displayNum);
|
ds = dateStr(ny, nm, displayNum);
|
||||||
} else {
|
} else {
|
||||||
ds = dateStr(viewYear, viewMonth, dayNum);
|
ds = dateStr(state.calYear, state.calMonth, dayNum);
|
||||||
}
|
}
|
||||||
const isToday = ds === todayStr;
|
const isToday = ds === todayStr;
|
||||||
const cards = getCardsForDate(ds);
|
const cards = getCardsForDate(ds);
|
||||||
@@ -317,9 +317,9 @@ function render() {
|
|||||||
|
|
||||||
function navigate(delta) {
|
function navigate(delta) {
|
||||||
closeDayPopup();
|
closeDayPopup();
|
||||||
viewMonth += delta;
|
state.calMonth += delta;
|
||||||
if (viewMonth > 11) { viewMonth = 0; viewYear++; }
|
if (state.calMonth > 11) { state.calMonth = 0; state.calYear++; }
|
||||||
if (viewMonth < 0) { viewMonth = 11; viewYear--; }
|
if (state.calMonth < 0) { state.calMonth = 11; state.calYear--; }
|
||||||
render();
|
render();
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -495,8 +495,8 @@ export function refreshCalendar() {
|
|||||||
|
|
||||||
export function openCalendar() {
|
export function openCalendar() {
|
||||||
const now = new Date();
|
const now = new Date();
|
||||||
viewYear = now.getFullYear();
|
state.calYear = now.getFullYear();
|
||||||
viewMonth = now.getMonth();
|
state.calMonth = now.getMonth();
|
||||||
render();
|
render();
|
||||||
overlay.classList.add('open');
|
overlay.classList.add('open');
|
||||||
overlay.style.visibility = '';
|
overlay.style.visibility = '';
|
||||||
@@ -514,8 +514,8 @@ $('cal-prev')?.addEventListener('click', () => navigate(-1));
|
|||||||
$('cal-next')?.addEventListener('click', () => navigate(1));
|
$('cal-next')?.addEventListener('click', () => navigate(1));
|
||||||
$('cal-today')?.addEventListener('click', () => {
|
$('cal-today')?.addEventListener('click', () => {
|
||||||
const now = new Date();
|
const now = new Date();
|
||||||
viewYear = now.getFullYear();
|
state.calYear = now.getFullYear();
|
||||||
viewMonth = now.getMonth();
|
state.calMonth = now.getMonth();
|
||||||
render();
|
render();
|
||||||
});
|
});
|
||||||
overlay?.addEventListener('click', e => { if (e.target === overlay) closeCalendar(); });
|
overlay?.addEventListener('click', e => { if (e.target === overlay) closeCalendar(); });
|
||||||
@@ -29,25 +29,24 @@ const FS_MAX_SCALE = 8;
|
|||||||
// ── Zoom ──
|
// ── Zoom ──
|
||||||
|
|
||||||
const ZOOM_KEY = 'hyprcrd-detail-zoom';
|
const ZOOM_KEY = 'hyprcrd-detail-zoom';
|
||||||
let zoom = parseFloat(localStorage.getItem(ZOOM_KEY) || '1') || 1;
|
|
||||||
|
|
||||||
function applyZoom() {
|
function applyZoom() {
|
||||||
if (!titleEl) return;
|
if (!titleEl) return;
|
||||||
titleEl.style.fontSize = `${(1.125 * zoom).toFixed(3)}rem`;
|
titleEl.style.fontSize = `${(1.125 * state.zoom).toFixed(3)}rem`;
|
||||||
bodyEl.style.fontSize = `${(0.875 * zoom).toFixed(3)}rem`;
|
bodyEl.style.fontSize = `${(0.875 * state.zoom).toFixed(3)}rem`;
|
||||||
metaEl.style.fontSize = `${Math.max(0.625, 0.6875 * zoom).toFixed(3)}rem`;
|
metaEl.style.fontSize = `${Math.max(0.625, 0.6875 * state.zoom).toFixed(3)}rem`;
|
||||||
titleEl.style.lineHeight = `${Math.max(1.35, 1.55 + (zoom - 1) * 0.3).toFixed(3)}`;
|
titleEl.style.lineHeight = `${Math.max(1.35, 1.55 + (state.zoom - 1) * 0.3).toFixed(3)}`;
|
||||||
bodyEl.style.lineHeight = `${Math.max(1.5, 1.7 + (zoom - 1) * 0.35).toFixed(3)}`;
|
bodyEl.style.lineHeight = `${Math.max(1.5, 1.7 + (state.zoom - 1) * 0.35).toFixed(3)}`;
|
||||||
metaEl.style.lineHeight = `${Math.max(1.2, 1.35 + (zoom - 1) * 0.25).toFixed(3)}`;
|
metaEl.style.lineHeight = `${Math.max(1.2, 1.35 + (state.zoom - 1) * 0.25).toFixed(3)}`;
|
||||||
}
|
}
|
||||||
applyZoom();
|
applyZoom();
|
||||||
|
|
||||||
scroll?.addEventListener('wheel', e => {
|
scroll?.addEventListener('wheel', e => {
|
||||||
if (!detail.classList.contains('open') || !e.shiftKey || Math.abs(e.deltaY) < Math.abs(e.deltaX)) return;
|
if (!detail.classList.contains('open') || !e.shiftKey || Math.abs(e.deltaY) < Math.abs(e.deltaX)) return;
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
zoom = Math.min(2.2, Math.max(0.75, zoom + (e.deltaY < 0 ? 0.08 : -0.08)));
|
state.zoom = Math.min(2.2, Math.max(0.75, state.zoom + (e.deltaY < 0 ? 0.08 : -0.08)));
|
||||||
zoom = Math.round(zoom * 100) / 100;
|
state.zoom = Math.round(state.zoom * 100) / 100;
|
||||||
localStorage.setItem(ZOOM_KEY, String(zoom));
|
localStorage.setItem(ZOOM_KEY, String(state.zoom));
|
||||||
applyZoom();
|
applyZoom();
|
||||||
}, { passive: false });
|
}, { passive: false });
|
||||||
|
|
||||||
|
|||||||
@@ -1,11 +1,9 @@
|
|||||||
// ── Editor (card create/edit) ──
|
// ── Editor (card create/edit) ──
|
||||||
|
|
||||||
import { $, api, escRe, getCsrf } from './util.js';
|
import { $, esc, api, escRe, getCsrf, fuzzyMatch } from './util.js';
|
||||||
import { state, getChildren, CARD_COLORS } from './state.js';
|
import { state, getChildren, CARD_COLORS } from './state.js';
|
||||||
import { renderMd, addCodeCopyButtons, formatTodoItems } from './markdown.js';
|
import { renderMd, addCodeCopyButtons, formatTodoItems } from './markdown.js';
|
||||||
import { pushCardEdit, snapshotCard } from './undo.js';
|
import { pushCardEdit, snapshotCard } from './undo.js';
|
||||||
import { fuzzyMatch } from './util.js';
|
|
||||||
import { esc } from './util.js';
|
|
||||||
|
|
||||||
const modal = $('modal');
|
const modal = $('modal');
|
||||||
const body = $('editor-body');
|
const body = $('editor-body');
|
||||||
@@ -975,7 +973,6 @@ async function uploadFile(file) {
|
|||||||
try {
|
try {
|
||||||
const res = await fetch('api/upload', { method: 'POST', body: form, headers: { 'X-CSRF-Token': getCsrf() } });
|
const res = await fetch('api/upload', { method: 'POST', body: form, headers: { 'X-CSRF-Token': getCsrf() } });
|
||||||
const text = await res.text();
|
const text = await res.text();
|
||||||
console.log('Upload response:', res.status, text.substring(0, 300));
|
|
||||||
const data = JSON.parse(text);
|
const data = JSON.parse(text);
|
||||||
if (data.error) throw new Error(data.error);
|
if (data.error) throw new Error(data.error);
|
||||||
if (data.url) {
|
if (data.url) {
|
||||||
@@ -988,8 +985,7 @@ async function uploadFile(file) {
|
|||||||
body.selectionStart = body.selectionEnd = idx + md.length;
|
body.selectionStart = body.selectionEnd = idx + md.length;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
} catch (err) {
|
} catch {
|
||||||
console.error('Upload failed:', err);
|
|
||||||
const idx = body.value.indexOf(placeholder);
|
const idx = body.value.indexOf(placeholder);
|
||||||
if (idx !== -1) {
|
if (idx !== -1) {
|
||||||
body.value = body.value.substring(0, idx) + body.value.substring(idx + placeholder.length);
|
body.value = body.value.substring(0, idx) + body.value.substring(idx + placeholder.length);
|
||||||
|
|||||||
@@ -3,22 +3,44 @@
|
|||||||
import { esc } from './util.js';
|
import { esc } from './util.js';
|
||||||
|
|
||||||
export const state = {
|
export const state = {
|
||||||
|
// Data
|
||||||
cards: [],
|
cards: [],
|
||||||
buckets: [],
|
buckets: [],
|
||||||
|
|
||||||
|
// Filters & view
|
||||||
activeBucket: 'all',
|
activeBucket: 'all',
|
||||||
activeTags: [],
|
activeTags: [],
|
||||||
|
activeGroup: null,
|
||||||
|
sortMode: localStorage.getItem('hyprcrd-sort') || 'manual',
|
||||||
hideTermine: localStorage.getItem('hyprcrd-hide-termine') === '1',
|
hideTermine: localStorage.getItem('hyprcrd-hide-termine') === '1',
|
||||||
hideDone: localStorage.getItem('hyprcrd-hide-done') === '1',
|
hideDone: localStorage.getItem('hyprcrd-hide-done') === '1',
|
||||||
superView: localStorage.getItem('hyprcrd-superview') === '1',
|
superView: localStorage.getItem('hyprcrd-superview') === '1',
|
||||||
activeGroup: null,
|
|
||||||
sortMode: localStorage.getItem('hyprcrd-sort') || 'manual',
|
// Board split preview
|
||||||
|
boardSplitOpen: localStorage.getItem('hyprcrd-board-split') === '1',
|
||||||
|
boardPreviewCardId: null,
|
||||||
|
|
||||||
|
// Multi-select
|
||||||
|
selectedIds: new Set(),
|
||||||
|
|
||||||
|
// Detail / editor
|
||||||
editingId: null,
|
editingId: null,
|
||||||
viewingCard: null,
|
viewingCard: null,
|
||||||
|
zoom: parseFloat(localStorage.getItem('hyprcrd-detail-zoom') || '1') || 1,
|
||||||
|
|
||||||
|
// Navigation context
|
||||||
openedFromSearch: false,
|
openedFromSearch: false,
|
||||||
openedFromGallery: false,
|
openedFromGallery: false,
|
||||||
galleryOrigin: null,
|
galleryOrigin: null,
|
||||||
returnToGalleryAfterEdit: false,
|
returnToGalleryAfterEdit: false,
|
||||||
previewVisible: false,
|
previewVisible: false,
|
||||||
|
|
||||||
|
// Calendar
|
||||||
|
calYear: new Date().getFullYear(),
|
||||||
|
calMonth: new Date().getMonth(),
|
||||||
|
|
||||||
|
// Sync
|
||||||
|
bgSyncRunning: false,
|
||||||
};
|
};
|
||||||
|
|
||||||
export const SORT_MODES = ['manual', 'newest', 'oldest', 'alpha', 'tag'];
|
export const SORT_MODES = ['manual', 'newest', 'oldest', 'alpha', 'tag'];
|
||||||
|
|||||||
+3
-3
@@ -406,7 +406,7 @@ class CalDavSync
|
|||||||
$this->addLog('sync', null, null, 'fetch', 'ok', "total remote: " . count($remoteMap) . " events");
|
$this->addLog('sync', null, null, 'fetch', 'ok', "total remote: " . count($remoteMap) . " events");
|
||||||
|
|
||||||
// 3. Fetch local appointment cards — only #termin tagged or already synced
|
// 3. Fetch local appointment cards — only #termin tagged or already synced
|
||||||
$stmt = $this->db->query("SELECT * FROM cards WHERE tags = 'termin' OR tags LIKE 'termin,%' OR tags LIKE '%,termin,%' OR tags LIKE '%,termin' OR tags = 'geburtstag' OR tags LIKE 'geburtstag,%' OR tags LIKE '%,geburtstag,%' OR tags LIKE '%,geburtstag' OR (caldav_uid IS NOT NULL AND caldav_uid NOT LIKE 'note:%')");
|
$stmt = $this->db->query("SELECT * FROM cards WHERE tags = 'termin' OR tags LIKE 'termin,%' OR tags LIKE '%,termin,%' OR tags LIKE '%,termin' OR tags = 'geburtstag' OR tags LIKE 'geburtstag,%' OR tags LIKE '%,geburtstag,%' OR tags LIKE '%,geburtstag' OR caldav_uid IS NOT NULL");
|
||||||
$localCards = $stmt->fetchAll();
|
$localCards = $stmt->fetchAll();
|
||||||
$localMap = []; // caldav_uid -> card
|
$localMap = []; // caldav_uid -> card
|
||||||
$localNew = []; // cards without caldav_uid
|
$localNew = []; // cards without caldav_uid
|
||||||
@@ -418,9 +418,9 @@ class CalDavSync
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// 4. Tombstones (only CalDAV, not notes)
|
// 4. Tombstones
|
||||||
$tombstones = [];
|
$tombstones = [];
|
||||||
foreach ($this->db->query("SELECT * FROM caldav_tombstones WHERE caldav_uid NOT LIKE 'note:%'")->fetchAll() as $t) {
|
foreach ($this->db->query("SELECT * FROM caldav_tombstones")->fetchAll() as $t) {
|
||||||
$tombstones[$t['caldav_uid']] = $t;
|
$tombstones[$t['caldav_uid']] = $t;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
Reference in new issue
Block a user