mirror of
https://github.com/pewdiepie-archdaemon/odysseus.git
synced 2026-10-09 00:12:21 +02:00
Squash Odysseus development history
This commit is contained in:
+250
-57
@@ -3,12 +3,12 @@
|
||||
* Renders as a sidebar panel (like document editor), not a modal.
|
||||
*/
|
||||
|
||||
import uiModule from './ui.js';
|
||||
import { spawnConfetti } from './compare/vote.js';
|
||||
import uiModule from './ui.js?v=20260908weekhoverfix1';
|
||||
import { spawnConfetti } from './compare/vote.js?v=20260828resendcaldrag1';
|
||||
import * as Modals from './modalManager.js';
|
||||
import { attachColorPicker } from './colorPicker.js';
|
||||
import { attachColorPicker } from './colorPicker.js?v=20260910eyedropper1';
|
||||
import { makeWindowDraggable } from './windowDrag.js';
|
||||
import { snapModalToZone } from './tileManager.js';
|
||||
import { snapModalToZone } from './tileManager.js?v=20260910responsivebounds1';
|
||||
import { applyEdgeDock, clearDockSide } from './modalSnap.js';
|
||||
import { topToolWindowZ, topPortalZ } from './toolWindowZOrder.js';
|
||||
import { bindMenuDismiss, dismissOrRemove } from './escMenuStack.js';
|
||||
@@ -28,6 +28,7 @@ let _searchQuery = '';
|
||||
let _viewMode = (typeof localStorage !== 'undefined' && localStorage.getItem('odysseus-notes-view')) || 'list'; // 'list' or 'grid'
|
||||
let _showingArchived = false;
|
||||
let _selectMode = false;
|
||||
let _suppressNextNoteTapId = null;
|
||||
let _reminderTimer = null;
|
||||
// Tracks the global keydown listener so closePanel can remove it
|
||||
// (previously leaked one per openPanel; on multi-open sessions this
|
||||
@@ -48,6 +49,8 @@ const REMINDER_ACTIVE_HIGHLIGHT_KEY = 'odysseus-notes-reminder-active-highlight'
|
||||
// the rail "fired" badge so old reminders don't re-fire on every page reload.
|
||||
const REMINDER_DISMISSED_AT_KEY = 'odysseus-notes-reminder-dismissed-at';
|
||||
const NOTES_FIRST_OPEN_HINT_KEY = 'odysseus-notes-first-open-hint-v1';
|
||||
const NOTES_LABELS_COLLAPSED_KEY = 'odysseus-notes-labels-collapsed';
|
||||
let _labelsCollapsed = (typeof localStorage !== 'undefined' && localStorage.getItem(NOTES_LABELS_COLLAPSED_KEY) === '1');
|
||||
|
||||
function _forceCloseNotesPanel() {
|
||||
_open = false;
|
||||
@@ -237,6 +240,15 @@ function _loadActiveHighlights() {
|
||||
function _saveActiveHighlights(set) {
|
||||
try { localStorage.setItem(REMINDER_ACTIVE_HIGHLIGHT_KEY, JSON.stringify([...set])); } catch {}
|
||||
}
|
||||
|
||||
function _isCalendarReminderNote(n) {
|
||||
return n && n.source === 'calendar' && n.label === 'calendar';
|
||||
}
|
||||
|
||||
function _userVisibleNotes() {
|
||||
return _notes.filter(n => !_isCalendarReminderNote(n));
|
||||
}
|
||||
|
||||
function _clearViewedReminderGlows() {
|
||||
const active = _loadActiveHighlights();
|
||||
if (!active.size) return;
|
||||
@@ -302,8 +314,7 @@ function _flushPendingHighlights() {
|
||||
// Bring the first one into view so it can't get buried below the fold.
|
||||
if (firstCard) {
|
||||
requestAnimationFrame(() => {
|
||||
try { firstCard.scrollIntoView({ block: 'center', behavior: 'smooth' }); }
|
||||
catch { firstCard.scrollIntoView(); }
|
||||
_smoothScrollToNoteCard(firstCard);
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -386,6 +397,15 @@ const COLOR_HEX = {
|
||||
purple: '#e2bcee',
|
||||
};
|
||||
|
||||
// Plain notes get a stable visual variant derived from their identity. This
|
||||
// adds recognition without changing a color the user explicitly selected.
|
||||
function _standardVisualVariant(note) {
|
||||
const source = String(note?.id || note?.title || note?.created_at || 'standard-note');
|
||||
let hash = 0;
|
||||
for (let i = 0; i < source.length; i++) hash = ((hash << 5) - hash + source.charCodeAt(i)) | 0;
|
||||
return Math.abs(hash) % 5;
|
||||
}
|
||||
|
||||
// ---- API ----
|
||||
|
||||
let _loading = false;
|
||||
@@ -1103,7 +1123,8 @@ function _updateRailBadge() {
|
||||
if (!dot) {
|
||||
dot = document.createElement('span');
|
||||
dot.className = 'tool-notes-dot';
|
||||
sidebarBtn.appendChild(dot);
|
||||
const newNoteBtn = sidebarBtn.querySelector('#notes-new-note-btn');
|
||||
sidebarBtn.insertBefore(dot, newNoteBtn || null);
|
||||
}
|
||||
} else if (dot) {
|
||||
dot.remove();
|
||||
@@ -1147,6 +1168,14 @@ export function openPanel() {
|
||||
_bringNotesToFront();
|
||||
return;
|
||||
}
|
||||
// Notes and the document editor occupy the same primary workspace. Preserve
|
||||
// an open document as a dock chip instead of allowing the two panes to stack
|
||||
// into a broken layout (or closing the user's document outright).
|
||||
try {
|
||||
if (window.documentModule?.isPanelOpen?.()) {
|
||||
window.documentModule.closePanel('down');
|
||||
}
|
||||
} catch (_) {}
|
||||
_open = true;
|
||||
_editingId = null;
|
||||
_searchQuery = '';
|
||||
@@ -1209,6 +1238,9 @@ export function openPanel() {
|
||||
<button id="notes-bulk-delete" class="memory-toolbar-btn danger" disabled>
|
||||
<svg width="12" height="12" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" style="vertical-align:-2px;margin-right:4px;"><polyline points="3 6 5 6 21 6"/><path d="M19 6l-1 14a2 2 0 0 1-2 2H8a2 2 0 0 1-2-2L5 6"/><path d="M8 6V4a2 2 0 0 1 2-2h4a2 2 0 0 1 2 2v2"/><line x1="10" y1="11" x2="10" y2="17"/><line x1="14" y1="11" x2="14" y2="17"/></svg>Delete
|
||||
</button>
|
||||
<button id="notes-bulk-cancel" class="memory-toolbar-btn active" title="Cancel selection" aria-label="Cancel selection">
|
||||
<svg width="12" height="12" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5" stroke-linecap="round" stroke-linejoin="round"><line x1="18" y1="6" x2="6" y2="18"/><line x1="6" y1="6" x2="18" y2="18"/></svg>
|
||||
</button>
|
||||
</div>
|
||||
<div class="notes-pane-body"></div>
|
||||
`;
|
||||
@@ -1371,6 +1403,7 @@ export function openPanel() {
|
||||
_renderNotes();
|
||||
uiModule.showToast(`Deleted ${ids.length}`);
|
||||
});
|
||||
document.getElementById('notes-bulk-cancel').addEventListener('click', _exitSelectMode);
|
||||
// Escape: exit select mode first (if active), otherwise close the panel.
|
||||
// Skip when the user is editing a form field — those have their own
|
||||
// ESC-to-cancel handlers and we don't want to nuke the whole panel
|
||||
@@ -1453,9 +1486,10 @@ function _renderLoadingSkeleton() {
|
||||
body.appendChild(skel);
|
||||
}
|
||||
|
||||
function _enterSelectMode() {
|
||||
function _enterSelectMode(initialId = null) {
|
||||
_selectMode = true;
|
||||
_selectedIds.clear();
|
||||
if (initialId) _selectedIds.add(initialId);
|
||||
const bar = document.getElementById('notes-bulk-bar');
|
||||
const btn = document.getElementById('notes-select-btn');
|
||||
if (bar) bar.classList.remove('hidden');
|
||||
@@ -1528,17 +1562,30 @@ async function _clearPastReminders() {
|
||||
function _renderLabels(root = document) {
|
||||
const bar = root.querySelector?.('.notes-labels-bar') || document.querySelector('.notes-labels-bar');
|
||||
if (!bar) return;
|
||||
if (_labelsCollapsed) {
|
||||
bar.innerHTML = '';
|
||||
bar.style.display = 'none';
|
||||
return;
|
||||
}
|
||||
const visibleNotes = _userVisibleNotes();
|
||||
const labels = new Set();
|
||||
for (const n of _notes) for (const t of _visibleNoteTags(n)) labels.add(t);
|
||||
for (const n of visibleNotes) for (const t of _visibleNoteTags(n)) labels.add(t);
|
||||
const sortedLabels = [...labels].sort();
|
||||
// Count active reminders (not archived, has datetime due_date)
|
||||
const reminderCount = _notes.filter(n => !n.archived && n.due_date && _hasTimeComponent(n.due_date)).length;
|
||||
const pastReminderCount = _notes.filter(n => !n.archived && _isPastReminder(n)).length;
|
||||
const defaultCount = _notes.filter(n => !n.archived && _visibleNoteTags(n).length === 0).length;
|
||||
const reminderCount = visibleNotes.filter(n => !n.archived && n.due_date && _hasTimeComponent(n.due_date)).length;
|
||||
const pastReminderCount = visibleNotes.filter(n => !n.archived && _isPastReminder(n)).length;
|
||||
const defaultCount = visibleNotes.filter(n => !n.archived && _visibleNoteTags(n).length === 0).length;
|
||||
// Active goals = non-archived goal notes. Today view lists pending steps
|
||||
// from each, so we surface the count next to the chip.
|
||||
const goalCount = _notes.filter(n => n.note_type === 'goal' && !n.archived).length;
|
||||
const todayCount = _notes.filter(n => n.note_type === 'goal' && !n.archived && _nextGoalStep(n)).length;
|
||||
const goalCount = visibleNotes.filter(n => n.note_type === 'goal' && !n.archived).length;
|
||||
const todayCount = visibleNotes.filter(n => n.note_type === 'goal' && !n.archived && _nextGoalStep(n)).length;
|
||||
const hasUsefulFilters = sortedLabels.length > 0 || reminderCount > 0 || goalCount > 0;
|
||||
const hasActiveFilter = _activeLabel !== null || _activeFilter !== null;
|
||||
if (!hasUsefulFilters && !hasActiveFilter) {
|
||||
bar.innerHTML = '';
|
||||
bar.style.display = 'none';
|
||||
return;
|
||||
}
|
||||
bar.style.display = '';
|
||||
const allActive = _activeLabel === null && _activeFilter === null;
|
||||
let html = `<button class="notes-label-chip${allActive ? ' active' : ''}" data-action="all">All</button>`;
|
||||
@@ -1606,7 +1653,27 @@ function _renderLabels(root = document) {
|
||||
|
||||
function _renderLabelsInto(_body) {
|
||||
if (!_body) return;
|
||||
let bar = _body.querySelector(':scope > .notes-labels-bar');
|
||||
const pane = _body.closest('.notes-pane');
|
||||
const toggleHost = pane?.querySelector('.notes-search-bar') || _body;
|
||||
let toggle = toggleHost.querySelector(':scope > .notes-label-toggle');
|
||||
if (!toggle) {
|
||||
toggle = document.createElement('button');
|
||||
toggle.type = 'button';
|
||||
toggle.className = 'notes-label-toggle cal-filter-toggle';
|
||||
toggle.addEventListener('click', () => {
|
||||
_labelsCollapsed = !_labelsCollapsed;
|
||||
try { localStorage.setItem(NOTES_LABELS_COLLAPSED_KEY, _labelsCollapsed ? '1' : '0'); } catch {}
|
||||
_renderNotes();
|
||||
});
|
||||
const selectButton = toggleHost.querySelector('#notes-select-btn');
|
||||
toggleHost.insertBefore(toggle, selectButton || null);
|
||||
}
|
||||
toggle.textContent = _labelsCollapsed ? '+ Tags' : '− Tags';
|
||||
toggle.title = _labelsCollapsed ? 'Show tags' : 'Hide tags';
|
||||
toggle.setAttribute('aria-expanded', _labelsCollapsed ? 'false' : 'true');
|
||||
|
||||
// The shared chip scroller wraps this row once it is mounted.
|
||||
let bar = _body.querySelector('.notes-labels-bar');
|
||||
if (!bar) {
|
||||
bar = document.createElement('div');
|
||||
bar.className = 'notes-labels-bar';
|
||||
@@ -1744,7 +1811,8 @@ function _renderNotes() {
|
||||
const prevPositions = _captureCardPositions();
|
||||
const activeReminderHighlights = _loadActiveHighlights();
|
||||
|
||||
let filtered = _activeLabel ? _notes.filter(n => _noteTags(n).includes(_activeLabel)) : _notes;
|
||||
const displayNotes = _userVisibleNotes();
|
||||
let filtered = _activeLabel ? displayNotes.filter(n => _noteTags(n).includes(_activeLabel)) : displayNotes;
|
||||
if (_activeFilter === 'reminders') {
|
||||
filtered = filtered.filter(n => n.due_date && _hasTimeComponent(n.due_date));
|
||||
} else if (_activeFilter === 'no-reminders') {
|
||||
@@ -1883,6 +1951,8 @@ function _renderNotes() {
|
||||
const isBg = _isBgImage(note.color);
|
||||
const cc = (note.color && !isBg) ? ' note-color-' + note.color : '';
|
||||
const cardStyle = isBg ? ` style="${_customColorStyle(note.color)}"` : '';
|
||||
const isStandard = !note.note_type || note.note_type === 'note';
|
||||
const visualClass = isStandard && !note.color ? ` note-standard note-standard-v${_standardVisualVariant(note)}` : '';
|
||||
const sel = _selectedIds.has(note.id) ? ' note-card-selected' : '';
|
||||
const reminderTagHtml = note.due_date && _hasTimeComponent(note.due_date)
|
||||
? `<div class="note-card-reminder${overdue ? ' overdue' : ''}">
|
||||
@@ -1901,7 +1971,8 @@ function _renderNotes() {
|
||||
Goal${_goalProgress(note)}
|
||||
</span>`
|
||||
: '';
|
||||
html += `<div class="note-card${note.pinned ? ' note-card-pinned' : ''}${cc}${sel}${goalClass}${reminderGlowClass}${_selectMode ? ' note-card-selectmode' : ''}" draggable="${(_selectMode || _isNotesMobileMode()) ? 'false' : 'true'}" data-note-id="${note.id}"${cardStyle}>
|
||||
const drawingClass = note.note_type === 'draw' ? ' note-card-drawing' : '';
|
||||
html += `<div class="note-card${note.pinned ? ' note-card-pinned' : ''}${cc}${visualClass}${sel}${goalClass}${drawingClass}${reminderGlowClass}${_selectMode ? ' note-card-selectmode' : ''}" draggable="${(_selectMode || _isNotesMobileMode()) ? 'false' : 'true'}" data-note-id="${note.id}"${cardStyle}>
|
||||
${_selectMode ? `<input type="checkbox" class="memory-select-cb note-card-cb" data-note-id="${note.id}" ${_selectedIds.has(note.id) ? 'checked' : ''} />` : ''}
|
||||
${goalPill}
|
||||
<button class="note-card-pin${note.pinned ? ' active' : ''}" data-note-id="${note.id}" title="${note.pinned ? 'Unpin' : 'Pin'}">
|
||||
@@ -2000,17 +2071,24 @@ function _applyMasonry(body) {
|
||||
if (_masonryObserver) { try { _masonryObserver.disconnect(); } catch {} _masonryObserver = null; }
|
||||
if (!isGrid) {
|
||||
// Clear any leftover inline spans so list view lays out normally.
|
||||
body.querySelectorAll('.note-card, .notes-labels-bar, .notes-quick-add, .note-form').forEach(c => { c.style.gridRowEnd = ''; });
|
||||
body.querySelectorAll('.note-card, .notes-label-toggle, .notes-labels-bar, .notes-quick-add, .note-form, .doclib-chip-scroll-frame:has(> .notes-labels-bar)').forEach(c => { c.style.gridRowEnd = ''; });
|
||||
return;
|
||||
}
|
||||
const ROW_PX = 4;
|
||||
const spanForHeight = (h) => Math.max(1, Math.ceil(h / ROW_PX));
|
||||
const recomputeFullRows = () => {
|
||||
const quickAdd = body.querySelector('.notes-quick-add');
|
||||
const labelsToggle = body.querySelector('.notes-label-toggle');
|
||||
const labelsBar = body.querySelector('.notes-labels-bar');
|
||||
if (labelsBar && getComputedStyle(labelsBar).display !== 'none') {
|
||||
const labelsLayout = labelsBar?.parentElement?.classList.contains('doclib-chip-scroll-frame')
|
||||
? labelsBar.parentElement
|
||||
: labelsBar;
|
||||
if (labelsToggle && getComputedStyle(labelsToggle).display !== 'none') {
|
||||
labelsToggle.style.gridRowEnd = `span ${Math.max(1, spanForHeight(labelsToggle.scrollHeight))}`;
|
||||
}
|
||||
if (labelsLayout && getComputedStyle(labelsLayout).display !== 'none') {
|
||||
const shave = isMobileGrid ? 4 : 0;
|
||||
labelsBar.style.gridRowEnd = `span ${Math.max(1, spanForHeight(labelsBar.scrollHeight) - shave)}`;
|
||||
labelsLayout.style.gridRowEnd = `span ${Math.max(1, spanForHeight(labelsLayout.scrollHeight) - shave)}`;
|
||||
}
|
||||
if (quickAdd) {
|
||||
const shave = isMobileGrid ? 4 : 0;
|
||||
@@ -2048,7 +2126,12 @@ function _applyMasonry(body) {
|
||||
if (fullRowsChanged) recomputeFullRows();
|
||||
});
|
||||
body.querySelectorAll('.note-card').forEach(c => _masonryObserver.observe(c));
|
||||
body.querySelectorAll('.notes-labels-bar, .notes-quick-add, .note-form').forEach(c => _masonryObserver.observe(c));
|
||||
body.querySelectorAll('.notes-label-toggle, .notes-quick-add, .note-form').forEach(c => _masonryObserver.observe(c));
|
||||
const labelsBar = body.querySelector('.notes-labels-bar');
|
||||
const labelsLayout = labelsBar?.parentElement?.classList.contains('doclib-chip-scroll-frame')
|
||||
? labelsBar.parentElement
|
||||
: labelsBar;
|
||||
if (labelsLayout) _masonryObserver.observe(labelsLayout);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2177,8 +2260,22 @@ function _renderQuickAdd(body) {
|
||||
}
|
||||
|
||||
function _bindCardEvents(body) {
|
||||
// Skip completed leading rows so the first unfinished checklist item is
|
||||
// visible when a note preview has its own scroll area.
|
||||
requestAnimationFrame(() => {
|
||||
body.querySelectorAll('.note-checklist-preview').forEach(preview => {
|
||||
const firstPending = preview.querySelector('.note-checkbox:not(.done)');
|
||||
if (firstPending) preview.scrollTop = Math.max(0, firstPending.offsetTop - 4);
|
||||
});
|
||||
});
|
||||
const tapToEditOrSelect = (cardEl) => {
|
||||
const id = cardEl.dataset.noteId;
|
||||
// A long-press enters select mode and re-renders the card. Ignore the
|
||||
// synthetic click that mobile browsers emit when that same touch ends.
|
||||
if (_suppressNextNoteTapId === id) {
|
||||
_suppressNextNoteTapId = null;
|
||||
return;
|
||||
}
|
||||
if (_selectMode) {
|
||||
const cb = cardEl.querySelector('.note-card-cb');
|
||||
if (cb) {
|
||||
@@ -2193,9 +2290,9 @@ function _bindCardEvents(body) {
|
||||
_editNote(id);
|
||||
}
|
||||
};
|
||||
// Mobile: long-press anywhere on a note card → enter drag-to-reorder mode.
|
||||
// Cancelled by movement (so it doesn't interfere with vertical scrolling)
|
||||
// or by lifting the finger before the timer fires.
|
||||
// Mobile: long-press anywhere on a note card → enter Select mode and
|
||||
// select that card. Cancelled by movement (so it doesn't interfere with
|
||||
// vertical scrolling) or by lifting the finger before the timer fires.
|
||||
if (_isNotesMobileMode()) {
|
||||
body.querySelectorAll('.note-card').forEach(card => _bindLongPressDrag(card));
|
||||
}
|
||||
@@ -2204,14 +2301,37 @@ function _bindCardEvents(body) {
|
||||
});
|
||||
// Click title — edit, or toggle select in select mode
|
||||
body.querySelectorAll('.note-card-title[data-action="edit"]').forEach(el => {
|
||||
el.addEventListener('click', (e) => { e.stopPropagation(); tapToEditOrSelect(el.closest('.note-card')); });
|
||||
el.addEventListener('click', (e) => {
|
||||
e.stopPropagation();
|
||||
_smoothScrollToNoteCard(el.closest('.note-card'));
|
||||
tapToEditOrSelect(el.closest('.note-card'));
|
||||
});
|
||||
});
|
||||
// Click content — edit, or toggle select in select mode
|
||||
body.querySelectorAll('.note-content-preview').forEach(el => {
|
||||
el.addEventListener('click', (e) => { e.stopPropagation(); tapToEditOrSelect(el.closest('.note-card')); });
|
||||
});
|
||||
// A saved drawing is the note's primary content. Open its editor directly
|
||||
// when the preview is clicked, matching title/content preview behavior.
|
||||
body.querySelectorAll('.note-card-drawing .note-card-image').forEach(el => {
|
||||
el.addEventListener('click', (e) => {
|
||||
e.stopPropagation();
|
||||
tapToEditOrSelect(el.closest('.note-card'));
|
||||
});
|
||||
});
|
||||
// Click empty area of checklist preview (not on checkbox/X) — edit
|
||||
body.querySelectorAll('.note-checklist-preview').forEach(el => {
|
||||
el.addEventListener('wheel', (e) => {
|
||||
const atTop = el.scrollTop <= 0;
|
||||
const atBottom = el.scrollTop + el.clientHeight >= el.scrollHeight - 1;
|
||||
if ((e.deltaY < 0 && atTop) || (e.deltaY > 0 && atBottom)) {
|
||||
const pane = el.closest('.notes-pane-body');
|
||||
if (pane) {
|
||||
pane.scrollTop += e.deltaY;
|
||||
e.preventDefault();
|
||||
}
|
||||
}
|
||||
}, { passive: false });
|
||||
el.addEventListener('click', (e) => {
|
||||
if (e.target.closest('.note-checkbox, .note-checkbox-rm, .note-checkbox-agent, .note-cl-quickadd, input')) return;
|
||||
e.stopPropagation();
|
||||
@@ -2652,6 +2772,7 @@ function _bindCardEvents(body) {
|
||||
body.querySelectorAll('.note-checkbox').forEach(el => {
|
||||
el.addEventListener('click', (e) => {
|
||||
if (_selectMode) return; // let card-level handler take over
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
const noteId = el.dataset.noteId;
|
||||
const idx = parseInt(el.dataset.idx);
|
||||
@@ -3653,9 +3774,10 @@ function _buildForm(note = null) {
|
||||
// persistent file. We block the save until upload completes — drawings
|
||||
// can't be re-rendered later without the URL.
|
||||
const canvas = form.querySelector('.note-form-canvas');
|
||||
const url = await _uploadCanvasAsPng(canvas);
|
||||
if (!url) { uiModule.showError('Failed to save drawing'); return; }
|
||||
payload.image_url = url;
|
||||
const upload = await _uploadCanvasAsPng(canvas, note?.gallery_id || null);
|
||||
if (!upload?.url) { uiModule.showError('Failed to save drawing'); return; }
|
||||
payload.image_url = upload.url;
|
||||
if (upload.galleryId) payload.gallery_id = upload.galleryId;
|
||||
} else if (currentType === 'goal') {
|
||||
// Legacy: existing goal-type notes still edit through this branch.
|
||||
// No AI involvement — save as a normal note with description + items.
|
||||
@@ -3728,10 +3850,17 @@ function _buildForm(note = null) {
|
||||
// touching a separate Archive button.
|
||||
if (isEdit && window.innerWidth <= 768) {
|
||||
const _saveLabelEl = _saveBtnEl0.querySelector('.nft-label');
|
||||
const _isFinishedTodo = () => {
|
||||
if (currentType !== 'todo') return false;
|
||||
const rows = [...form.querySelectorAll('.note-cl-row')];
|
||||
const withText = rows.filter(row => (row.querySelector('.note-cl-text')?.value || '').trim());
|
||||
return withText.length > 0 && withText.every(row => row.classList.contains('done'));
|
||||
};
|
||||
const _enterArchive = () => {
|
||||
_saveBtnEl0.classList.add('archive-mode');
|
||||
if (_saveLabelEl) _saveLabelEl.textContent = 'Archive';
|
||||
_saveBtnEl0.title = 'Archive';
|
||||
const label = _isFinishedTodo() ? 'Finish' : 'Archive';
|
||||
if (_saveLabelEl) _saveLabelEl.textContent = label;
|
||||
_saveBtnEl0.title = label;
|
||||
};
|
||||
const _enterUpdate = () => {
|
||||
if (!_saveBtnEl0.classList.contains('archive-mode')) return;
|
||||
@@ -3742,6 +3871,16 @@ function _buildForm(note = null) {
|
||||
_enterArchive();
|
||||
form.addEventListener('input', _enterUpdate, true);
|
||||
form.addEventListener('change', _enterUpdate, true);
|
||||
// Checklist dots use a custom control and dispatch their update before
|
||||
// this click bubbles. Re-evaluate afterward so a fully completed todo
|
||||
// offers Finish (which archives/removes it from the active list).
|
||||
form.addEventListener('click', (event) => {
|
||||
if (!event.target.closest('.note-cl-dot')) return;
|
||||
setTimeout(() => {
|
||||
if (_isFinishedTodo()) _enterArchive();
|
||||
else _enterUpdate();
|
||||
}, 0);
|
||||
}, true);
|
||||
}
|
||||
|
||||
// Cancel
|
||||
@@ -3847,6 +3986,9 @@ function _wireRow(row, container) {
|
||||
row.querySelector('.note-cl-dot')?.addEventListener('click', () => {
|
||||
const wasDone = row.classList.contains('done');
|
||||
row.classList.toggle('done');
|
||||
// The custom dot has no native input/change event. Notify the form so
|
||||
// mobile edit mode switches its action from Archive back to Update.
|
||||
container.dispatchEvent(new Event('input', { bubbles: true }));
|
||||
const becameDone = !wasDone; // we just flipped it
|
||||
const dot = row.querySelector('.note-cl-dot');
|
||||
const dRect = (dot || row).getBoundingClientRect();
|
||||
@@ -4002,6 +4144,35 @@ function _buildDrawHtml() {
|
||||
function _wireCanvas(container, initialImageUrl) {
|
||||
const canvas = container.querySelector('.note-form-canvas');
|
||||
if (!canvas) return;
|
||||
const resolveThemeColor = (expression, fallback) => {
|
||||
const probe = document.createElement('span');
|
||||
probe.style.color = expression;
|
||||
probe.style.position = 'fixed';
|
||||
probe.style.visibility = 'hidden';
|
||||
document.body.appendChild(probe);
|
||||
const resolved = getComputedStyle(probe).color || fallback;
|
||||
probe.remove();
|
||||
return resolved;
|
||||
};
|
||||
const colorToHex = (color, fallback) => {
|
||||
try {
|
||||
const probe = document.createElement('canvas');
|
||||
probe.width = probe.height = 1;
|
||||
const probeCtx = probe.getContext('2d');
|
||||
probeCtx.fillStyle = color;
|
||||
probeCtx.fillRect(0, 0, 1, 1);
|
||||
const [r, g, b] = probeCtx.getImageData(0, 0, 1, 1).data;
|
||||
return `#${[r, g, b].map(v => v.toString(16).padStart(2, '0')).join('')}`;
|
||||
} catch { return fallback; }
|
||||
};
|
||||
const surfaceColor = resolveThemeColor('var(--panel, var(--bg))', '#282c34');
|
||||
const surfaceColorHex = colorToHex(surfaceColor, '#282c34');
|
||||
const highlightColor = colorToHex(
|
||||
resolveThemeColor('var(--accent, var(--red))', '#e06c75'),
|
||||
'#e06c75',
|
||||
);
|
||||
const colorInput = container.querySelector('.note-form-draw-color');
|
||||
if (colorInput) colorInput.value = highlightColor;
|
||||
// Bump the backing-store resolution for retina displays so strokes stay
|
||||
// crisp. Set only style.width — leaving style.height to auto so the canvas
|
||||
// scales uniformly via its intrinsic aspect ratio. If both CSS dimensions
|
||||
@@ -4022,7 +4193,7 @@ function _wireCanvas(container, initialImageUrl) {
|
||||
canvas.height = cssH * dpr;
|
||||
const ctx = canvas.getContext('2d');
|
||||
ctx.scale(dpr, dpr);
|
||||
ctx.fillStyle = '#ffffff';
|
||||
ctx.fillStyle = surfaceColor;
|
||||
ctx.fillRect(0, 0, cssW, cssH);
|
||||
ctx.lineCap = 'round';
|
||||
ctx.lineJoin = 'round';
|
||||
@@ -4046,7 +4217,7 @@ function _wireCanvas(container, initialImageUrl) {
|
||||
rm.addEventListener('click', (e) => {
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
ctx.fillStyle = '#ffffff';
|
||||
ctx.fillStyle = surfaceColor;
|
||||
ctx.fillRect(0, 0, cssW, cssH);
|
||||
rm.remove();
|
||||
});
|
||||
@@ -4054,7 +4225,6 @@ function _wireCanvas(container, initialImageUrl) {
|
||||
}
|
||||
}
|
||||
|
||||
const colorInput = container.querySelector('.note-form-draw-color');
|
||||
// Swap the native browser color dialog for the in-house HSV picker
|
||||
// (same one used by Themes + the gallery editor). Existing `input` event
|
||||
// listeners + .value reads keep working — see colorPicker.js.
|
||||
@@ -4087,6 +4257,7 @@ function _wireCanvas(container, initialImageUrl) {
|
||||
// on Undo. Cap to 30 to keep memory bounded.
|
||||
const _undoStack = [];
|
||||
const UNDO_LIMIT = 30;
|
||||
const _markCanvasDirty = () => container.dispatchEvent(new Event('input', { bubbles: true }));
|
||||
const _snapshot = () => {
|
||||
try {
|
||||
const w = canvas.width, h = canvas.height;
|
||||
@@ -4103,6 +4274,7 @@ function _wireCanvas(container, initialImageUrl) {
|
||||
ctx.setTransform(1, 0, 0, 1, 0, 0);
|
||||
ctx.putImageData(prev, 0, 0);
|
||||
ctx.restore();
|
||||
_markCanvasDirty();
|
||||
};
|
||||
|
||||
const _pos = (e) => {
|
||||
@@ -4212,6 +4384,7 @@ function _wireCanvas(container, initialImageUrl) {
|
||||
ctx.textBaseline = 'top';
|
||||
ctx.fillText(text, logical.x, logical.y - logicalSize * 0.7);
|
||||
ctx.restore();
|
||||
_markCanvasDirty();
|
||||
};
|
||||
input.addEventListener('blur', commit);
|
||||
input.addEventListener('keydown', (ev) => {
|
||||
@@ -4250,14 +4423,17 @@ function _wireCanvas(container, initialImageUrl) {
|
||||
return;
|
||||
}
|
||||
const erasing = mode === 'eraser';
|
||||
ctx.globalCompositeOperation = erasing ? 'destination-out' : 'source-over';
|
||||
ctx.strokeStyle = erasing ? 'rgba(0,0,0,1)' : (colorInput?.value || '#222');
|
||||
ctx.globalCompositeOperation = 'source-over';
|
||||
ctx.strokeStyle = erasing ? surfaceColor : (colorInput?.value || highlightColor);
|
||||
ctx.lineWidth = Number(sizeInput?.value || 3) * (erasing ? 2.5 : 1);
|
||||
ctx.lineTo(p.x, p.y);
|
||||
ctx.stroke();
|
||||
last = p;
|
||||
};
|
||||
const _end = () => { drawing = false; last = null; _shapeSnapshot = null; };
|
||||
const _end = () => {
|
||||
if (drawing) _markCanvasDirty();
|
||||
drawing = false; last = null; _shapeSnapshot = null;
|
||||
};
|
||||
|
||||
canvas.addEventListener('mousedown', _begin);
|
||||
canvas.addEventListener('mousemove', _move);
|
||||
@@ -4280,10 +4456,10 @@ function _wireCanvas(container, initialImageUrl) {
|
||||
const isEraser = next === 'eraser';
|
||||
const isPen = next === 'pen';
|
||||
const isText = next.startsWith('text-');
|
||||
// Swatch: white while erasing, restored as soon as we leave that mode.
|
||||
// Swatch: theme surface while erasing, restored when leaving that mode.
|
||||
if (isEraser && !wasEraser && colorInput) {
|
||||
_preEraseColor = colorInput.value;
|
||||
colorInput.value = '#ffffff';
|
||||
colorInput.value = surfaceColorHex;
|
||||
} else if (!isEraser && wasEraser && colorInput && _preEraseColor) {
|
||||
colorInput.value = _preEraseColor;
|
||||
_preEraseColor = null;
|
||||
@@ -4339,17 +4515,21 @@ function _wireCanvas(container, initialImageUrl) {
|
||||
|
||||
// Export the canvas as a PNG dataURL, upload it via the existing /api/upload
|
||||
// endpoint, and resolve to a persistent URL. Returns null on failure.
|
||||
async function _uploadCanvasAsPng(canvas) {
|
||||
async function _uploadCanvasAsPng(canvas, galleryId = null) {
|
||||
if (!canvas) return null;
|
||||
const blob = await new Promise(r => canvas.toBlob(r, 'image/png'));
|
||||
if (!blob) return null;
|
||||
const fd = new FormData();
|
||||
fd.append('files', blob, 'drawing.png');
|
||||
if (galleryId) fd.append('gallery_id', galleryId);
|
||||
try {
|
||||
const res = await fetch(`${API_BASE}/api/upload`, { method: 'POST', body: fd, credentials: 'same-origin' });
|
||||
const data = await res.json();
|
||||
const id = data.files?.[0]?.id;
|
||||
return id ? `${API_BASE}/api/upload/${id}` : null;
|
||||
return id ? {
|
||||
url: `${API_BASE}/api/upload/${id}`,
|
||||
galleryId: data.files?.[0]?.gallery_id || null,
|
||||
} : null;
|
||||
} catch { return null; }
|
||||
}
|
||||
|
||||
@@ -4364,10 +4544,18 @@ function _createNote(type = 'todo') {
|
||||
const form = _buildForm(_n);
|
||||
form.classList.add('note-form-new');
|
||||
body.prepend(form);
|
||||
form.querySelector('.note-form-title').focus();
|
||||
const initialField = type === 'note'
|
||||
? form.querySelector('.note-form-content')
|
||||
: form.querySelector('.note-cl-text, .note-form-title');
|
||||
initialField?.focus();
|
||||
if (restored) uiModule.showToast('Restored unsaved note');
|
||||
}
|
||||
|
||||
export function newNote() {
|
||||
if (!_open) openPanel();
|
||||
requestAnimationFrame(() => _createNote('note'));
|
||||
}
|
||||
|
||||
// Build the plain-text/markdown form of a note for clipboard copy.
|
||||
function _serializeNoteForCopy(note) {
|
||||
const lines = [];
|
||||
@@ -4821,17 +5009,12 @@ function _openMobileFullscreenEdit(id, fromCard) {
|
||||
// ISN'T a link flips back to the input for editing.
|
||||
form.querySelectorAll('.note-cl-row').forEach(_addRowReadMode);
|
||||
|
||||
// Move Archive + Delete from the form's footer actions row up into
|
||||
// the header bar (to the right of the back chevron) so they're
|
||||
// always reachable without scrolling and free up the footer for
|
||||
// Cancel/Save only. Handlers stay attached when nodes move.
|
||||
// Move only Delete from the form's footer actions row into the header.
|
||||
// Archive intentionally stays out of the header: the footer Update button
|
||||
// already morphs into the mobile Archive action for an untouched note.
|
||||
const headerActions = overlay.querySelector('.note-fullscreen-actions');
|
||||
const archiveBtn = form.querySelector('.note-form-archive-btn');
|
||||
const deleteBtn = form.querySelector('.note-form-delete-btn');
|
||||
if (headerActions && archiveBtn) {
|
||||
archiveBtn.classList.remove('note-form-collapsible');
|
||||
headerActions.appendChild(archiveBtn);
|
||||
}
|
||||
if (headerActions && deleteBtn) {
|
||||
deleteBtn.classList.remove('note-form-collapsible');
|
||||
headerActions.appendChild(deleteBtn);
|
||||
@@ -4955,7 +5138,7 @@ function _closeMobileFullscreenEdit(opts = {}) {
|
||||
}, 220);
|
||||
}
|
||||
|
||||
// ── Long-press drag-to-reorder ───────────────────────────────────────
|
||||
// ── Long-press select ────────────────────────────────────────────────
|
||||
function _bindLongPressDrag(card) {
|
||||
let pressTimer = null;
|
||||
let startX = 0, startY = 0;
|
||||
@@ -4971,14 +5154,11 @@ function _bindLongPressDrag(card) {
|
||||
armed = true;
|
||||
startX = e.touches[0].clientX;
|
||||
startY = e.touches[0].clientY;
|
||||
// Capture the touch object so the timer callback can pass it to
|
||||
// _enterDragMode → _beginGrab. The finger is still held down, so
|
||||
// the drag starts the instant the timer fires.
|
||||
const heldTouch = { clientX: startX, clientY: startY };
|
||||
pressTimer = setTimeout(() => {
|
||||
if (!armed) return;
|
||||
try { navigator.vibrate?.(15); } catch {}
|
||||
_enterDragMode(card, heldTouch);
|
||||
_suppressNextNoteTapId = card.dataset.noteId;
|
||||
_enterSelectMode(card.dataset.noteId);
|
||||
}, HOLD_MS);
|
||||
}, { passive: true });
|
||||
card.addEventListener('touchmove', (e) => {
|
||||
@@ -5313,6 +5493,19 @@ async function _initReminders() {
|
||||
// link the agent emits after a manage_notes create. Falls back to
|
||||
// just opening the panel when the card isn't found (panel still
|
||||
// loading, note in a different filter, etc.).
|
||||
function _smoothScrollToNoteCard(card) {
|
||||
if (!card) return;
|
||||
const scroller = card.closest('.notes-pane-body');
|
||||
if (!scroller) {
|
||||
try { card.scrollIntoView({ block: 'center', behavior: 'smooth' }); } catch (_) {}
|
||||
return;
|
||||
}
|
||||
const scrollerRect = scroller.getBoundingClientRect();
|
||||
const cardRect = card.getBoundingClientRect();
|
||||
const delta = (cardRect.top - scrollerRect.top) - (scroller.clientHeight - cardRect.height) / 2;
|
||||
scroller.scrollTo({ top: Math.max(0, scroller.scrollTop + delta), behavior: 'smooth' });
|
||||
}
|
||||
|
||||
async function openNote(noteId) {
|
||||
const wasOpen = !!(isPanelOpen && isPanelOpen());
|
||||
_showingArchived = false;
|
||||
@@ -5340,7 +5533,7 @@ async function openNote(noteId) {
|
||||
const card = document.querySelector(`.note-card[data-note-id="${noteId}"]`)
|
||||
|| document.querySelector(`.note-card[data-note-id^="${noteId.slice(0, 8)}"]`);
|
||||
if (card) {
|
||||
try { card.scrollIntoView({ behavior: 'smooth', block: 'center' }); } catch (_) {}
|
||||
_smoothScrollToNoteCard(card);
|
||||
card.classList.add('note-card-flash');
|
||||
setTimeout(() => card.classList.remove('note-card-flash'), 1600);
|
||||
return true;
|
||||
@@ -5354,7 +5547,7 @@ async function openNote(noteId) {
|
||||
setTimeout(tryNext, 120);
|
||||
}
|
||||
|
||||
const notesModule = { openPanel, closePanel, togglePanel, isPanelOpen, openNote, openNotes: openPanel, closeNotes: closePanel, isNotesOpen: isPanelOpen, refreshDueBadge };
|
||||
const notesModule = { openPanel, closePanel, togglePanel, isPanelOpen, openNote, newNote, openNotes: openPanel, closeNotes: closePanel, isNotesOpen: isPanelOpen, refreshDueBadge };
|
||||
export default notesModule;
|
||||
export { openPanel as openNotes, closePanel as closeNotes, isPanelOpen as isNotesOpen, openNote };
|
||||
window.notesModule = notesModule;
|
||||
|
||||
Reference in New Issue
Block a user