import React, { useEffect, useMemo, useRef, useState } from 'react'; import { IcNavBack, IcCheck, IcFile, IcFolder, IcPhone, IcSearch, IcSettings } from '../res/icons'; import { SIMULATOR_CONFIG } from '@/os/data'; const { statusBarHeight, bottomGestureHeight } = SIMULATOR_CONFIG.framework; import * as TimeService from '../../../os/TimeService'; import { useShallow } from 'zustand/react/shallow'; import { useNotesStore, selectVisibleNotes } from '../state'; import { BottomTabBar } from '../components/BottomTabBar'; import { IcDataItemAudio, IcDataItemClock, IcDataItemMindOutline, IcDataItemStick, IcNoteLock, IcFab, } from '../res/icons'; import { ActionSheet } from '../components/ActionSheet'; import { Toast } from '@/os/components/Toast'; import { colors } from '../res/colors'; import { dimens } from '../res/dimens'; import { strings } from '../res/strings'; import { stringsEn } from '../res/strings.en'; import { useAppStrings } from '../../../os/useAppStrings'; import type { Note } from '../types'; import { useNotesGestures } from '../hooks/useNotesGestures'; import { getFolderDisplayName } from '../utils'; const NOTE_LONG_PRESS_MS = 520; const NOTE_LONG_PRESS_MOVE_CANCEL_PX = 12; function formatListTime(timestamp: number, s: typeof strings) { const d = TimeService.fromTimestamp(timestamp); const now = TimeService.getDate(); const y = d.getFullYear(); const m = d.getMonth() + 1; const day = d.getDate(); if (y !== now.getFullYear()) return `${y}${s.date_suffix_year}${m}${s.date_suffix_month}${day}${s.date_suffix_day}`; const hh = d.getHours().toString().padStart(2, '0'); const mm = d.getMinutes().toString().padStart(2, '0'); return `${m}${s.date_suffix_month}${day}${s.date_suffix_day} ${hh}:${mm}`; } function snippet(text: string) { return (text || '').replace(/\s+/g, ' ').trim(); } const NoteCard: React.FC<{ note: Note; selectionMode: boolean; selected: boolean; onOpen: () => void; onToggleSelected: () => void; onLongPress: () => void; }> = ({ note, selectionMode, selected, onOpen, onToggleSelected, onLongPress }) => { const s = useAppStrings(strings, stringsEn); const title = note.title.trim() || s.untitled; const content = snippet(note.content); const isCall = note.folderId === 'call'; const longPressTimerRef = useRef(null); const startPosRef = useRef<{ x: number; y: number } | null>(null); const didLongPressRef = useRef(false); const clearLongPress = () => { if (longPressTimerRef.current) window.clearTimeout(longPressTimerRef.current); longPressTimerRef.current = null; }; useEffect(() => { return () => clearLongPress(); }, []); return (
{selectionMode ? (
) : null}
); }; export const NotesListPage: React.FC = () => { const { go } = useNotesGestures(); const notes = useNotesStore(selectVisibleNotes); const { folders, selectedFolderId, setSelectedFolderId, settings, updateNote, deleteNote, hideNote } = useNotesStore( useShallow(s => ({ folders: s.folders, selectedFolderId: s.selectedFolderId, setSelectedFolderId: s.setSelectedFolderId, settings: s.settings, updateNote: s.updateNote, deleteNote: s.deleteNote, hideNote: s.hideNote, })) ); const s = useAppStrings(strings, stringsEn); const [query, setQuery] = useState(''); const [activeNote, setActiveNote] = useState(null); const [moveNote, setMoveNote] = useState(null); const [selectionMode, setSelectionMode] = useState(false); const [selectedNoteIds, setSelectedNoteIds] = useState>(() => new Set()); const [batchMoveIds, setBatchMoveIds] = useState(null); const [toast, setToast] = useState({ message: '', visible: false }); const filtered = useMemo(() => { const q = query.trim().toLowerCase(); return notes.filter(n => { const folderOk = selectedFolderId === 'all' || n.folderId === selectedFolderId; if (!folderOk) return false; if (!q) return true; return ( n.title.toLowerCase().includes(q) || n.content.toLowerCase().includes(q) ); }); }, [notes, query, selectedFolderId]); const noteById = useMemo(() => new Map(notes.map(n => [n.id, n] as const)), [notes]); const selectedNotes = useMemo(() => { const out: Note[] = []; selectedNoteIds.forEach((id) => { const n = noteById.get(id); if (n) out.push(n); }); return out; }, [noteById, selectedNoteIds]); const selectedCount = selectedNoteIds.size; const allPinned = selectedNotes.length > 0 && selectedNotes.every(n => !!n.pinned); const allFilteredSelected = filtered.length > 0 && filtered.every(n => selectedNoteIds.has(n.id)); useEffect(() => { if (selectionMode && selectedNoteIds.size === 0) setSelectionMode(false); }, [selectionMode, selectedNoteIds]); const cancelSelection = () => { setSelectionMode(false); setSelectedNoteIds(new Set()); setBatchMoveIds(null); }; const startSelection = (noteId: string) => { setSelectionMode(true); setSelectedNoteIds(new Set([noteId])); setActiveNote(null); setMoveNote(null); }; const toggleSelected = (noteId: string) => { setSelectedNoteIds((prev) => { const next = new Set(prev); if (next.has(noteId)) next.delete(noteId); else next.add(noteId); return next; }); }; const showToast = (message: string) => { setToast({ message, visible: true }); window.setTimeout(() => setToast({ message: '', visible: false }), 1200); }; const handleBatchStick = () => { if (!selectedNotes.length) return; const nextPinned = !allPinned; selectedNotes.forEach((n) => updateNote(n.id, { pinned: nextPinned })); showToast(nextPinned ? s.toast_pinned : s.toast_unpinned); cancelSelection(); }; const handleBatchHide = () => { if (!selectedNotes.length) return; selectedNotes.forEach((n) => hideNote(n.id)); showToast(s.toast_set_private); cancelSelection(); }; const handleBatchDelete = () => { if (!selectedNotes.length) return; selectedNotes.forEach((n) => deleteNote(n.id)); showToast(s.toast_moved_to_trash); cancelSelection(); }; const topPad = statusBarHeight + 18; const tabBarHeight = 64 + bottomGestureHeight; const fabBottom = tabBarHeight + dimens.fab_margin; return (
{/* Header */}
{selectionMode ? (
{s.selected_count_prefix}{selectedCount}{s.selected_count_suffix}
) : ( <>
{s.notes}
setQuery(e.target.value)} placeholder={s.search_placeholder} className="w-full h-10 bg-[#ededed] rounded-[14px] pl-11 pr-4 text-[15px] text-black placeholder:text-[#bdbdbd] outline-none" />
{folders.map(f => { const active = f.id === selectedFolderId; return ( ); })}
)}
{/* List */}
{filtered.length ? ( settings.notesViewMode === 'grid' ? (
{filtered.map(note => ( go('note.open', { id: note.id })} onToggleSelected={() => toggleSelected(note.id)} onLongPress={() => startSelection(note.id)} /> ))}
) : (
{filtered.map(note => ( go('note.open', { id: note.id })} onToggleSelected={() => toggleSelected(note.id)} onLongPress={() => startSelection(note.id)} /> ))}
) ) : (
{s.empty_notes}
)}
{/* Floating action button */} {!selectionMode ? ( ) : null} {selectionMode ? (
) : ( )} {!selectionMode ? ( { updateNote(activeNote.id, { pinned: !activeNote.pinned }); setActiveNote(null); showToast(activeNote.pinned ? s.toast_unpinned : s.toast_pinned); }, }, { key: 'move', label: s.action_move_to, onClick: () => { setMoveNote(activeNote); setActiveNote(null); }, }, { key: 'hide', label: s.action_set_private, onClick: () => { hideNote(activeNote.id); setActiveNote(null); showToast(s.toast_set_private); }, }, { key: 'delete', label: s.action_delete, danger: true, onClick: () => { deleteNote(activeNote.id); setActiveNote(null); showToast(s.toast_moved_to_trash); }, }, ] : [] } onClose={() => setActiveNote(null)} /> ) : null} {/* Move to folder sheet */} {!selectionMode && moveNote ? (
))}
) : null} {/* Batch move sheet */} {batchMoveIds ? (
))}
) : null} ); }; export default NotesListPage;