mirror of
https://github.com/zadam/trilium.git
synced 2026-05-07 00:15:35 +02:00
feat(mobile/nav): add preview for current note
This commit is contained in:
@@ -6,20 +6,15 @@
|
||||
contain: content;
|
||||
}
|
||||
|
||||
.mobile-navigator-header {
|
||||
.mobile-navigator-toolbar {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 4px;
|
||||
padding: 6px 8px;
|
||||
padding: 4px 6px;
|
||||
border-bottom: 1px solid var(--main-border-color);
|
||||
background: var(--main-background-color);
|
||||
position: sticky;
|
||||
top: 0;
|
||||
z-index: 1;
|
||||
}
|
||||
|
||||
.mobile-navigator-back {
|
||||
flex: 0 0 auto;
|
||||
font-size: 1.6em;
|
||||
min-width: 40px;
|
||||
min-height: 40px;
|
||||
@@ -29,41 +24,7 @@
|
||||
visibility: hidden;
|
||||
}
|
||||
|
||||
.mobile-navigator-title {
|
||||
flex: 1 1 auto;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
background: none;
|
||||
border: none;
|
||||
color: var(--main-text-color);
|
||||
padding: 6px 8px;
|
||||
min-height: 40px;
|
||||
font-size: 1.05em;
|
||||
font-weight: 600;
|
||||
text-align: start;
|
||||
min-width: 0;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.mobile-navigator-title:disabled {
|
||||
cursor: default;
|
||||
opacity: 0.7;
|
||||
}
|
||||
|
||||
.mobile-navigator-title-icon {
|
||||
font-size: 1.1em;
|
||||
flex: 0 0 auto;
|
||||
}
|
||||
|
||||
.mobile-navigator-title-text {
|
||||
flex: 1 1 auto;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.mobile-navigator-list {
|
||||
.mobile-navigator-scroll {
|
||||
flex: 1 1 auto;
|
||||
overflow-y: auto;
|
||||
overflow-x: hidden;
|
||||
@@ -73,6 +34,98 @@
|
||||
padding-bottom: env(safe-area-inset-bottom, 0);
|
||||
}
|
||||
|
||||
.mobile-navigator-current-tile {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 10px;
|
||||
padding: 14px 16px;
|
||||
background: var(--accented-background-color, var(--hover-item-background-color));
|
||||
border-bottom: 2px solid var(--main-border-color);
|
||||
cursor: pointer;
|
||||
user-select: none;
|
||||
-webkit-tap-highlight-color: transparent;
|
||||
color: var(--main-text-color);
|
||||
}
|
||||
|
||||
.mobile-navigator-current-tile:active {
|
||||
filter: brightness(0.95);
|
||||
}
|
||||
|
||||
.mobile-navigator-current-tile.is-active {
|
||||
background: var(--active-item-background-color);
|
||||
color: var(--active-item-text-color);
|
||||
}
|
||||
|
||||
.mobile-navigator-current-tile.is-archived {
|
||||
opacity: 0.7;
|
||||
}
|
||||
|
||||
.mobile-navigator-current-header {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 10px;
|
||||
min-height: 32px;
|
||||
}
|
||||
|
||||
.mobile-navigator-current-icon {
|
||||
flex: 0 0 auto;
|
||||
font-size: 1.3em;
|
||||
}
|
||||
|
||||
.mobile-navigator-current-title {
|
||||
flex: 1 1 auto;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
font-size: 1.15em;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.mobile-navigator-current-open {
|
||||
flex: 0 0 auto;
|
||||
font-size: 1.1em;
|
||||
opacity: 0.6;
|
||||
}
|
||||
|
||||
.mobile-navigator-current-preview {
|
||||
position: relative;
|
||||
max-height: 140px;
|
||||
overflow: hidden;
|
||||
font-size: 0.92em;
|
||||
line-height: 1.35;
|
||||
color: var(--muted-text-color, var(--main-text-color));
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
.mobile-navigator-current-preview::after {
|
||||
content: "";
|
||||
position: absolute;
|
||||
inset-inline: 0;
|
||||
bottom: 0;
|
||||
height: 32px;
|
||||
background: linear-gradient(to bottom, transparent, var(--accented-background-color, var(--hover-item-background-color)));
|
||||
}
|
||||
|
||||
.mobile-navigator-current-preview:empty,
|
||||
.mobile-navigator-current-preview .note-book-content:empty {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.mobile-navigator-current-preview .note-book-content {
|
||||
max-width: 100%;
|
||||
}
|
||||
|
||||
.mobile-navigator-current-preview img,
|
||||
.mobile-navigator-current-preview video {
|
||||
max-width: 100%;
|
||||
height: auto;
|
||||
}
|
||||
|
||||
.mobile-navigator-list {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.mobile-navigator-row {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
@@ -115,5 +168,5 @@
|
||||
.mobile-navigator-row-chevron {
|
||||
flex: 0 0 auto;
|
||||
font-size: 1.4em;
|
||||
opacity: 0.6;
|
||||
opacity: 0.5;
|
||||
}
|
||||
|
||||
@@ -6,6 +6,7 @@ import { useCallback, useEffect, useMemo, useRef, useState } from "preact/hooks"
|
||||
import type FNote from "../../entities/fnote";
|
||||
import froca from "../../services/froca";
|
||||
import { t } from "../../services/i18n";
|
||||
import { NoteContent } from "../collections/legacy/ListOrGridView";
|
||||
import ActionButton from "../react/ActionButton";
|
||||
import {
|
||||
useActiveNoteContext,
|
||||
@@ -19,8 +20,9 @@ import NoItems from "../react/NoItems";
|
||||
|
||||
/**
|
||||
* A touch-native replacement for the Fancytree-based note tree on mobile. Shows one
|
||||
* "column" of children at a time (iOS Files / macOS Finder style), with a back
|
||||
* chevron to pop up the hierarchy and tappable rows to drill in or open notes.
|
||||
* "column" of children at a time (iOS Files / macOS Finder style): the column's
|
||||
* current note is rendered at the top with a content preview and opens on tap,
|
||||
* while tapping child rows drills deeper without navigating.
|
||||
*/
|
||||
export default function MobileNoteNavigator() {
|
||||
const { notePath: activeNotePath, hoistedNoteId, noteContext, parentComponent } = useActiveNoteContext();
|
||||
@@ -42,7 +44,6 @@ export default function MobileNoteNavigator() {
|
||||
const parentPath = stack[stack.length - 1];
|
||||
const parentNoteId = getLastSegment(parentPath);
|
||||
if (loadResults.getBranchRows().some((b) => b.parentNoteId === parentNoteId)) {
|
||||
// No state change, but force a re-render by resetting the same stack reference.
|
||||
setStack((prev) => [...prev]);
|
||||
}
|
||||
});
|
||||
@@ -50,6 +51,7 @@ export default function MobileNoteNavigator() {
|
||||
const currentParentPath = stack[stack.length - 1];
|
||||
const currentParentId = getLastSegment(currentParentPath);
|
||||
const parentNote = useNote(currentParentId);
|
||||
const parentIcon = useNoteIcon(parentNote);
|
||||
const activeNoteId = activeNotePath ? getLastSegment(activeNotePath) : undefined;
|
||||
|
||||
// Froca's initial `tree` response only returns the top of the tree; deeper levels are
|
||||
@@ -82,34 +84,23 @@ export default function MobileNoteNavigator() {
|
||||
setStack((prev) => (prev.length > 1 ? prev.slice(0, -1) : prev));
|
||||
}, []);
|
||||
|
||||
const openNote = useCallback(
|
||||
async (childNotePath: string, closeSidebar: boolean) => {
|
||||
await noteContext?.setNote(childNotePath);
|
||||
if (closeSidebar) {
|
||||
manualStackRef.current = false;
|
||||
parentComponent?.triggerCommand("setActiveScreen", { screen: "detail" });
|
||||
}
|
||||
},
|
||||
[noteContext, parentComponent]
|
||||
);
|
||||
|
||||
const drillInto = useCallback(
|
||||
async (childNotePath: string) => {
|
||||
manualStackRef.current = true;
|
||||
setStack((prev) => [...prev, childNotePath]);
|
||||
await noteContext?.setNote(childNotePath);
|
||||
},
|
||||
[noteContext]
|
||||
);
|
||||
|
||||
const openParent = useCallback(() => {
|
||||
const openCurrent = useCallback(async () => {
|
||||
if (!currentParentPath) return;
|
||||
openNote(currentParentPath, true);
|
||||
}, [currentParentPath, openNote]);
|
||||
await noteContext?.setNote(currentParentPath);
|
||||
manualStackRef.current = false;
|
||||
parentComponent?.triggerCommand("setActiveScreen", { screen: "detail" });
|
||||
}, [currentParentPath, noteContext, parentComponent]);
|
||||
|
||||
const drillInto = useCallback((childNotePath: string) => {
|
||||
manualStackRef.current = true;
|
||||
setStack((prev) => [...prev, childNotePath]);
|
||||
}, []);
|
||||
|
||||
const isCurrentActive = !!activeNoteId && activeNoteId === currentParentId;
|
||||
|
||||
return (
|
||||
<div className="mobile-note-navigator">
|
||||
<div className="mobile-navigator-header">
|
||||
<div className="mobile-navigator-toolbar">
|
||||
<ActionButton
|
||||
className={clsx("mobile-navigator-back", !canGoBack && "invisible")}
|
||||
icon="bx bx-chevron-left"
|
||||
@@ -117,37 +108,56 @@ export default function MobileNoteNavigator() {
|
||||
onClick={canGoBack ? goBack : undefined}
|
||||
disabled={!canGoBack}
|
||||
/>
|
||||
<button
|
||||
type="button"
|
||||
className="mobile-navigator-title"
|
||||
onClick={openParent}
|
||||
disabled={!parentNote}
|
||||
>
|
||||
<Icon icon={parentNote?.getIcon() ?? "bx bx-folder"} className="mobile-navigator-title-icon" />
|
||||
<span className="mobile-navigator-title-text">
|
||||
{parentNote?.title ?? t("mobile_note_navigator.loading")}
|
||||
</span>
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div className="mobile-navigator-list">
|
||||
{!isLoaded ? (
|
||||
<NoItems icon="bx bx-loader" text={t("mobile_note_navigator.loading")} />
|
||||
) : children.length === 0 ? (
|
||||
<NoItems icon="bx bx-folder-open" text={t("mobile_note_navigator.empty")} />
|
||||
) : (
|
||||
children.map((child) => (
|
||||
<NavigatorRow
|
||||
key={child.branchId}
|
||||
note={child.note}
|
||||
prefix={child.prefix}
|
||||
childNotePath={`${currentParentPath}/${child.note.noteId}`}
|
||||
isActive={child.note.noteId === activeNoteId}
|
||||
onOpen={openNote}
|
||||
onDrill={drillInto}
|
||||
/>
|
||||
))
|
||||
)}
|
||||
<div className="mobile-navigator-scroll">
|
||||
<div
|
||||
className={clsx("mobile-navigator-current-tile", {
|
||||
"is-active": isCurrentActive,
|
||||
"is-archived": parentNote?.isArchived
|
||||
})}
|
||||
role="button"
|
||||
tabIndex={0}
|
||||
onClick={openCurrent}
|
||||
>
|
||||
<div className="mobile-navigator-current-header">
|
||||
<Icon icon={parentIcon ?? "bx bx-folder"} className="mobile-navigator-current-icon" />
|
||||
<span className="mobile-navigator-current-title">
|
||||
{parentNote?.title ?? t("mobile_note_navigator.loading")}
|
||||
</span>
|
||||
<Icon icon="bx bx-link-external" className="mobile-navigator-current-open" />
|
||||
</div>
|
||||
{parentNote && (
|
||||
<div className="mobile-navigator-current-preview">
|
||||
<NoteContent
|
||||
note={parentNote}
|
||||
trim
|
||||
noChildrenList
|
||||
highlightedTokens={null}
|
||||
includeArchivedNotes={!hideArchived}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<div className="mobile-navigator-list">
|
||||
{!isLoaded ? (
|
||||
<NoItems icon="bx bx-loader" text={t("mobile_note_navigator.loading")} />
|
||||
) : children.length === 0 ? (
|
||||
<NoItems icon="bx bx-folder-open" text={t("mobile_note_navigator.empty")} />
|
||||
) : (
|
||||
children.map((child) => (
|
||||
<NavigatorRow
|
||||
key={child.branchId}
|
||||
note={child.note}
|
||||
prefix={child.prefix}
|
||||
childNotePath={`${currentParentPath}/${child.note.noteId}`}
|
||||
isActive={child.note.noteId === activeNoteId}
|
||||
onDrill={drillInto}
|
||||
/>
|
||||
))
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
@@ -158,11 +168,10 @@ interface NavigatorRowProps {
|
||||
prefix?: string;
|
||||
childNotePath: string;
|
||||
isActive: boolean;
|
||||
onOpen: (notePath: string, closeSidebar: boolean) => void;
|
||||
onDrill: (notePath: string) => void;
|
||||
}
|
||||
|
||||
function NavigatorRow({ note, prefix, childNotePath, isActive, onOpen, onDrill }: NavigatorRowProps) {
|
||||
function NavigatorRow({ note, prefix, childNotePath, isActive, onDrill }: NavigatorRowProps) {
|
||||
const icon = useNoteIcon(note);
|
||||
const hasChildren = note.hasChildren();
|
||||
const colorClass = note.getColorClass();
|
||||
@@ -177,11 +186,11 @@ function NavigatorRow({ note, prefix, childNotePath, isActive, onOpen, onDrill }
|
||||
})}
|
||||
role="button"
|
||||
tabIndex={0}
|
||||
onClick={() => (hasChildren ? onDrill(childNotePath) : onOpen(childNotePath, true))}
|
||||
onClick={() => onDrill(childNotePath)}
|
||||
>
|
||||
<Icon icon={icon ?? "bx bx-note"} className="mobile-navigator-row-icon" />
|
||||
<span className="mobile-navigator-row-title">{title}</span>
|
||||
{hasChildren && <Icon icon="bx bx-chevron-right" className="mobile-navigator-row-chevron" />}
|
||||
<Icon icon="bx bx-chevron-right" className="mobile-navigator-row-chevron" />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user