From 7ec73698ab9576f351d9028f6cf2ec1bf983abcb Mon Sep 17 00:00:00 2001 From: SiriusXT <1160925501@qq.com> Date: Mon, 14 Apr 2025 17:20:35 +0800 Subject: [PATCH 01/10] Allow the tab row scroll --- src/public/app/widgets/tab_row.ts | 254 ++++++++++++++++---- src/public/stylesheets/theme-next/shell.css | 11 +- 2 files changed, 220 insertions(+), 45 deletions(-) diff --git a/src/public/app/widgets/tab_row.ts b/src/public/app/widgets/tab_row.ts index 4de766357..a1c731971 100644 --- a/src/public/app/widgets/tab_row.ts +++ b/src/public/app/widgets/tab_row.ts @@ -11,10 +11,11 @@ import type NoteContext from "../components/note_context.js"; const isDesktop = utils.isDesktop(); -const TAB_CONTAINER_MIN_WIDTH = 24; +const TAB_CONTAINER_MIN_WIDTH = 100; const TAB_CONTAINER_MAX_WIDTH = 240; const TAB_CONTAINER_LEFT_PADDING = 5; -const NEW_TAB_WIDTH = 32; +const SCROLL_BUTTON_WIDTH = 36; +const NEW_TAB_WIDTH = 36; const MIN_FILLER_WIDTH = isDesktop ? 50 : 15; const MARGIN_WIDTH = 5; @@ -32,6 +33,8 @@ const TAB_TPL = ` `; +const CONTAINER_ANCHOR_TPL = `
`; + const NEW_TAB_BUTTON_TPL = `
+
`; const FILLER_TPL = `
`; @@ -39,6 +42,7 @@ const TAB_ROW_TPL = `
- -
+
+
+
+
+
`; export default class TabRowWidget extends BasicWidget { @@ -244,11 +307,24 @@ export default class TabRowWidget extends BasicWidget { private draggabillyDragging?: Draggabilly | null; private $style!: JQuery; + private $tabScrollingContainer!: JQuery; + private $tabContainer!: JQuery; + private $scrollButtonLeft!: JQuery; + private $scrollButtonRight!: JQuery; + private $containerAnchor!: JQuery; private $filler!: JQuery; private $newTab!: JQuery; + private updateScrollTimeout: ReturnType | undefined; + + private newTabOuterWidth: number = 0; + private scrollButtonsOuterWidth: number = 0; doRender() { this.$widget = $(TAB_ROW_TPL); + this.$tabScrollingContainer = this.$widget.children(".tab-row-widget-scrolling-container"); + this.$tabContainer = this.$widget.find(".tab-row-widget-container"); + this.$scrollButtonLeft = this.$widget.children(".tab-scroll-button-left"); + this.$scrollButtonRight = this.$widget.children(".tab-scroll-button-right"); const documentStyle = window.getComputedStyle(document.documentElement); this.showNoteIcons = documentStyle.getPropertyValue("--tab-note-icons") === "true"; @@ -257,11 +333,13 @@ export default class TabRowWidget extends BasicWidget { this.setupStyle(); this.setupEvents(); + this.setupContainerAnchor(); this.setupDraggabilly(); this.setupNewButton(); this.setupFiller(); this.layoutTabs(); this.setVisibility(); + this.setupScrollEvents(); this.$widget.on("contextmenu", ".note-tab", (e) => { e.preventDefault(); @@ -300,6 +378,54 @@ export default class TabRowWidget extends BasicWidget { this.$widget.append(this.$style); } + scrollTabContainer(direction: number) { + const currentScrollLeft = this.$tabScrollingContainer?.scrollLeft() ?? 0; + this.$tabScrollingContainer[0].scrollTo({ + left: currentScrollLeft + direction, + behavior: "smooth" + }); + }; + + setupScrollEvents() { + this.$scrollButtonLeft[0].addEventListener('click', () => this.scrollTabContainer(-200)); + this.$scrollButtonRight[0].addEventListener('click', () => this.scrollTabContainer(200)); + + this.$tabScrollingContainer[0].addEventListener('wheel', (event) => { + const targetScrollLeft = event.deltaY*1.5; + this.scrollTabContainer(targetScrollLeft); + }); + + this.$tabScrollingContainer[0].addEventListener('scroll', () => { + clearTimeout(this.updateScrollTimeout); + this.updateScrollTimeout = setTimeout(() => { + this.updateScrollButtonState(); + }, 100); + }); + } + + updateScrollButtonState() { + const scrollLeft = this.$tabScrollingContainer[0].scrollLeft; + const scrollWidth = this.$tabScrollingContainer[0].scrollWidth; + const clientWidth = this.$tabScrollingContainer[0].clientWidth; + // Detect whether the scrollbar is at the far left or far right. + this.$scrollButtonLeft.toggleClass("disabled", Math.abs(scrollLeft) <= 1); + this.$scrollButtonRight.toggleClass("disabled", Math.abs(scrollLeft + clientWidth - scrollWidth) <= 1); + } + + setScrollButtonVisibility(show: boolean = true) { + if (show) { + this.$scrollButtonLeft.css("display", "flex"); + this.$scrollButtonRight.css("display", "flex"); + clearTimeout(this.updateScrollTimeout); + this.updateScrollTimeout = setTimeout(() => { + this.updateScrollButtonState(); + }, 200); + } else { + this.$scrollButtonLeft.css("display", "none"); + this.$scrollButtonRight.css("display", "none"); + } + } + setupEvents() { new ResizeObserver((_) => { this.cleanUpPreviouslyDraggedTabs(); @@ -317,14 +443,32 @@ export default class TabRowWidget extends BasicWidget { return Array.prototype.slice.call(this.$widget.find(".note-tab")); } - get $tabContainer() { - return this.$widget.find(".tab-row-widget-container"); + updateOuterWidth() { + if (this.newTabOuterWidth == 0) { + this.newTabOuterWidth = this.$newTab?.outerWidth(true) ?? 0; + } + if (this.scrollButtonsOuterWidth == 0) { + this.scrollButtonsOuterWidth = (this.$scrollButtonLeft?.outerWidth(true) ?? 0) + (this.$scrollButtonRight?.outerWidth(true) ?? 0); + } } + get tabWidths() { const numberOfTabs = this.tabEls.length; - const tabsContainerWidth = this.$tabContainer[0].clientWidth - NEW_TAB_WIDTH - MIN_FILLER_WIDTH; - const marginWidth = (numberOfTabs - 1) * MARGIN_WIDTH; + // this.$newTab may include margin, and using NEW_TAB_WIDTH could cause tabsContainerWidth to be slightly larger, + // resulting in misaligned scrollbars/buttons. Therefore, use outerwidth. + this.updateOuterWidth(); + let tabsContainerWidth = Math.floor(this.$widget.width() ?? 0); + tabsContainerWidth -= this.newTabOuterWidth + MIN_FILLER_WIDTH; + // Check whether the scroll buttons need to be displayed. + if ((TAB_CONTAINER_MIN_WIDTH + MARGIN_WIDTH) * numberOfTabs > tabsContainerWidth) { + tabsContainerWidth -= this.scrollButtonsOuterWidth; + this.setScrollButtonVisibility(true); + } else { + this.setScrollButtonVisibility(false); + } + + const marginWidth = (numberOfTabs - 1) * MARGIN_WIDTH + TAB_CONTAINER_LEFT_PADDING; const targetWidth = (tabsContainerWidth - marginWidth) / numberOfTabs; const clampedTargetWidth = Math.max(TAB_CONTAINER_MIN_WIDTH, Math.min(TAB_CONTAINER_MAX_WIDTH, targetWidth)); const flooredClampedTargetWidth = Math.floor(clampedTargetWidth); @@ -344,10 +488,6 @@ export default class TabRowWidget extends BasicWidget { } } - if (this.$filler) { - this.$filler.css("width", `${extraWidthRemaining + MIN_FILLER_WIDTH}px`); - } - return widths; } @@ -362,10 +502,9 @@ export default class TabRowWidget extends BasicWidget { position -= MARGIN_WIDTH; // the last margin should not be applied - const newTabPosition = position; - const fillerPosition = position + 32; + const anchorPosition = position; - return { tabPositions, newTabPosition, fillerPosition }; + return { tabPositions, anchorPosition }; } layoutTabs() { @@ -386,15 +525,14 @@ export default class TabRowWidget extends BasicWidget { let styleHTML = ""; - const { tabPositions, newTabPosition, fillerPosition } = this.getTabPositions(); + const { tabPositions, anchorPosition } = this.getTabPositions(); tabPositions.forEach((position, i) => { styleHTML += `.note-tab:nth-child(${i + 1}) { transform: translate3d(${position}px, 0, 0)} `; }); - styleHTML += `.note-new-tab { transform: translate3d(${newTabPosition}px, 0, 0) } `; - styleHTML += `.tab-row-filler { transform: translate3d(${fillerPosition}px, 0, 0) } `; - + styleHTML += `.tab-row-container-anchor { transform: translate3d(${anchorPosition}px, 0, 0) } `; + styleHTML += `.tab-row-widget-container {width: ${anchorPosition}px}`; this.$style.html(styleHTML); } @@ -406,8 +544,7 @@ export default class TabRowWidget extends BasicWidget { $tab.addClass("note-tab-was-just-added"); setTimeout(() => $tab.removeClass("note-tab-was-just-added"), 500); - - this.$newTab.before($tab); + this.$containerAnchor.before($tab); this.setVisibility(); this.setTabCloseEvent($tab); this.updateTitle($tab, t("tab_row.new_tab")); @@ -507,6 +644,7 @@ export default class TabRowWidget extends BasicWidget { setupDraggabilly() { const tabEls = this.tabEls; const { tabPositions } = this.getTabPositions(); + let initialScrollLeft = 0; if (this.isDragging && this.draggabillyDragging) { this.isDragging = false; @@ -542,11 +680,20 @@ export default class TabRowWidget extends BasicWidget { this.draggabillyDragging = draggabilly; tabEl.classList.add("note-tab-is-dragging"); this.$widget.addClass("tab-row-widget-is-sorting"); + + initialScrollLeft = this.$tabScrollingContainer?.scrollLeft() ?? 0; + draggabilly.positionDrag = () => { }; }); draggabilly.on("dragEnd", () => { this.isDragging = false; - const finalTranslateX = parseFloat(tabEl.style.left); + const currentScrollLeft = this.$tabScrollingContainer?.scrollLeft() ?? 0; + const scrollDelta = currentScrollLeft - initialScrollLeft; + const translateX = parseFloat(tabEl.style.left) + scrollDelta; + const maxTranslateX = this.$tabContainer[0]?.offsetWidth - tabEl.offsetWidth; + const minTranslateX = 0; + const finalTranslateX = Math.min(maxTranslateX, Math.max(minTranslateX, translateX)); + tabEl.style.transform = `translate3d(0, 0, 0)`; // Animate dragged tab back into its place @@ -570,12 +717,31 @@ export default class TabRowWidget extends BasicWidget { }); }); - draggabilly.on("dragMove", (event: unknown, pointer: unknown, moveVector: MoveVector) => { + draggabilly.on("dragMove", (event: unknown, pointer: PointerEvent, moveVector: MoveVector) => { // The current index be computed within the event since it can change during the dragMove const tabEls = this.tabEls; const currentIndex = tabEls.indexOf(tabEl); - const currentTabPositionX = originalTabPositionX + moveVector.x; + const scorllContainerBounds = this.$tabScrollingContainer[0]?.getBoundingClientRect(); + const pointerX = pointer.pageX; + const scrollSpeed = 100; // The increment of each scroll. + // Check if the mouse is near the edge of the container and trigger scrolling. + if (pointerX < scorllContainerBounds.left) { + this.scrollTabContainer(- scrollSpeed); + } else if (pointerX > scorllContainerBounds.right) { + this.scrollTabContainer(scrollSpeed); + } + + const currentScrollLeft = this.$tabScrollingContainer?.scrollLeft() ?? 0; + const scrollDelta = currentScrollLeft - initialScrollLeft; + let translateX = moveVector.x + scrollDelta; + + // Limit the `translateX` so that `tabEl` cannot exceed the left and right boundaries of the container. + const maxTranslateX = this.$tabContainer[0]?.offsetWidth - tabEl.offsetWidth - originalTabPositionX; + const minTranslateX = - originalTabPositionX; + translateX = Math.min(maxTranslateX, Math.max(minTranslateX, translateX)); + tabEl.style.transform = `translate3d(${translateX}px, 0, 0)`; + const currentTabPositionX = originalTabPositionX + translateX; const destinationIndexTarget = this.closest(currentTabPositionX, tabPositions); const destinationIndex = Math.max(0, Math.min(tabEls.length, destinationIndexTarget)); @@ -594,8 +760,7 @@ export default class TabRowWidget extends BasicWidget { if (destinationIndex < originIndex) { tabEl.parentNode?.insertBefore(tabEl, this.tabEls[destinationIndex]); } else { - const beforeEl = this.tabEls[destinationIndex + 1] || this.$newTab[0]; - + const beforeEl = this.tabEls[destinationIndex + 1] || this.$containerAnchor[0]; tabEl.parentNode?.insertBefore(tabEl, beforeEl); } this.triggerEvent("tabReorder", { ntxIdsInOrder: this.getNtxIdsInOrder() }); @@ -604,14 +769,19 @@ export default class TabRowWidget extends BasicWidget { setupNewButton() { this.$newTab = $(NEW_TAB_BUTTON_TPL); - - this.$tabContainer.append(this.$newTab); + this.$widget.append(this.$newTab); } setupFiller() { this.$filler = $(FILLER_TPL); - this.$tabContainer.append(this.$filler); + this.$widget.append(this.$filler); + } + + setupContainerAnchor() { + this.$containerAnchor = $(CONTAINER_ANCHOR_TPL); + + this.$tabContainer.append(this.$containerAnchor); } closest(value: number, array: number[]) { @@ -660,7 +830,9 @@ export default class TabRowWidget extends BasicWidget { updateTabById(ntxId: string | null) { const $tab = this.getTabById(ntxId); - + $tab[0].scrollIntoView({ + behavior: 'smooth' + }); const noteContext = appContext.tabManager.getNoteContextById(ntxId); this.updateTab($tab, noteContext); diff --git a/src/public/stylesheets/theme-next/shell.css b/src/public/stylesheets/theme-next/shell.css index da8f3bb1d..81ecff173 100644 --- a/src/public/stylesheets/theme-next/shell.css +++ b/src/public/stylesheets/theme-next/shell.css @@ -871,7 +871,6 @@ body.mobile .fancytree-node > span { /* #region Apply a border to the tab bar that avoids the current tab but also allows a transparent active tab. */ body.layout-horizontal .tab-row-widget, -body.layout-horizontal .tab-row-widget-container, body.layout-horizontal .tab-row-container .note-tab[active] { overflow: visible !important; } @@ -905,10 +904,13 @@ body.layout-vertical.electron.platform-darwin .tab-row-container { } .tab-row-widget-container { - margin-top: calc((var(--tab-bar-height) - var(--tab-height)) / 2); height: var(--tab-height) !important; } +.tab-row-widget { + padding-top: calc((var(--tab-bar-height) - var(--tab-height)) / 2); +} + body.layout-horizontal .tab-row-container { padding-top: calc((var(--tab-bar-height) - var(--tab-height))); } @@ -923,13 +925,14 @@ body.layout-vertical #left-pane .quick-search { /* Limit the drag area for the previous elements to include just to the element itself and not its descendants also */ body.layout-horizontal .tab-row-container > *, -body.layout-vertical .tab-row-widget > *, +body.layout-vertical .tab-row-widget > *:not(.tab-row-filler), body.layout-vertical #left-pane .quick-search > * { -webkit-app-region: no-drag; } +body.layout-horizontal .tab-row-widget, body.layout-horizontal .tab-row-widget-container { - margin-top: 0; + padding-top: 0; position: relative; overflow: hidden; } From 91231874e3b92eadafe83e0870f4dd14baea99ae Mon Sep 17 00:00:00 2001 From: SiriusXT <1160925501@qq.com> Date: Mon, 14 Apr 2025 18:43:54 +0800 Subject: [PATCH 02/10] Avoid triggering tab switch on long press. --- src/public/app/types-lib.d.ts | 2 +- src/public/app/widgets/tab_row.ts | 18 ++++++++++++------ 2 files changed, 13 insertions(+), 7 deletions(-) diff --git a/src/public/app/types-lib.d.ts b/src/public/app/types-lib.d.ts index 51155abfb..770021751 100644 --- a/src/public/app/types-lib.d.ts +++ b/src/public/app/types-lib.d.ts @@ -13,7 +13,7 @@ declare module "draggabilly" { containment: HTMLElement }); element: HTMLElement; - on(event: "pointerDown" | "dragStart" | "dragEnd" | "dragMove", callback: Callback); + on(event: "pointerDown" | "pointerUp" | "dragStart" | "dragEnd" | "dragMove", callback: Callback); dragEnd(); isDragging: boolean; positionDrag: () => void; diff --git a/src/public/app/widgets/tab_row.ts b/src/public/app/widgets/tab_row.ts index a1c731971..44299aa8e 100644 --- a/src/public/app/widgets/tab_row.ts +++ b/src/public/app/widgets/tab_row.ts @@ -388,13 +388,13 @@ export default class TabRowWidget extends BasicWidget { setupScrollEvents() { this.$scrollButtonLeft[0].addEventListener('click', () => this.scrollTabContainer(-200)); - this.$scrollButtonRight[0].addEventListener('click', () => this.scrollTabContainer(200)); + this.$scrollButtonRight[0].addEventListener('click', () => this.scrollTabContainer(200)); this.$tabScrollingContainer[0].addEventListener('wheel', (event) => { - const targetScrollLeft = event.deltaY*1.5; + const targetScrollLeft = event.deltaY * 1.5; this.scrollTabContainer(targetScrollLeft); }); - + this.$tabScrollingContainer[0].addEventListener('scroll', () => { clearTimeout(this.updateScrollTimeout); this.updateScrollTimeout = setTimeout(() => { @@ -458,7 +458,7 @@ export default class TabRowWidget extends BasicWidget { // this.$newTab may include margin, and using NEW_TAB_WIDTH could cause tabsContainerWidth to be slightly larger, // resulting in misaligned scrollbars/buttons. Therefore, use outerwidth. this.updateOuterWidth(); - let tabsContainerWidth = Math.floor(this.$widget.width() ?? 0); + let tabsContainerWidth = Math.floor(this.$widget.width() ?? 0); tabsContainerWidth -= this.newTabOuterWidth + MIN_FILLER_WIDTH; // Check whether the scroll buttons need to be displayed. if ((TAB_CONTAINER_MIN_WIDTH + MARGIN_WIDTH) * numberOfTabs > tabsContainerWidth) { @@ -671,8 +671,14 @@ export default class TabRowWidget extends BasicWidget { this.draggabillies.push(draggabilly); + let pointerDownTime: number = 0; draggabilly.on("pointerDown", () => { - appContext.tabManager.activateNoteContext(tabEl.getAttribute("data-ntx-id")); + pointerDownTime = Date.now(); + }); + draggabilly.on("pointerUp", () => { + if (Date.now() - pointerDownTime < 200) { + appContext.tabManager.activateNoteContext(tabEl.getAttribute("data-ntx-id")); + } }); draggabilly.on("dragStart", () => { @@ -780,7 +786,7 @@ export default class TabRowWidget extends BasicWidget { setupContainerAnchor() { this.$containerAnchor = $(CONTAINER_ANCHOR_TPL); - + this.$tabContainer.append(this.$containerAnchor); } From 83327b290394baf33ca5c4c92809db8b95941ad8 Mon Sep 17 00:00:00 2001 From: SiriusXT <1160925501@qq.com> Date: Tue, 15 Apr 2025 19:43:28 +0800 Subject: [PATCH 03/10] Fix horizontal line issue in horizontal view and drag animation in vertical view. --- src/public/app/widgets/tab_row.ts | 1 - src/public/stylesheets/theme-next/shell.css | 37 ++++----------------- 2 files changed, 6 insertions(+), 32 deletions(-) diff --git a/src/public/app/widgets/tab_row.ts b/src/public/app/widgets/tab_row.ts index 44299aa8e..3e47acb70 100644 --- a/src/public/app/widgets/tab_row.ts +++ b/src/public/app/widgets/tab_row.ts @@ -47,7 +47,6 @@ const TAB_ROW_TPL = ` position: relative; width: 100%; background: var(--main-background-color); - overflow: hidden; user-select: none; } diff --git a/src/public/stylesheets/theme-next/shell.css b/src/public/stylesheets/theme-next/shell.css index 81ecff173..483d6910d 100644 --- a/src/public/stylesheets/theme-next/shell.css +++ b/src/public/stylesheets/theme-next/shell.css @@ -869,36 +869,11 @@ body.mobile .fancytree-node > span { position: relative; } -/* #region Apply a border to the tab bar that avoids the current tab but also allows a transparent active tab. */ -body.layout-horizontal .tab-row-widget, -body.layout-horizontal .tab-row-container .note-tab[active] { - overflow: visible !important; -} - -body.layout-horizontal .tab-row-container .note-tab[active]:before { - content: ""; - position: absolute; - bottom: 0; - left: -100vw; - top: var(--tab-height); - right: calc(100% - 1px); - height: 1px; +/* Apply a border to the tab bar that avoids the current tab but also allows a transparent active tab. */ +body.layout-horizontal .tab-row-container { border-bottom: 1px solid var(--launcher-pane-horiz-border-color); } -body.layout-horizontal .tab-row-container .note-tab[active]:after { - content: ""; - position: absolute; - bottom: 0; - left: 100%; - top: var(--tab-height); - right: 0; - width: 100vw; - height: 1px; - border-bottom: 1px solid var(--launcher-pane-horiz-border-color); -} -/* #endregion */ - body.layout-vertical.electron.platform-darwin .tab-row-container { border-bottom: 1px solid var(--subtle-border-color); } @@ -907,8 +882,8 @@ body.layout-vertical.electron.platform-darwin .tab-row-container { height: var(--tab-height) !important; } -.tab-row-widget { - padding-top: calc((var(--tab-bar-height) - var(--tab-height)) / 2); +.tab-row-widget > * { + margin-top: calc((var(--tab-bar-height) - var(--tab-height)) / 2); } body.layout-horizontal .tab-row-container { @@ -932,7 +907,7 @@ body.layout-vertical #left-pane .quick-search > * { body.layout-horizontal .tab-row-widget, body.layout-horizontal .tab-row-widget-container { - padding-top: 0; + margin-top: 0; position: relative; overflow: hidden; } @@ -994,7 +969,7 @@ body.layout-horizontal .tab-row-widget .note-tab .note-tab-wrapper { text-overflow: ellipsis; } -body.layout-vertical .tab-row-widget-is-sorting .note-tab[active] .note-tab-wrapper { +body.layout-vertical .tab-row-widget-is-sorting .note-tab.note-tab-is-dragging .note-tab-wrapper { transform: scale(0.85); box-shadow: var(--active-tab-dragging-shadow) !important; } From 897fde73324d8e31b43d6ef8efe3b87cf03b9754 Mon Sep 17 00:00:00 2001 From: SiriusXT <1160925501@qq.com> Date: Tue, 15 Apr 2025 22:09:55 +0800 Subject: [PATCH 04/10] Fix lag when scrolling the tab row. --- src/public/app/widgets/tab_row.ts | 22 ++++++++++++++-------- 1 file changed, 14 insertions(+), 8 deletions(-) diff --git a/src/public/app/widgets/tab_row.ts b/src/public/app/widgets/tab_row.ts index 3e47acb70..e5c93ab99 100644 --- a/src/public/app/widgets/tab_row.ts +++ b/src/public/app/widgets/tab_row.ts @@ -377,23 +377,29 @@ export default class TabRowWidget extends BasicWidget { this.$widget.append(this.$style); } - scrollTabContainer(direction: number) { - const currentScrollLeft = this.$tabScrollingContainer?.scrollLeft() ?? 0; + scrollTabContainer(direction: number, behavior: ScrollBehavior = "smooth") { + const currentScrollLeft = this.$tabScrollingContainer[0]?.scrollLeft; this.$tabScrollingContainer[0].scrollTo({ left: currentScrollLeft + direction, - behavior: "smooth" + behavior }); }; setupScrollEvents() { + let isScrolling = false; + this.$tabScrollingContainer[0].addEventListener('wheel', (event) => { + if (!isScrolling) { + isScrolling = true; + requestAnimationFrame(() => { + this.scrollTabContainer(event.deltaY * 1.5, 'instant'); + isScrolling = false; + }); + } + }); + this.$scrollButtonLeft[0].addEventListener('click', () => this.scrollTabContainer(-200)); this.$scrollButtonRight[0].addEventListener('click', () => this.scrollTabContainer(200)); - this.$tabScrollingContainer[0].addEventListener('wheel', (event) => { - const targetScrollLeft = event.deltaY * 1.5; - this.scrollTabContainer(targetScrollLeft); - }); - this.$tabScrollingContainer[0].addEventListener('scroll', () => { clearTimeout(this.updateScrollTimeout); this.updateScrollTimeout = setTimeout(() => { From 46cbbec53a445dc23653240cf4a17e4fe5efd0c9 Mon Sep 17 00:00:00 2001 From: Elian Doran Date: Tue, 15 Apr 2025 17:49:32 +0300 Subject: [PATCH 05/10] docs(release): update changelog --- docs/Release Notes/!!!meta.json | 4 ++-- .../Release Notes/{v0.92.8-beta.md => v0.93.0.md} | 3 ++- 2 files changed, 4 insertions(+), 3 deletions(-) rename docs/Release Notes/Release Notes/{v0.92.8-beta.md => v0.93.0.md} (96%) diff --git a/docs/Release Notes/!!!meta.json b/docs/Release Notes/!!!meta.json index c9e9b98a2..9a5c041f1 100644 --- a/docs/Release Notes/!!!meta.json +++ b/docs/Release Notes/!!!meta.json @@ -61,7 +61,7 @@ "hD3V4hiu2VW4", "VN3xnce1vLkX" ], - "title": "v0.92.8-beta", + "title": "v0.93.0", "notePosition": 10, "prefix": null, "isExpanded": false, @@ -69,7 +69,7 @@ "mime": "text/html", "attributes": [], "format": "markdown", - "dataFileName": "v0.92.8-beta.md", + "dataFileName": "v0.93.0.md", "attachments": [] }, { diff --git a/docs/Release Notes/Release Notes/v0.92.8-beta.md b/docs/Release Notes/Release Notes/v0.93.0.md similarity index 96% rename from docs/Release Notes/Release Notes/v0.92.8-beta.md rename to docs/Release Notes/Release Notes/v0.93.0.md index 3b99b9519..0d30c0b82 100644 --- a/docs/Release Notes/Release Notes/v0.92.8-beta.md +++ b/docs/Release Notes/Release Notes/v0.93.0.md @@ -1,4 +1,4 @@ -# v0.92.8-beta +# v0.93.0 ## 💡 Key highlights * … @@ -32,6 +32,7 @@ * [Center Search results under quick search bar](https://github.com/TriliumNext/Notes/issues/1679) * Native ARM builds for Windows are now back. * Basic Touch Bar support for macOS. +* [Support Bearer Token](https://github.com/TriliumNext/Notes/issues/1701) ## 🌍 Internationalization From bbc85360685cb383a36902d4911d420b03792bf9 Mon Sep 17 00:00:00 2001 From: Elian Doran Date: Tue, 15 Apr 2025 17:56:18 +0300 Subject: [PATCH 06/10] chore(docs): mention in the documentation about Bearer tokens --- docs/User Guide/!!!meta.json | 46 +++++++++---------- .../Advanced Usage/ETAPI (REST API).md | 14 +++++- .../Advanced Usage/ETAPI (REST API).html | 15 ++++-- .../Using Docker.html | 4 +- .../User Guide/Scripting/Script API.html | 9 ++-- 5 files changed, 52 insertions(+), 36 deletions(-) diff --git a/docs/User Guide/!!!meta.json b/docs/User Guide/!!!meta.json index b548e1846..cd9f21f61 100644 --- a/docs/User Guide/!!!meta.json +++ b/docs/User Guide/!!!meta.json @@ -9636,6 +9636,13 @@ "isInheritable": false, "position": 10 }, + { + "type": "relation", + "name": "internalLink", + "value": "habiZ3HU8Kw8", + "isInheritable": false, + "position": 20 + }, { "type": "relation", "name": "internalLink", @@ -9649,13 +9656,6 @@ "value": "default-note-title", "isInheritable": false, "position": 30 - }, - { - "type": "relation", - "name": "internalLink", - "value": "habiZ3HU8Kw8", - "isInheritable": false, - "position": 20 } ], "format": "markdown", @@ -10014,6 +10014,13 @@ "isInheritable": false, "position": 40 }, + { + "type": "relation", + "name": "internalLink", + "value": "habiZ3HU8Kw8", + "isInheritable": false, + "position": 50 + }, { "type": "relation", "name": "internalLink", @@ -10027,13 +10034,6 @@ "value": "bx bx-list-plus", "isInheritable": false, "position": 10 - }, - { - "type": "relation", - "name": "internalLink", - "value": "habiZ3HU8Kw8", - "isInheritable": false, - "position": 50 } ], "format": "markdown", @@ -11066,32 +11066,32 @@ "mime": "text/markdown", "attributes": [ { - "type": "label", - "name": "shareAlias", - "value": "script-api", + "type": "relation", + "name": "internalLink", + "value": "CdNpE2pqjmI6", "isInheritable": false, "position": 10 }, { "type": "relation", "name": "internalLink", - "value": "CdNpE2pqjmI6", + "value": "Q2z6av6JZVWm", "isInheritable": false, "position": 20 }, { "type": "relation", "name": "internalLink", - "value": "Q2z6av6JZVWm", + "value": "MEtfsqa5VwNi", "isInheritable": false, "position": 30 }, { - "type": "relation", - "name": "internalLink", - "value": "MEtfsqa5VwNi", + "type": "label", + "name": "shareAlias", + "value": "script-api", "isInheritable": false, - "position": 40 + "position": 10 } ], "format": "markdown", diff --git a/docs/User Guide/User Guide/Advanced Usage/ETAPI (REST API).md b/docs/User Guide/User Guide/Advanced Usage/ETAPI (REST API).md index b9c4db4f3..c24200049 100644 --- a/docs/User Guide/User Guide/Advanced Usage/ETAPI (REST API).md +++ b/docs/User Guide/User Guide/Advanced Usage/ETAPI (REST API).md @@ -9,16 +9,26 @@ As an alternative to calling the API directly, there are client libraries to sim * [trilium-py](https://github.com/Nriver/trilium-py), you can use Python to communicate with Trilium. +## Obtaining a token + +All operations with the REST API have to be authenticated using a token. You can get this token either from Options -> ETAPI or programmatically using the `/auth/login` REST call (see the [spec](https://github.com/TriliumNext/Notes/blob/master/src/etapi/etapi.openapi.yaml)). + ## Authentication -All operations have to be authenticated using a token. You can get this token either from Options -> ETAPI or programmatically using the `/auth/login` REST call (see the [spec](https://github.com/TriliumNext/Notes/blob/master/src/etapi/etapi.openapi.yaml)): +### Via the `Authorization` header ``` GET https://myserver.com/etapi/app-info Authorization: ETAPITOKEN ``` -Alternatively, since 0.56 you can also use basic auth format: +where `ETAPITOKEN` is the token obtained in the previous step. + +For compatibility with various tools, it's also possible to specify the value of the `Authorization` header in the format `Bearer ETAPITOKEN` (since 0.93.0). + +### Basic authentication + +Since v0.56 you can also use basic auth format: ``` GET https://myserver.com/etapi/app-info diff --git a/src/public/app/doc_notes/en/User Guide/User Guide/Advanced Usage/ETAPI (REST API).html b/src/public/app/doc_notes/en/User Guide/User Guide/Advanced Usage/ETAPI (REST API).html index 0aa779d60..a7ea78e55 100644 --- a/src/public/app/doc_notes/en/User Guide/User Guide/Advanced Usage/ETAPI (REST API).html +++ b/src/public/app/doc_notes/en/User Guide/User Guide/Advanced Usage/ETAPI (REST API).html @@ -8,12 +8,19 @@
  • trilium-py, you can use Python to communicate with Trilium.
  • +

    Obtaining a token

    +

    All operations with the REST API have to be authenticated using a token. + You can get this token either from Options -> ETAPI or programmatically + using the /auth/login REST call (see the spec).

    Authentication

    -

    All operations have to be authenticated using a token. You can get this - token either from Options -> ETAPI or programmatically using the /auth/login REST - call (see the spec):

    GET https://myserver.com/etapi/app-info
    +

    Via the Authorization header

    GET https://myserver.com/etapi/app-info
     Authorization: ETAPITOKEN
    -

    Alternatively, since 0.56 you can also use basic auth format:

    GET https://myserver.com/etapi/app-info
    +

    where ETAPITOKEN is the token obtained in the previous step.

    +

    For compatibility with various tools, it's also possible to specify the + value of the Authorization header in the format Bearer ETAPITOKEN (since + 0.93.0).

    +

    Basic authentication

    +

    Since v0.56 you can also use basic auth format:

    GET https://myserver.com/etapi/app-info
     Authorization: Basic BATOKEN
    • Where BATOKEN = BASE64(username + ':' + password) - this is diff --git a/src/public/app/doc_notes/en/User Guide/User Guide/Installation & Setup/Server Installation/1. Installing the server/Using Docker.html b/src/public/app/doc_notes/en/User Guide/User Guide/Installation & Setup/Server Installation/1. Installing the server/Using Docker.html index 72b77aef3..26e837d8b 100644 --- a/src/public/app/doc_notes/en/User Guide/User Guide/Installation & Setup/Server Installation/1. Installing the server/Using Docker.html +++ b/src/public/app/doc_notes/en/User Guide/User Guide/Installation & Setup/Server Installation/1. Installing the server/Using Docker.html @@ -36,8 +36,8 @@

      Running the Docker Container

      Local Access Only

      Run the container to make it accessible only from the localhost. This - setup is suitable for testing or when using a prox ay server like Nginx - or Apache.

      sudo docker run -t -i -p 127.0.0.1:8080:8080 -v ~/trilium-data:/home/node/trilium-data triliumnext/notes:[VERSION]
      + setup is suitable for testing or when using a proxy server like Nginx or + Apache.

      sudo docker run -t -i -p 127.0.0.1:8080:8080 -v ~/trilium-data:/home/node/trilium-data triliumnext/notes:[VERSION]
      1. Verify the container is running using docker ps.
      2. Access Trilium via a web browser at 127.0.0.1:8080.
      3. diff --git a/src/public/app/doc_notes/en/User Guide/User Guide/Scripting/Script API.html b/src/public/app/doc_notes/en/User Guide/User Guide/Scripting/Script API.html index a2ea49ead..25b181b48 100644 --- a/src/public/app/doc_notes/en/User Guide/User Guide/Scripting/Script API.html +++ b/src/public/app/doc_notes/en/User Guide/User Guide/Scripting/Script API.html @@ -1,11 +1,10 @@ -

        For script code notes, - Trilium offers an API that gives them access to various features of the - application.

        +

        For script code notes, Trilium offers + an API that gives them access to various features of the application.

        There are two APIs:

        In both cases, the API resides in a global variable, api, From d1c2672f99dd2137e1c49cb692349b367c10453d Mon Sep 17 00:00:00 2001 From: SiriusXT <1160925501@qq.com> Date: Tue, 15 Apr 2025 23:38:08 +0800 Subject: [PATCH 07/10] Fix trackpad not switching tabs. --- src/public/app/types-lib.d.ts | 2 +- src/public/app/widgets/tab_row.ts | 10 ++-------- 2 files changed, 3 insertions(+), 9 deletions(-) diff --git a/src/public/app/types-lib.d.ts b/src/public/app/types-lib.d.ts index 770021751..57b810f76 100644 --- a/src/public/app/types-lib.d.ts +++ b/src/public/app/types-lib.d.ts @@ -13,7 +13,7 @@ declare module "draggabilly" { containment: HTMLElement }); element: HTMLElement; - on(event: "pointerDown" | "pointerUp" | "dragStart" | "dragEnd" | "dragMove", callback: Callback); + on(event: "staticClick" | "dragStart" | "dragEnd" | "dragMove", callback: Callback); dragEnd(); isDragging: boolean; positionDrag: () => void; diff --git a/src/public/app/widgets/tab_row.ts b/src/public/app/widgets/tab_row.ts index e5c93ab99..c5fb4e582 100644 --- a/src/public/app/widgets/tab_row.ts +++ b/src/public/app/widgets/tab_row.ts @@ -676,14 +676,8 @@ export default class TabRowWidget extends BasicWidget { this.draggabillies.push(draggabilly); - let pointerDownTime: number = 0; - draggabilly.on("pointerDown", () => { - pointerDownTime = Date.now(); - }); - draggabilly.on("pointerUp", () => { - if (Date.now() - pointerDownTime < 200) { - appContext.tabManager.activateNoteContext(tabEl.getAttribute("data-ntx-id")); - } + draggabilly.on("staticClick", () => { + appContext.tabManager.activateNoteContext(tabEl.getAttribute("data-ntx-id")); }); draggabilly.on("dragStart", () => { From 2b4d9f85368a6cc7452b37c87c180040e85dbff5 Mon Sep 17 00:00:00 2001 From: Elian Doran Date: Tue, 15 Apr 2025 19:54:48 +0300 Subject: [PATCH 08/10] fix(sql): prepared statements leak raw state (fixes #1705) --- src/services/sql.ts | 26 +++++++++++++++++++------- 1 file changed, 19 insertions(+), 7 deletions(-) diff --git a/src/services/sql.ts b/src/services/sql.ts index d135b54da..f686b0876 100644 --- a/src/services/sql.ts +++ b/src/services/sql.ts @@ -112,12 +112,21 @@ function upsert(tableName: string, primaryKey: string, rec: T) { execute(query, rec); } -function stmt(sql: string) { - if (!(sql in statementCache)) { - statementCache[sql] = dbConnection.prepare(sql); +/** + * For the given SQL query, returns a prepared statement. For the same query (string comparison), the same statement is returned. + * + * @param sql the SQL query for which to return a prepared statement. + * @param isRaw indicates whether `.raw()` is going to be called on the prepared statement in order to return the raw rows (e.g. via {@link getRawRows()}). The reason is that the raw state is preserved in the saved statement and would break non-raw calls for the same query. + * @returns the corresponding {@link Statement}. + */ +function stmt(sql: string, isRaw?: boolean) { + const key = (isRaw ? "raw/" + sql : sql); + + if (!(key in statementCache)) { + statementCache[key] = dbConnection.prepare(sql); } - return statementCache[sql]; + return statementCache[key]; } function getRow(query: string, params: Params = []): T { @@ -172,7 +181,7 @@ function getRows(query: string, params: Params = []): T[] { } function getRawRows(query: string, params: Params = []): T[] { - return (wrap(query, (s) => s.raw().all(params)) as T[]) || []; + return (wrap(query, (s) => s.raw().all(params), true) as T[]) || []; } function iterateRows(query: string, params: Params = []): IterableIterator { @@ -234,7 +243,10 @@ function executeScript(query: string): DatabaseType { return dbConnection.exec(query); } -function wrap(query: string, func: (statement: Statement) => unknown): unknown { +/** + * @param isRaw indicates whether `.raw()` is going to be called on the prepared statement in order to return the raw rows (e.g. via {@link getRawRows()}). The reason is that the raw state is preserved in the saved statement and would break non-raw calls for the same query. + */ +function wrap(query: string, func: (statement: Statement) => unknown, isRaw?: boolean): unknown { const startTimestamp = Date.now(); let result; @@ -243,7 +255,7 @@ function wrap(query: string, func: (statement: Statement) => unknown): unknown { } try { - result = func(stmt(query)); + result = func(stmt(query, isRaw)); } catch (e: any) { if (e.message.includes("The database connection is not open")) { // this often happens on killing the app which puts these alerts in front of user From b2b52956ad40874fc7de7de52d247c4256ca351a Mon Sep 17 00:00:00 2001 From: Elian Doran Date: Tue, 15 Apr 2025 19:57:26 +0300 Subject: [PATCH 09/10] chore(release): mention bugfix --- docs/Release Notes/Release Notes/v0.93.0.md | 1 + 1 file changed, 1 insertion(+) diff --git a/docs/Release Notes/Release Notes/v0.93.0.md b/docs/Release Notes/Release Notes/v0.93.0.md index 0d30c0b82..56726e070 100644 --- a/docs/Release Notes/Release Notes/v0.93.0.md +++ b/docs/Release Notes/Release Notes/v0.93.0.md @@ -14,6 +14,7 @@ * [Note background is gray in 0.92.7 (light theme)](https://github.com/TriliumNext/Notes/issues/1689) * [config.Session.cookieMaxAge is ignored](https://github.com/TriliumNext/Notes/issues/1709) by @pano9000 * [Return correct HTTP status code on failed login attempts instead of 200](https://github.com/TriliumNext/Notes/issues/1707) by @pano9000 +* [Calendar stops displaying notes after adding a Day Note](https://github.com/TriliumNext/Notes/issues/1705) ## ✨ Improvements From 80f895a2d536280ecf2c55b5307a1d49a1563486 Mon Sep 17 00:00:00 2001 From: Elian Doran Date: Tue, 15 Apr 2025 21:26:04 +0300 Subject: [PATCH 10/10] chore(release): mention feature in changelog --- docs/Release Notes/Release Notes/v0.93.0.md | 1 + 1 file changed, 1 insertion(+) diff --git a/docs/Release Notes/Release Notes/v0.93.0.md b/docs/Release Notes/Release Notes/v0.93.0.md index 56726e070..fd13d57b4 100644 --- a/docs/Release Notes/Release Notes/v0.93.0.md +++ b/docs/Release Notes/Release Notes/v0.93.0.md @@ -34,6 +34,7 @@ * Native ARM builds for Windows are now back. * Basic Touch Bar support for macOS. * [Support Bearer Token](https://github.com/TriliumNext/Notes/issues/1701) +* The tab bar is now scrollable when there are many tabs by @SiriusXT ## 🌍 Internationalization