(`note-map/${note.noteId}/backlinks`).then(async (backlinks) => {
// prefetch all
const noteIds = backlinks
- .filter(bl => "noteId" in bl)
- .map((bl) => bl.noteId);
+ .filter(bl => "noteId" in bl)
+ .map((bl) => bl.noteId);
await froca.getNotes(noteIds);
setBacklinks(backlinks);
});
diff --git a/apps/client/src/widgets/NoteTitleDetails.tsx b/apps/client/src/widgets/NoteTitleDetails.tsx
index 793a3a593..58f4da0f7 100644
--- a/apps/client/src/widgets/NoteTitleDetails.tsx
+++ b/apps/client/src/widgets/NoteTitleDetails.tsx
@@ -1,23 +1,60 @@
-import { t } from "../services/i18n";
+import { type ComponentChild } from "preact";
+
import { formatDateTime } from "../utils/formatters";
-import { useNoteContext } from "./react/hooks";
+import { useNoteContext, useStaticTooltip } from "./react/hooks";
import { joinElements } from "./react/react_utils";
import { useNoteMetadata } from "./ribbon/NoteInfoTab";
+import { Trans } from "react-i18next";
+import { useRef } from "preact/hooks";
export default function NoteTitleDetails() {
- const { note } = useNoteContext();
+ const { note, noteContext } = useNoteContext();
const { metadata } = useNoteMetadata(note);
+ const isHiddenNote = note?.noteId.startsWith("_");
+ const isDefaultView = noteContext?.viewScope?.viewMode === "default";
+
+ const items: ComponentChild[] = [
+ (isDefaultView && !isHiddenNote && metadata?.dateCreated &&
+ ),
+ (isDefaultView && !isHiddenNote && metadata?.dateModified &&
+ )
+ ].filter(item => !!item);
return (
- {joinElements([
- metadata?.dateCreated &&
- {t("note_title.created_on", { date: formatDateTime(metadata.dateCreated, "medium", "none")} )}
- ,
- metadata?.dateModified &&
- {t("note_title.last_modified", { date: formatDateTime(metadata.dateModified, "medium", "none")} )}
-
- ], " • ")}
+ {joinElements(items, " • ")}
);
}
+
+function TextWithValue({ i18nKey, value, valueTooltip }: {
+ i18nKey: string;
+ value: string;
+ valueTooltip: string;
+}) {
+ const listItemRef = useRef(null);
+ useStaticTooltip(listItemRef, {
+ selector: "span.value",
+ title: valueTooltip,
+ popperConfig: { placement: "bottom" }
+ });
+
+ return (
+
+ {value} as React.ReactElement
+ }}
+ />
+
+ );
+}
diff --git a/apps/client/src/widgets/attribute_widgets/attribute_detail.ts b/apps/client/src/widgets/attribute_widgets/attribute_detail.ts
index 2a7a55aef..8ae3ae674 100644
--- a/apps/client/src/widgets/attribute_widgets/attribute_detail.ts
+++ b/apps/client/src/widgets/attribute_widgets/attribute_detail.ts
@@ -12,6 +12,7 @@ import shortcutService from "../../services/shortcuts.js";
import appContext from "../../components/app_context.js";
import type { Attribute } from "../../services/attribute_parser.js";
import { focusSavedElement, saveFocusedElement } from "../../services/focus.js";
+import { isExperimentalFeatureEnabled } from "../../services/experimental_features.js";
const TPL = /*html*/`
- )
+ );
}
diff --git a/apps/client/src/widgets/note_title.css b/apps/client/src/widgets/note_title.css
index 7d39b6b02..8769c74ae 100644
--- a/apps/client/src/widgets/note_title.css
+++ b/apps/client/src/widgets/note_title.css
@@ -29,30 +29,73 @@ body.desktop .note-title-widget input.note-title {
font-size: 180%;
}
-body.experimental-feature-new-layout .title-row,
-body.experimental-feature-new-layout .title-details {
- max-width: var(--max-content-width);
-}
+body.experimental-feature-new-layout {
+ .title-row,
+ .title-details {
+ max-width: var(--max-content-width);
+ padding: 0;
+ padding-inline-start: 24px;
+ }
-body.experimental-feature-new-layout .title-row {
- margin-top: 2em;
- margin-left: 12px;
-}
+ .title-row {
+ margin-left: 12px;
-body.experimental-feature-new-layout .title-details {
- margin-top: 0;
- contain: none;
- padding: 0;
- padding-inline-start: 24px;
- opacity: 0.85;
- display: flex;
- gap: 0.25em;
- margin: 0;
- list-style-type: none;
- margin-bottom: 2em;
-}
+ .note-icon-widget {
+ padding: 0;
+ width: 41px;
+ }
+ }
-body.experimental-feature-new-layout.prefers-centered-content .title-row,
-body.experimental-feature-new-layout.prefers-centered-content .title-details {
- margin-inline: auto;
+ .note-split.type-code:not(.mime-text-x-sqlite) .title-row,
+ .note-split.type-code:not(.mime-text-x-sqlite) .title-details {
+ background-color: var(--main-background-color);
+ }
+
+ .title-details {
+ margin-top: 0;
+ contain: none;
+ display: flex;
+ gap: 0.25em;
+ margin: 0;
+ list-style-type: none;
+
+ span.value {
+ font-weight: 500;
+ }
+ }
+
+ .note-split.view-mode-default {
+ .title-row {
+ padding-top: 2em;
+ box-sizing: content-box;
+ }
+
+ .title-details {
+ padding-bottom: 2em;
+ }
+ }
+
+ .scrolling-container:has(> :is(.note-detail.full-height, .note-list-widget.full-height)) {
+ .title-row,
+ .title-details {
+ width: 100%;
+ max-width: unset;
+ padding-inline-start: 15px;
+ }
+
+ .title-row {
+ margin-top: 0;
+ }
+
+ .title-details {
+ margin-bottom: 0.2em;
+ opacity: 0.65;
+ font-size: 0.8em;
+ }
+ }
+
+ &.prefers-centered-content .title-row,
+ &.prefers-centered-content .title-details {
+ margin-inline: auto;
+ }
}
diff --git a/apps/client/src/widgets/note_wrapper.ts b/apps/client/src/widgets/note_wrapper.ts
index f3c61859d..d743d9ffa 100644
--- a/apps/client/src/widgets/note_wrapper.ts
+++ b/apps/client/src/widgets/note_wrapper.ts
@@ -62,6 +62,7 @@ export default class NoteWrapperWidget extends FlexContainer {
this.$widget.addClass(utils.getNoteTypeClass(note.type));
this.$widget.addClass(utils.getMimeTypeClass(note.mime));
+ this.$widget.addClass(`view-mode-${this.noteContext?.viewScope?.viewMode ?? "default"}`);
this.$widget.toggleClass(["bgfx", "options"], note.isOptions());
this.$widget.toggleClass("protected", note.isProtected);
diff --git a/apps/client/src/widgets/react/Dropdown.tsx b/apps/client/src/widgets/react/Dropdown.tsx
index 5416e38ac..dec0660c0 100644
--- a/apps/client/src/widgets/react/Dropdown.tsx
+++ b/apps/client/src/widgets/react/Dropdown.tsx
@@ -117,8 +117,8 @@ export default function Dropdown({ id, className, buttonClassName, isStatic, chi
aria-expanded="false"
id={id ?? ariaId}
disabled={disabled}
- onMouseOver={() => showTooltip()}
- onMouseLeave={() => hideTooltip()}
+ onMouseEnter={showTooltip}
+ onMouseLeave={hideTooltip}
{...buttonProps}
>
{text}
diff --git a/apps/client/src/widgets/react/FormList.tsx b/apps/client/src/widgets/react/FormList.tsx
index 5faab055f..0eb6108b8 100644
--- a/apps/client/src/widgets/react/FormList.tsx
+++ b/apps/client/src/widgets/react/FormList.tsx
@@ -161,11 +161,16 @@ export function FormDropdownDivider() {
return ;
}
-export function FormDropdownSubmenu({ icon, title, children }: { icon: string, title: ComponentChildren, children: ComponentChildren }) {
+export function FormDropdownSubmenu({ icon, title, children, dropStart }: {
+ icon: string,
+ title: ComponentChildren,
+ children: ComponentChildren,
+ dropStart?: boolean
+}) {
const [ openOnMobile, setOpenOnMobile ] = useState(false);
return (
-
+
{
@@ -184,5 +189,5 @@ export function FormDropdownSubmenu({ icon, title, children }: { icon: string, t
{children}
- )
+ );
}
diff --git a/apps/client/src/widgets/react/hooks.tsx b/apps/client/src/widgets/react/hooks.tsx
index fbcd7095e..38f0a1967 100644
--- a/apps/client/src/widgets/react/hooks.tsx
+++ b/apps/client/src/widgets/react/hooks.tsx
@@ -201,7 +201,7 @@ export function useTriliumOptionBool(name: OptionNames, needsRefresh?: boolean):
return [
(value === "true"),
(newValue) => setValue(newValue ? "true" : "false")
- ]
+ ];
}
/**
@@ -217,17 +217,18 @@ export function useTriliumOptionInt(name: OptionNames): [number, (newValue: numb
return [
(parseInt(value, 10)),
(newValue) => setValue(newValue)
- ]
+ ];
}
/**
* Similar to {@link useTriliumOption}, but the object value is parsed to and from a JSON instead of a string.
*
* @param name the name of the option to listen for.
+ * @param needsRefresh whether to reload the frontend whenever the value is changed.
* @returns an array where the first value is the current option value and the second value is the setter.
*/
-export function useTriliumOptionJson(name: OptionNames): [ T, (newValue: T) => Promise ] {
- const [ value, setValue ] = useTriliumOption(name);
+export function useTriliumOptionJson(name: OptionNames, needsRefresh?: boolean): [ T, (newValue: T) => Promise ] {
+ const [ value, setValue ] = useTriliumOption(name, needsRefresh);
useDebugValue(name);
return [
(JSON.parse(value) as T),
@@ -845,9 +846,9 @@ export function useGlobalShortcut(keyboardShortcut: string | null | undefined, h
export function useIsNoteReadOnly(note: FNote | null | undefined, noteContext: NoteContext | undefined) {
const [ isReadOnly, setIsReadOnly ] = useState(undefined);
- const enableEditing = useCallback(() => {
+ const enableEditing = useCallback((enabled = true) => {
if (noteContext?.viewScope) {
- noteContext.viewScope.readOnlyTemporarilyDisabled = true;
+ noteContext.viewScope.readOnlyTemporarilyDisabled = enabled;
appContext.triggerEvent("readOnlyTemporarilyDisabled", {noteContext});
}
}, [noteContext]);
@@ -862,7 +863,7 @@ export function useIsNoteReadOnly(note: FNote | null | undefined, noteContext: N
useTriliumEvent("readOnlyTemporarilyDisabled", ({noteContext: eventNoteContext}) => {
if (noteContext?.ntxId === eventNoteContext.ntxId) {
- setIsReadOnly(false);
+ setIsReadOnly(!noteContext.viewScope?.readOnlyTemporarilyDisabled);
}
});
diff --git a/apps/client/src/widgets/ribbon/NoteActions.tsx b/apps/client/src/widgets/ribbon/NoteActions.tsx
index 6adaf7f61..12655262e 100644
--- a/apps/client/src/widgets/ribbon/NoteActions.tsx
+++ b/apps/client/src/widgets/ribbon/NoteActions.tsx
@@ -13,7 +13,7 @@ import { isElectron as getIsElectron, isMac as getIsMac } from "../../services/u
import ws from "../../services/ws";
import ActionButton from "../react/ActionButton";
import Dropdown from "../react/Dropdown";
-import { FormDropdownDivider, FormListHeader, FormListItem } from "../react/FormList";
+import { FormDropdownDivider, FormDropdownSubmenu, FormListItem } from "../react/FormList";
import { useIsNoteReadOnly, useNoteContext, useNoteLabel, useNoteProperty, useTriliumOption } from "../react/hooks";
import { ParentComponent } from "../react/react_utils";
import { isExperimentalFeatureEnabled } from "../../services/experimental_features";
@@ -98,7 +98,7 @@ function NoteContextMenu({ note, noteContext }: { note: FNote, noteContext?: Not
}
-
+
-
+
window.open(`/?print=#root/${note.noteId}`, "_blank")}
>Open print page
- {note.type === "text" && (
- {
- noteContext?.getTextEditor(editor => {
- editor.editing.view.change(() => {
- throw new Error("Editor crashed.");
- });
+ {
+ noteContext?.getTextEditor(editor => {
+ editor.editing.view.change(() => {
+ throw new Error("Editor crashed.");
});
- }}>Crash editor)}
- >
+ });
+ }}>Crash editor
+
);
}
diff --git a/apps/client/src/widgets/ribbon/RibbonDefinition.ts b/apps/client/src/widgets/ribbon/RibbonDefinition.ts
index 280f7cdda..ab351637d 100644
--- a/apps/client/src/widgets/ribbon/RibbonDefinition.ts
+++ b/apps/client/src/widgets/ribbon/RibbonDefinition.ts
@@ -22,7 +22,7 @@ export const RIBBON_TAB_DEFINITIONS: TabConfiguration[] = [
{
title: t("classic_editor_toolbar.title"),
icon: "bx bx-text",
- show: async ({ note, noteContext }) => note?.type === "text"
+ show: async ({ note, noteContext }) => note?.type === "text" && noteContext?.viewScope?.viewMode === "default"
&& options.get("textNoteEditorType") === "ckeditor-classic"
&& !(await noteContext?.isReadOnly()),
toggleCommand: "toggleRibbonTabClassicEditor",
diff --git a/apps/client/src/widgets/shared_info.tsx b/apps/client/src/widgets/shared_info.tsx
index 954ceb5f0..cd6cf78f4 100644
--- a/apps/client/src/widgets/shared_info.tsx
+++ b/apps/client/src/widgets/shared_info.tsx
@@ -26,6 +26,7 @@ export default function SharedInfo() {
export function useShareInfo(note: FNote | null | undefined) {
const [ link, setLink ] = useState();
+ const [ linkHref, setLinkHref ] = useState();
const [ syncServerHost ] = useTriliumOption("syncServerHost");
function refresh() {
@@ -52,9 +53,10 @@ export function useShareInfo(note: FNote | null | undefined) {
}
setLink(`${link}`);
+ setLinkHref(link);
}
- useEffect(refresh, [ note ]);
+ useEffect(refresh, [ note, syncServerHost ]);
useTriliumEvent("entitiesReloaded", ({ loadResults }) => {
if (loadResults.getAttributeRows().find((attr) => attr.name?.startsWith("_share") && attributes.isAffecting(attr, note))) {
refresh();
@@ -63,7 +65,7 @@ export function useShareInfo(note: FNote | null | undefined) {
}
});
- return { link, isSharedExternally: !!syncServerHost };
+ return { link, linkHref, isSharedExternally: !!syncServerHost };
}
function getShareId(note: FNote) {
diff --git a/apps/client/src/widgets/type_widgets/options/advanced.tsx b/apps/client/src/widgets/type_widgets/options/advanced.tsx
index 958180063..4024a9d0e 100644
--- a/apps/client/src/widgets/type_widgets/options/advanced.tsx
+++ b/apps/client/src/widgets/type_widgets/options/advanced.tsx
@@ -158,7 +158,7 @@ function ExistingAnonymizedDatabases({ databases }: { databases: AnonymizedDbRes
))}
- )
+ );
}
function VacuumDatabaseOptions() {
@@ -175,11 +175,11 @@ function VacuumDatabaseOptions() {
}}
/>
- )
+ );
}
function ExperimentalOptions() {
- const [ enabledExperimentalFeatures, setEnabledExperimentalFeatures ] = useTriliumOptionJson("experimentalFeatures");
+ const [ enabledExperimentalFeatures, setEnabledExperimentalFeatures ] = useTriliumOptionJson("experimentalFeatures", true);
return (