diff --git a/apps/client/src/desktop.ts b/apps/client/src/desktop.ts index bba3b6b90b..6f22d3fc7b 100644 --- a/apps/client/src/desktop.ts +++ b/apps/client/src/desktop.ts @@ -46,10 +46,6 @@ if (utils.isElectron()) { electronContextMenu.setupContextMenu(); } -if (utils.isPWA()) { - initPWATopbarColor(); -} - function initOnElectron() { const electron: typeof Electron = utils.dynamicRequire("electron"); electron.ipcRenderer.on("globalShortcut", async (event, actionName) => appContext.triggerCommand(actionName)); @@ -134,20 +130,3 @@ function initDarkOrLightMode(style: CSSStyleDeclaration) { const { nativeTheme } = utils.dynamicRequire("@electron/remote") as typeof ElectronRemote; nativeTheme.themeSource = themeSource; } - -function initPWATopbarColor() { - const tracker = $("#background-color-tracker"); - - if (tracker.length) { - const applyThemeColor = () => { - let meta = $("meta[name='theme-color']"); - if (!meta.length) { - meta = $(``).appendTo($("head")); - } - meta.attr("content", tracker.css("color")); - }; - - tracker.on("transitionend", applyThemeColor); - applyThemeColor(); - } -} diff --git a/apps/client/src/layouts/mobile_layout.css b/apps/client/src/layouts/mobile_layout.css index 95b9dd8afb..396fa4a467 100644 --- a/apps/client/src/layouts/mobile_layout.css +++ b/apps/client/src/layouts/mobile_layout.css @@ -1,3 +1,7 @@ +#background-color-tracker { + color: var(--main-background-color) !important; +} + span.keyboard-shortcut, kbd { display: none; diff --git a/apps/client/src/layouts/mobile_layout.tsx b/apps/client/src/layouts/mobile_layout.tsx index 0f36f282c4..8b162bced3 100644 --- a/apps/client/src/layouts/mobile_layout.tsx +++ b/apps/client/src/layouts/mobile_layout.tsx @@ -77,7 +77,6 @@ export default class MobileLayout { .child() .child() .child() - .child() .child() .child() ) diff --git a/apps/client/src/menus/launcher_context_menu.ts b/apps/client/src/menus/launcher_context_menu.ts index 717180690a..f4ae7a148f 100644 --- a/apps/client/src/menus/launcher_context_menu.ts +++ b/apps/client/src/menus/launcher_context_menu.ts @@ -1,12 +1,12 @@ -import treeService from "../services/tree.js"; -import froca from "../services/froca.js"; -import contextMenu, { type MenuCommandItem, type MenuItem } from "./context_menu.js"; -import dialogService from "../services/dialog.js"; -import server from "../services/server.js"; -import { t } from "../services/i18n.js"; +import type { ContextMenuCommandData,FilteredCommandNames } from "../components/app_context.js"; import type { SelectMenuItemEventListener } from "../components/events.js"; +import dialogService from "../services/dialog.js"; +import froca from "../services/froca.js"; +import { t } from "../services/i18n.js"; +import server from "../services/server.js"; +import treeService from "../services/tree.js"; import type NoteTreeWidget from "../widgets/note_tree.js"; -import type { FilteredCommandNames, ContextMenuCommandData } from "../components/app_context.js"; +import contextMenu, { type MenuCommandItem, type MenuItem } from "./context_menu.js"; type LauncherCommandNames = FilteredCommandNames; @@ -32,8 +32,8 @@ export default class LauncherContextMenu implements SelectMenuItemEventListener< const note = this.node.data.noteId ? await froca.getNote(this.node.data.noteId) : null; const parentNoteId = this.node.getParent().data.noteId; - const isVisibleRoot = note?.noteId === "_lbVisibleLaunchers"; - const isAvailableRoot = note?.noteId === "_lbAvailableLaunchers"; + const isVisibleRoot = note?.noteId === "_lbVisibleLaunchers" || note?.noteId === "_lbMobileVisibleLaunchers"; + const isAvailableRoot = note?.noteId === "_lbAvailableLaunchers" || note?.noteId === "_lbMobileAvailableLaunchers"; const isVisibleItem = parentNoteId === "_lbVisibleLaunchers" || parentNoteId === "_lbMobileVisibleLaunchers"; const isAvailableItem = parentNoteId === "_lbAvailableLaunchers" || parentNoteId === "_lbMobileAvailableLaunchers"; const isItem = isVisibleItem || isAvailableItem; diff --git a/apps/client/src/stylesheets/style.css b/apps/client/src/stylesheets/style.css index 88f5e328bb..acfdf6da40 100644 --- a/apps/client/src/stylesheets/style.css +++ b/apps/client/src/stylesheets/style.css @@ -410,6 +410,7 @@ body.desktop .tabulator-popup-container, .dropdown-menu.static { box-shadow: unset; + backdrop-filter: unset !important; } .dropend .dropdown-toggle::after { @@ -1553,8 +1554,14 @@ body:not(.mobile) #launcher-pane.horizontal .dropdown-submenu > .dropdown-menu { max-height: calc(var(--tn-modal-max-height) - var(--dropdown-bottom)); } + body.mobile #launcher-container .dropdown > .dropdown-menu.show { + border-bottom-left-radius: 0; + border-bottom-right-radius: 0; + } + body.mobile .dropdown-menu.mobile-bottom-menu.show { --dropdown-bottom: 0px; + padding-bottom: calc(max(var(--menu-padding-size), env(safe-area-inset-bottom))) !important; } #mobile-sidebar-container { diff --git a/apps/client/src/stylesheets/theme-next/pages.css b/apps/client/src/stylesheets/theme-next/pages.css index e6125e80e2..a5041cd553 100644 --- a/apps/client/src/stylesheets/theme-next/pages.css +++ b/apps/client/src/stylesheets/theme-next/pages.css @@ -57,12 +57,12 @@ height: 18px; } -/* +/* * SEARCH PAGE */ /* Button bar */ - .search-definition-widget .search-setting-table tbody:last-child div { + .search-definition-widget .search-setting-table .search-actions-container { justify-content: flex-end; gap: 8px; } @@ -143,7 +143,7 @@ /* * OPTIONS PAGES */ - + :root { --options-card-min-width: 500px; --options-card-max-width: 900px; @@ -335,4 +335,4 @@ nav.options-section-tabs + .options-section { .etapi-options-section div { height: auto !important; -} \ No newline at end of file +} diff --git a/apps/client/src/translations/en/translation.json b/apps/client/src/translations/en/translation.json index 3c2c5f6c0f..9f33fd8c35 100644 --- a/apps/client/src/translations/en/translation.json +++ b/apps/client/src/translations/en/translation.json @@ -662,7 +662,8 @@ "show-cheatsheet": "Show Cheatsheet", "toggle-zen-mode": "Zen Mode", "new-version-available": "New Update Available", - "download-update": "Get Version {{latestVersion}}" + "download-update": "Get Version {{latestVersion}}", + "search_notes": "Search notes" }, "zen_mode": { "button_exit": "Exit Zen Mode" @@ -761,7 +762,8 @@ "note_revisions": "Note revisions", "error_cannot_get_branch_id": "Cannot get branchId for notePath '{{notePath}}'", "error_unrecognized_command": "Unrecognized command {{command}}", - "backlinks": "Backlinks" + "backlinks": "Backlinks", + "content_language_switcher": "Content language: {{language}}" }, "note_icon": { "change_note_icon": "Change note icon", @@ -906,6 +908,7 @@ "debug": "debug", "debug_description": "Debug will print extra debugging information into the console to aid in debugging complex queries", "action": "action", + "option": "option", "search_button": "Search", "search_execute": "Search & Execute actions", "save_to_note": "Save to note", @@ -2277,5 +2280,8 @@ "title_one": "{{count}} tab", "title_other": "{{count}} tabs", "more_options": "More options" + }, + "bookmark_buttons": { + "bookmarks": "Bookmarks" } } diff --git a/apps/client/src/widgets/NoteDetail.css b/apps/client/src/widgets/NoteDetail.css index 0f6cdbfb03..b07c6aa061 100644 --- a/apps/client/src/widgets/NoteDetail.css +++ b/apps/client/src/widgets/NoteDetail.css @@ -12,6 +12,7 @@ .fixed-note-tree-container { height: 60%; border-bottom: 1px solid var(--main-border-color); + overflow: auto; .tree-wrapper { padding: 0; diff --git a/apps/client/src/widgets/NoteDetail.tsx b/apps/client/src/widgets/NoteDetail.tsx index 4c3e778f1b..78450811da 100644 --- a/apps/client/src/widgets/NoteDetail.tsx +++ b/apps/client/src/widgets/NoteDetail.tsx @@ -38,7 +38,7 @@ export default function NoteDetail() { const [ noteTypesToRender, setNoteTypesToRender ] = useState<{ [ key in ExtendedNoteType ]?: (props: TypeWidgetProps) => VNode }>({}); const [ activeNoteType, setActiveNoteType ] = useState(); const widgetRequestId = useRef(0); - const hasFixedTree = noteContext?.hoistedNoteId === "_lbMobileRoot" && isMobile(); + const hasFixedTree = note && noteContext?.hoistedNoteId === "_lbMobileRoot" && isMobile() && note.noteId.startsWith("_lbMobile"); const props: TypeWidgetProps = { note: note!, @@ -216,7 +216,7 @@ export default function NoteDetail() { "fixed-tree": hasFixedTree })} > - {hasFixedTree && } + {hasFixedTree && } {Object.entries(noteTypesToRender).map(([ itemType, Element ]) => { return new NoteTreeWidget(), { noteContext }); return
{treeEl}
; } diff --git a/apps/client/src/widgets/buttons/global_menu.tsx b/apps/client/src/widgets/buttons/global_menu.tsx index 0986c8ad29..9db0393692 100644 --- a/apps/client/src/widgets/buttons/global_menu.tsx +++ b/apps/client/src/widgets/buttons/global_menu.tsx @@ -45,6 +45,10 @@ export default function GlobalMenu({ isHorizontalLayout }: { isHorizontalLayout: noDropdownListStyle mobileBackdrop > + {isMobile() && <> + + + } @@ -105,7 +109,7 @@ function BrowserOnlyOptions() { function DevelopmentOptions({ dropStart }: { dropStart: boolean }) { return <> - + {experimentalFeatures.map((feature) => ( diff --git a/apps/client/src/widgets/collections/board/index.css b/apps/client/src/widgets/collections/board/index.css index 9d2e44b0e3..4851410db8 100644 --- a/apps/client/src/widgets/collections/board/index.css +++ b/apps/client/src/widgets/collections/board/index.css @@ -10,12 +10,6 @@ --card-padding: 0.6em; } -body.mobile .board-view { - scroll-snap-type: x mandatory; - -webkit-overflow-scrolling: touch; - scroll-behavior: smooth; -} - .board-view-container { height: 100%; display: flex; @@ -26,6 +20,12 @@ body.mobile .board-view { overflow-x: auto; } +body.mobile .board-view-container { + scroll-snap-type: x mandatory; + -webkit-overflow-scrolling: touch; + scroll-behavior: smooth; +} + .board-view-container .board-column { width: 250px; flex-shrink: 0; diff --git a/apps/client/src/widgets/containers/root_container.ts b/apps/client/src/widgets/containers/root_container.ts index e81d867ef4..ea79893d19 100644 --- a/apps/client/src/widgets/containers/root_container.ts +++ b/apps/client/src/widgets/containers/root_container.ts @@ -18,14 +18,12 @@ import FlexContainer from "./flex_container.js"; * - `#root-container.vertical-layout`, if the current layout is horizontal. */ export default class RootContainer extends FlexContainer { - private originalViewportHeight: number; constructor(isHorizontalLayout: boolean) { super(isHorizontalLayout ? "column" : "row"); this.id("root-widget"); this.css("height", "100dvh"); - this.originalViewportHeight = getViewportHeight(); } render(): JQuery { @@ -40,6 +38,7 @@ export default class RootContainer extends FlexContainer { this.#setThemeCapabilities(); this.#setLocaleAndDirection(options.get("locale")); this.#setExperimentalFeatures(); + this.#initPWATopbarColor(); return super.render(); } @@ -65,8 +64,12 @@ export default class RootContainer extends FlexContainer { } #onMobileResize() { - const currentViewportHeight = getViewportHeight(); - const isKeyboardOpened = (currentViewportHeight < this.originalViewportHeight); + const viewportHeight = window.visualViewport?.height ?? window.innerHeight; + const windowHeight = window.innerHeight; + + // If viewport is significantly smaller, keyboard is likely open + const isKeyboardOpened = windowHeight - viewportHeight > 150; + this.$widget.toggleClass("virtual-keyboard-opened", isKeyboardOpened); } @@ -113,8 +116,23 @@ export default class RootContainer extends FlexContainer { document.body.lang = locale; document.body.dir = correspondingLocale?.rtl ? "rtl" : "ltr"; } + + #initPWATopbarColor() { + if (!utils.isPWA()) return; + const tracker = $("#background-color-tracker"); + + if (tracker.length) { + const applyThemeColor = () => { + let meta = $("meta[name='theme-color']"); + if (!meta.length) { + meta = $(``).appendTo($("head")); + } + meta.attr("content", tracker.css("color")); + }; + + tracker.on("transitionend", applyThemeColor); + applyThemeColor(); + } + } } -function getViewportHeight() { - return window.visualViewport?.height ?? window.innerHeight; -} diff --git a/apps/client/src/widgets/launch_bar/BookmarkButtons.tsx b/apps/client/src/widgets/launch_bar/BookmarkButtons.tsx index f3b88c7aa3..81f2c6e878 100644 --- a/apps/client/src/widgets/launch_bar/BookmarkButtons.tsx +++ b/apps/client/src/widgets/launch_bar/BookmarkButtons.tsx @@ -1,11 +1,16 @@ -import { useContext, useMemo } from "preact/hooks"; -import { LaunchBarContext, LaunchBarDropdownButton, useLauncherIconAndTitle } from "./launch_bar_widgets"; -import { CSSProperties } from "preact"; -import type FNote from "../../entities/fnote"; -import { useChildNotes, useNoteLabelBoolean } from "../react/hooks"; import "./BookmarkButtons.css"; + +import { CSSProperties } from "preact"; +import { useContext, useMemo } from "preact/hooks"; + +import type FNote from "../../entities/fnote"; +import { t } from "../../services/i18n"; +import { FormDropdownSubmenu, FormListItem } from "../react/FormList"; +import { useChildNotes, useNote, useNoteIcon, useNoteLabelBoolean } from "../react/hooks"; import NoteLink from "../react/NoteLink"; -import { CustomNoteLauncher } from "./GenericButtons"; +import ResponsiveContainer from "../react/ResponsiveContainer"; +import { CustomNoteLauncher, launchCustomNoteLauncher } from "./GenericButtons"; +import { LaunchBarContext, LaunchBarDropdownButton, useLauncherIconAndTitle } from "./launch_bar_widgets"; const PARENT_NOTE_ID = "_lbBookmarks"; @@ -19,17 +24,64 @@ export default function BookmarkButtons() { const childNotes = useChildNotes(PARENT_NOTE_ID); return ( -
- {childNotes?.map(childNote => )} -
- ) + + {childNotes?.map(childNote => )} + + } + mobile={ + + {childNotes?.map(childNote => )} + + } + /> + ); } function SingleBookmark({ note }: { note: FNote }) { const [ bookmarkFolder ] = useNoteLabelBoolean(note, "bookmarkFolder"); - return bookmarkFolder - ? - : note.noteId} /> + return + : note.noteId} /> + } + mobile={} + />; +} + +function MobileBookmarkItem({ noteId, bookmarkFolder }: { noteId: string, bookmarkFolder: boolean }) { + const note = useNote(noteId); + const noteIcon = useNoteIcon(note); + if (!note) return null; + + return ( + !bookmarkFolder + ? launchCustomNoteLauncher(e, { launcherNote: note, getTargetNoteId: () => note.noteId })}>{note.title} + : + ); +} + +function MobileBookmarkFolder({ note }: { note: FNote }) { + const childNotes = useChildNotes(note.noteId); + + return ( + + {childNotes.map(childNote => ( + launchCustomNoteLauncher(e, { launcherNote: childNote, getTargetNoteId: () => childNote.noteId })} + > + {childNote.title} + + ))} + + ); } function BookmarkFolder({ note }: { note: FNote }) { @@ -55,5 +107,5 @@ function BookmarkFolder({ note }: { note: FNote }) { - ) + ); } diff --git a/apps/client/src/widgets/launch_bar/GenericButtons.tsx b/apps/client/src/widgets/launch_bar/GenericButtons.tsx index 70fe0fccef..59c158dec1 100644 --- a/apps/client/src/widgets/launch_bar/GenericButtons.tsx +++ b/apps/client/src/widgets/launch_bar/GenericButtons.tsx @@ -7,32 +7,18 @@ import { isCtrlKey } from "../../services/utils"; import { useGlobalShortcut, useNoteLabel } from "../react/hooks"; import { LaunchBarActionButton, useLauncherIconAndTitle } from "./launch_bar_widgets"; -export function CustomNoteLauncher({ launcherNote, getTargetNoteId, getHoistedNoteId }: { +export function CustomNoteLauncher(props: { launcherNote: FNote; getTargetNoteId: (launcherNote: FNote) => string | null | Promise; getHoistedNoteId?: (launcherNote: FNote) => string | null; keyboardShortcut?: string; }) { + const { launcherNote, getTargetNoteId } = props; const { icon, title } = useLauncherIconAndTitle(launcherNote); const launch = useCallback(async (evt: MouseEvent | KeyboardEvent) => { - if (evt.which === 3) { - return; - } - - const targetNoteId = await getTargetNoteId(launcherNote); - if (!targetNoteId) return; - - const hoistedNoteIdWithDefault = getHoistedNoteId?.(launcherNote) || appContext.tabManager.getActiveContext()?.hoistedNoteId; - const ctrlKey = isCtrlKey(evt); - - if ((evt.which === 1 && ctrlKey) || evt.which === 2) { - const activate = !!evt.shiftKey; - await appContext.tabManager.openInNewTab(targetNoteId, hoistedNoteIdWithDefault, activate); - } else { - await appContext.tabManager.openInSameTab(targetNoteId, hoistedNoteIdWithDefault); - } - }, [ launcherNote, getTargetNoteId, getHoistedNoteId ]); + await launchCustomNoteLauncher(evt, props); + }, [ props ]); // Keyboard shortcut. const [ shortcut ] = useNoteLabel(launcherNote, "keyboardShortcut"); @@ -54,3 +40,24 @@ export function CustomNoteLauncher({ launcherNote, getTargetNoteId, getHoistedNo /> ); } + +export async function launchCustomNoteLauncher(evt: MouseEvent | KeyboardEvent, { launcherNote, getTargetNoteId, getHoistedNoteId }: { + launcherNote: FNote; + getTargetNoteId: (launcherNote: FNote) => string | null | Promise; + getHoistedNoteId?: (launcherNote: FNote) => string | null; +}) { + if (evt.which === 3) return; + + const targetNoteId = await getTargetNoteId(launcherNote); + if (!targetNoteId) return; + + const hoistedNoteIdWithDefault = getHoistedNoteId?.(launcherNote) || appContext.tabManager.getActiveContext()?.hoistedNoteId; + const ctrlKey = isCtrlKey(evt); + + if ((evt.which === 1 && ctrlKey) || evt.which === 2) { + const activate = !!evt.shiftKey; + await appContext.tabManager.openInNewTab(targetNoteId, hoistedNoteIdWithDefault, activate); + } else { + await appContext.tabManager.openInSameTab(targetNoteId, hoistedNoteIdWithDefault); + } +} diff --git a/apps/client/src/widgets/launch_bar/launch_bar_widgets.tsx b/apps/client/src/widgets/launch_bar/launch_bar_widgets.tsx index bb4a053c89..639b5aede7 100644 --- a/apps/client/src/widgets/launch_bar/launch_bar_widgets.tsx +++ b/apps/client/src/widgets/launch_bar/launch_bar_widgets.tsx @@ -49,6 +49,7 @@ export function LaunchBarDropdownButton({ children, icon, dropdownOptions, ...pr placement: isHorizontalLayout ? "bottom" : "right" } }} + mobileBackdrop {...props} >{children} ); diff --git a/apps/client/src/widgets/layout/NoteTitleActions.css b/apps/client/src/widgets/layout/NoteTitleActions.css index b1aa37d89c..4ae4c15816 100644 --- a/apps/client/src/widgets/layout/NoteTitleActions.css +++ b/apps/client/src/widgets/layout/NoteTitleActions.css @@ -50,6 +50,9 @@ body.experimental-feature-new-layout { } } + } + + body.desktop .title-actions { > .collapsible, > .note-type-switcher { padding-inline-start: calc(24px - var(--title-actions-padding-start)); @@ -57,3 +60,4 @@ body.experimental-feature-new-layout { } } } + diff --git a/apps/client/src/widgets/layout/NoteTitleActions.tsx b/apps/client/src/widgets/layout/NoteTitleActions.tsx index 7c4e68698a..73ad3e0e89 100644 --- a/apps/client/src/widgets/layout/NoteTitleActions.tsx +++ b/apps/client/src/widgets/layout/NoteTitleActions.tsx @@ -23,7 +23,7 @@ export default function NoteTitleActions() { {noteType === "search" && } - {viewScope?.viewMode === "default" && } + {(!viewScope?.viewMode || viewScope.viewMode === "default") && } ); } diff --git a/apps/client/src/widgets/layout/StatusBar.css b/apps/client/src/widgets/layout/StatusBar.css index 6418630dba..a0b22c8d26 100644 --- a/apps/client/src/widgets/layout/StatusBar.css +++ b/apps/client/src/widgets/layout/StatusBar.css @@ -58,33 +58,6 @@ .dropdown-note-info { padding: 1em !important; - - ul { - --row-block-margin: .2em; - - list-style-type: none; - padding: 0; - margin: 0; - margin-top: calc(0px - var(--row-block-margin)); - margin-bottom: 12px; - display: table; - - li { - display: table-row; - - > strong { - display: table-cell; - padding: var(--row-block-margin) 0; - opacity: .5; - } - - > span { - display: table-cell; - user-select: text; - padding-left: 2em; - } - } - } } .dropdown-note-paths { @@ -144,16 +117,50 @@ } - div.similar-notes-widget div.similar-notes-wrapper { - max-height: unset; - } - button.select-button:not(:focus-visible) { outline: none; } } +body.experimental-feature-new-layout .note-info-content { + ul { + --row-block-margin: .2em; + + list-style-type: none; + padding: 0; + margin: 0; + margin-top: calc(0px - var(--row-block-margin)); + margin-bottom: 12px; + display: table; + + li { + display: table-row; + + > strong { + display: table-cell; + padding: var(--row-block-margin) 0; + opacity: .5; + } + + > span { + display: table-cell; + user-select: text; + padding-left: 2em; + } + } + } +} + +body.experimental-feature-new-layout div.similar-notes-widget div.similar-notes-wrapper { + max-height: unset; +} + +body.experimental-feature-new-layout.mobile div.similar-notes-widget div.similar-notes-wrapper { + max-height: unset; + padding: 0; +} + .bottom-panel { margin: 0 !important; padding: 0; diff --git a/apps/client/src/widgets/layout/StatusBar.tsx b/apps/client/src/widgets/layout/StatusBar.tsx index bfc9b02648..c608c8f203 100644 --- a/apps/client/src/widgets/layout/StatusBar.tsx +++ b/apps/client/src/widgets/layout/StatusBar.tsx @@ -212,8 +212,8 @@ export function getLocaleName(locale: Locale | null | undefined) { //#region Note info & Similar interface NoteInfoContext extends StatusBarContext { - similarNotesShown: boolean; - setSimilarNotesShown: (value: boolean) => void; + similarNotesShown?: boolean; + setSimilarNotesShown?: (value: boolean) => void; } export function NoteInfoBadge(context: NoteInfoContext) { @@ -225,7 +225,7 @@ export function NoteInfoBadge(context: NoteInfoContext) { // Keyboard shortcut. useTriliumEvent("toggleRibbonTabNoteInfo", () => enabled && dropdownRef.current?.show()); - useTriliumEvent("toggleRibbonTabSimilarNotes", () => setSimilarNotesShown(!similarNotesShown)); + useTriliumEvent("toggleRibbonTabSimilarNotes", () => setSimilarNotesShown && setSimilarNotesShown(!similarNotesShown)); return (enabled && ; +export function NoteInfoContent({ note, setSimilarNotesShown, noteType, dropdownRef }: Pick & { + dropdownRef?: RefObject; noteType: NoteType; }) { const { metadata, ...sizeProps } = useNoteMetadata(note); @@ -251,7 +251,7 @@ function NoteInfoContent({ note, setSimilarNotesShown, noteType, dropdownRef }: const noteTypeMapping = useMemo(() => NOTE_TYPES.find(t => t.type === noteType), [ noteType ]); return ( - <> +
    {originalFileName && } @@ -262,14 +262,14 @@ function NoteInfoContent({ note, setSimilarNotesShown, noteType, dropdownRef }: } />
- { - dropdownRef.current?.hide(); + dropdownRef?.current?.hide(); setSimilarNotesShown(true); }} - /> - + />} +
); } diff --git a/apps/client/src/widgets/mobile_widgets/mobile_detail_menu.css b/apps/client/src/widgets/mobile_widgets/mobile_detail_menu.css new file mode 100644 index 0000000000..f3e1503471 --- /dev/null +++ b/apps/client/src/widgets/mobile_widgets/mobile_detail_menu.css @@ -0,0 +1,3 @@ +.code-note-switcher-modal .dropdown-menu { + background: none !important; +} diff --git a/apps/client/src/widgets/mobile_widgets/mobile_detail_menu.tsx b/apps/client/src/widgets/mobile_widgets/mobile_detail_menu.tsx index 358a61ffc3..909d3b6288 100644 --- a/apps/client/src/widgets/mobile_widgets/mobile_detail_menu.tsx +++ b/apps/client/src/widgets/mobile_widgets/mobile_detail_menu.tsx @@ -1,17 +1,24 @@ +import "./mobile_detail_menu.css"; + import { Dropdown as BootstrapDropdown } from "bootstrap"; import { createPortal, useRef, useState } from "preact/compat"; import FNote, { NotePathRecord } from "../../entities/fnote"; import { t } from "../../services/i18n"; import note_create from "../../services/note_create"; +import server from "../../services/server"; import { BacklinksList, useBacklinkCount } from "../FloatingButtonsDefinitions"; +import { getLocaleName, NoteInfoContent } from "../layout/StatusBar"; import ActionButton from "../react/ActionButton"; -import { FormDropdownDivider, FormListItem } from "../react/FormList"; -import { useNoteContext } from "../react/hooks"; +import { FormDropdownDivider, FormDropdownSubmenu, FormListItem } from "../react/FormList"; +import { useNoteContext, useNoteProperty } from "../react/hooks"; import Modal from "../react/Modal"; +import { NoteTypeCodeNoteList, useLanguageSwitcher, useMimeTypes } from "../ribbon/BasicPropertiesTab"; import { NoteContextMenu } from "../ribbon/NoteActions"; import NoteActionsCustom from "../ribbon/NoteActionsCustom"; import { NotePathsWidget, useSortedNotePaths } from "../ribbon/NotePathsTab"; +import SimilarNotesTab from "../ribbon/SimilarNotesTab"; +import { useProcessedLocales } from "../type_widgets/options/components/LocaleSelector"; export default function MobileDetailMenu() { const dropdownRef = useRef(null); @@ -20,6 +27,9 @@ export default function MobileDetailMenu() { const isMainContext = noteContext?.isMainContext(); const [ backlinksModalShown, setBacklinksModalShown ] = useState(false); const [ notePathsModalShown, setNotePathsModalShown ] = useState(false); + const [ noteInfoModalShown, setNoteInfoModalShown ] = useState(false); + const [ similarNotesModalShown, setSimilarNotesModalShown ] = useState(false); + const [ codeNoteSwitcherModalShown, setCodeNoteSwitcherModalShown ] = useState(false); const sortedNotePaths = useSortedNotePaths(note, hoistedNoteId); const backlinksCount = useBacklinkCount(note, viewScope?.viewMode === "default"); @@ -36,7 +46,7 @@ export default function MobileDetailMenu() { + itemsAtStart={<>
} } + itemsNearNoteSettings={<> + {note.type === "text" && } + {note.type === "code" && setCodeNoteSwitcherModalShown(true)}>{t("status_bar.code_note_switcher")}} + setNoteInfoModalShown(true)}>{t("note_info_widget.title")} + setSimilarNotesModalShown(true)}>{t("similar_notes.title")} + + } /> ) : ( + + + ), document.body)}
); } -function BacklinksModal({ note, modalShown, setModalShown }: { note: FNote | null | undefined, modalShown: boolean, setModalShown: (shown: boolean) => void }) { +function ContentLanguageSelector({ note }: { note: FNote | null | undefined }) { + const { locales, DEFAULT_LOCALE, currentNoteLanguage, setCurrentNoteLanguage } = useLanguageSwitcher(note); + const { activeLocale, processedLocales } = useProcessedLocales(locales, DEFAULT_LOCALE, currentNoteLanguage ?? DEFAULT_LOCALE.id); + + return ( + + {processedLocales.map((locale, index) => + (typeof locale === "object") ? ( + setCurrentNoteLanguage(locale.id)} + >{locale.name} + ) : ( + + ) + )} + + ); +} + +interface WithModal { + modalShown: boolean; + setModalShown: (shown: boolean) => void; +} + +function BacklinksModal({ note, modalShown, setModalShown }: { note: FNote | null | undefined } & WithModal) { return ( void }) { +function NotePathsModal({ note, modalShown, notePath, sortedNotePaths, setModalShown }: { note: FNote | null | undefined, sortedNotePaths: NotePathRecord[] | undefined, notePath: string | null | undefined } & WithModal) { return ( ); } + +function NoteInfoModal({ note, modalShown, setModalShown }: { note: FNote | null | undefined } & WithModal) { + return ( + setModalShown(false)} + > + {note && } + + ); +} + +function SimilarNotesModal({ note, modalShown, setModalShown }: { note: FNote | null | undefined } & WithModal) { + return ( + setModalShown(false)} + > + + + ); +} + +function CodeNoteSwitcherModal({ note, modalShown, setModalShown }: { note: FNote | null | undefined } & WithModal) { + const currentNoteMime = useNoteProperty(note, "mime"); + const mimeTypes = useMimeTypes(); + + return ( + setModalShown(false)} + > +
+ {note && { + server.put(`notes/${note.noteId}/type`, { type, mime }); + setModalShown(false); + }} + />} +
+
+ ); +} diff --git a/apps/client/src/widgets/react/ResponsiveContainer.tsx b/apps/client/src/widgets/react/ResponsiveContainer.tsx new file mode 100644 index 0000000000..6c077a5c06 --- /dev/null +++ b/apps/client/src/widgets/react/ResponsiveContainer.tsx @@ -0,0 +1,12 @@ +import { ComponentChildren } from "preact"; + +import { isMobile } from "../../services/utils"; + +interface ResponsiveContainerProps { + mobile?: ComponentChildren; + desktop?: ComponentChildren; +} + +export default function ResponsiveContainer({ mobile, desktop }: ResponsiveContainerProps) { + return (isMobile() ? mobile : desktop); +} diff --git a/apps/client/src/widgets/ribbon/BasicPropertiesTab.tsx b/apps/client/src/widgets/ribbon/BasicPropertiesTab.tsx index 9b800bebb9..5dbd0d29cb 100644 --- a/apps/client/src/widgets/ribbon/BasicPropertiesTab.tsx +++ b/apps/client/src/widgets/ribbon/BasicPropertiesTab.tsx @@ -1,5 +1,4 @@ import { MimeType, NoteType, ToggleInParentResponse } from "@triliumnext/commons"; -import { ComponentChildren } from "preact"; import { createPortal } from "preact/compat"; import { Dispatch, StateUpdater, useCallback, useEffect, useMemo, useState } from "preact/hooks"; @@ -117,19 +116,18 @@ export function NoteTypeDropdownContent({ currentNoteType, currentNoteMime, note onClick={() => changeNoteType(type, mime)} >{title} ); - } else { - return ( - <> - - - {title} - - - ); } + return ( + <> + + + {title} + + + ); })} {!noCodeNotes && } @@ -141,7 +139,7 @@ export function NoteTypeCodeNoteList({ currentMimeType, mimeTypes, changeNoteTyp currentMimeType?: string; mimeTypes: MimeType[]; changeNoteType(type: NoteType, mime: string): void; - setModalShown(shown: boolean): void; + setModalShown?(shown: boolean): void; }) { return ( <> @@ -155,8 +153,10 @@ export function NoteTypeCodeNoteList({ currentMimeType, mimeTypes, changeNoteTyp ))} - - setModalShown(true)}>{t("basic_properties.configure_code_notes")} + {setModalShown && <> + + setModalShown(true)}>{t("basic_properties.configure_code_notes")} + } ); } @@ -195,7 +195,7 @@ function ProtectedNoteSwitch({ note }: { note?: FNote | null }) { onChange={(shouldProtect) => note && protected_session.protectNote(note.noteId, shouldProtect, false)} />
- ) + ); } function EditabilitySelect({ note }: { note?: FNote | null }) { @@ -417,9 +417,9 @@ function findTypeTitle(type?: NoteType, mime?: string | null) { const found = mimeTypes.find((mt) => mt.mime === mime); return found ? found.title : mime; - } else { - const noteType = NOTE_TYPES.find((nt) => nt.type === type); - - return noteType ? noteType.title : type; } + const noteType = NOTE_TYPES.find((nt) => nt.type === type); + + return noteType ? noteType.title : type; + } diff --git a/apps/client/src/widgets/ribbon/NoteActions.tsx b/apps/client/src/widgets/ribbon/NoteActions.tsx index daeab278cc..b818672709 100644 --- a/apps/client/src/widgets/ribbon/NoteActions.tsx +++ b/apps/client/src/widgets/ribbon/NoteActions.tsx @@ -63,10 +63,11 @@ function RevisionsButton({ note }: { note: FNote }) { type ItemToFocus = "basic-properties"; -export function NoteContextMenu({ note, noteContext, extraItems, dropdownRef: externalDropdownRef }: { +export function NoteContextMenu({ note, noteContext, itemsAtStart, itemsNearNoteSettings, dropdownRef: externalDropdownRef }: { note: FNote, noteContext?: NoteContext, - extraItems?: ComponentChildren; + itemsAtStart?: ComponentChildren; + itemsNearNoteSettings?: ComponentChildren; dropdownRef?: RefObject; }) { const dropdownRef = useSyncedRef(externalDropdownRef, null); @@ -112,7 +113,7 @@ export function NoteContextMenu({ note, noteContext, extraItems, dropdownRef: ex onHidden={() => itemToFocusRef.current = null } mobileBackdrop > - {extraItems} + {itemsAtStart} {isReadOnly && <> } + {itemsNearNoteSettings} + parentComponent?.triggerCommand("showImportDialog", { noteId: note.noteId })} /> diff --git a/apps/client/src/widgets/ribbon/SearchDefinitionTab.tsx b/apps/client/src/widgets/ribbon/SearchDefinitionTab.tsx index 9d182a138d..2d58fc1913 100644 --- a/apps/client/src/widgets/ribbon/SearchDefinitionTab.tsx +++ b/apps/client/src/widgets/ribbon/SearchDefinitionTab.tsx @@ -2,6 +2,7 @@ import "./SearchDefinitionTab.css"; import { SaveSearchNoteResponse } from "@triliumnext/commons"; import { useContext, useEffect, useState } from "preact/hooks"; +import { Fragment } from "preact/jsx-runtime"; import appContext from "../../components/app_context"; import FNote from "../../entities/fnote"; @@ -15,12 +16,13 @@ import tree from "../../services/tree"; import { getErrorMessage } from "../../services/utils"; import ws from "../../services/ws"; import RenameNoteBulkAction from "../bulk_actions/note/rename_note"; -import Button from "../react/Button"; +import Button, { SplitButton } from "../react/Button"; import Dropdown from "../react/Dropdown"; import { FormListHeader, FormListItem } from "../react/FormList"; import { useTriliumEvent } from "../react/hooks"; import Icon from "../react/Icon"; import { ParentComponent } from "../react/react_utils"; +import ResponsiveContainer from "../react/ResponsiveContainer"; import { TabContext } from "./ribbon-interface"; import { SEARCH_OPTIONS, SearchOption } from "./SearchDefinitionOptions"; @@ -84,15 +86,32 @@ export default function SearchDefinitionTab({ note, ntxId, hidden }: Pick {t("search_definition.add_search_option")} - {searchOptions?.availableOptions.map(({ icon, label, tooltip, attributeName, attributeType, defaultValue }) => ( -