Compare commits

..

39 Commits

Author SHA1 Message Date
perfectra1n
6c656c73a3 feat(dev): merge main into feature branch 2025-12-27 22:39:07 -08:00
perfectra1n
09df73e125 fix(fts): remove migration statements of old 0236 2025-11-28 21:36:12 -08:00
perfectra1n
f21aa321f6 fix(fts): remove index of components 2025-11-28 21:29:48 -08:00
perfectra1n
7be8b6c71e fix(fts): merge the two migrations into one file 2025-11-28 21:27:01 -08:00
perfectra1n
bb8e5ebd4a fix(fts): fix suggestions from elian 2025-11-28 21:25:24 -08:00
perfectra1n
6b8b71f7d1 feat(fts): implement missing unit tests 2025-11-28 21:12:39 -08:00
perfectra1n
191a18d7f6 feat(fts): add fts to in-memory sqlite for testing 2025-11-28 21:08:49 -08:00
perfectra1n
574a3441ee feat(fts): update imports from breaking up large fts_search file 2025-11-28 20:59:50 -08:00
perfectra1n
9940ee3bee feat(fts): break up the huge fts_search into smaller files 2025-11-28 20:57:18 -08:00
perfectra1n
41f6fedc61 feat(fts5): get rid of search comparison code 2025-11-24 14:24:07 -08:00
perfectra1n
0ddf48c460 feat(fts5): add more unit tests for search 2025-11-24 13:30:40 -08:00
perfectra1n
3957d789da feat(fts5): also create an fts5 index for attributes, and allow them to be searchable using fts5 indexes 2025-11-18 13:08:11 -08:00
perfectra1n
15719a1ee9 fix(fts5): correctly allow for exact word searches with fts5 2025-11-18 13:07:51 -08:00
perfectra1n
334c7dd27a Merge branch 'main' into feat/rice-searching-with-sqlite
Resolved conflicts by:
- Combining imports from both branches in search.ts (added
hoistedNoteService and beccaService alongside existing ftsSearchService
and log imports)
- Integrating FTS5 search optimization from feature branch with flatText
attribute search logic from main in note_content_fulltext.ts
- Maintained backward compatibility by keeping fallback search
implementation for cases where FTS5 is not available or not suitable
2025-11-16 14:23:01 -08:00
perfectra1n
30da95d75a feat(search): update fulltext search and add stress test improvements
- Modified note_content_fulltext.ts for enhanced search capabilities
- Updated becca_mocking.ts for better test support
- Improved stress-test-populate.ts script
2025-11-16 14:20:46 -08:00
perfectra1n
09ff9ccc65 feat(dev): add new stress test population script 2025-11-15 16:54:09 -08:00
perf3ct
5f1773609f fix(tests): rename some of the silly-ily named tests 2025-11-04 15:56:49 -08:00
perf3ct
da0302066d fix(tests): resolve issues with new search tests not passing 2025-11-04 15:55:42 -08:00
perf3ct
942647ab9c fix(search): get rid of exporting dbConnection 2025-11-04 14:47:46 -08:00
perf3ct
b8aa7402d8 feat(tests): create a ton of tests for the various search capabilities that we support 2025-11-04 14:34:50 -08:00
perf3ct
052e28ab1b feat(search): if the search is empty, return all notes 2025-11-04 11:59:41 -08:00
perf3ct
16912e606e fix(search): resolve compilation issue due to performance log in new search 2025-11-03 12:04:00 -08:00
Jon Fuller
321752ac18 Merge branch 'main' into feat/rice-searching-with-sqlite 2025-11-03 11:47:44 -08:00
perf3ct
10988095c2 feat(search): get the correct comparison and rice out the fts5 search 2025-10-27 14:37:44 -07:00
perf3ct
253da139de feat(search): try again to get fts5 searching done well 2025-10-24 21:47:06 -07:00
Jon Fuller
d992a5e4a2 Merge branch 'main' into feat/rice-searching-with-sqlite 2025-10-24 09:18:11 -07:00
perf3ct
58c225237c feat(search): try a ground-up sqlite search approach 2025-09-03 00:34:55 +00:00
perf3ct
d074841885 Revert "feat(search): try to get fts search to work in large environments"
This reverts commit 053f722cb8.
2025-09-02 19:24:50 +00:00
perf3ct
06b2d71b27 Revert "feat(search): try to decrease complexity"
This reverts commit 5b79e0d71e.
2025-09-02 19:24:47 +00:00
perf3ct
0afb8a11c8 Revert "feat(search): try to deal with huge dbs, might need to squash later"
This reverts commit 37d0136c50.
2025-09-02 19:24:46 +00:00
perf3ct
f529ddc601 Revert "feat(search): further improve fts search"
This reverts commit 7c5553bd4b.
2025-09-02 19:24:45 +00:00
perf3ct
8572f82e0a Revert "feat(search): I honestly have no idea what I'm doing"
This reverts commit b09a2c386d.
2025-09-02 19:24:44 +00:00
perf3ct
b09a2c386d feat(search): I honestly have no idea what I'm doing 2025-09-01 22:29:59 -07:00
perf3ct
7c5553bd4b feat(search): further improve fts search 2025-09-01 21:40:05 -07:00
perf3ct
37d0136c50 feat(search): try to deal with huge dbs, might need to squash later 2025-09-01 04:33:10 +00:00
perf3ct
5b79e0d71e feat(search): try to decrease complexity 2025-08-30 22:30:01 -07:00
perf3ct
053f722cb8 feat(search): try to get fts search to work in large environments 2025-08-31 03:15:29 +00:00
perf3ct
21aaec2c38 feat(search): also fix tests for new fts functionality 2025-08-30 20:48:42 +00:00
perf3ct
1db4971da6 feat(search): implement FST5 w/ sqlite for faster and better searching
feat(search): don't limit the number of blobs to put in virtual tables

fix(search): improve FTS triggers to handle all SQL operations correctly

The root cause of FTS index issues during import was that database triggers
weren't properly handling all SQL operations, particularly upsert operations
(INSERT ... ON CONFLICT ... DO UPDATE) that are commonly used during imports.

Key improvements:
- Fixed INSERT trigger to handle INSERT OR REPLACE operations
- Updated UPDATE trigger to fire on ANY change (not just specific columns)
- Improved blob triggers to use INSERT OR REPLACE for atomic updates
- Added proper handling for notes created before their blobs (import scenario)
- Added triggers for protection state changes
- All triggers now use LEFT JOIN to handle missing blobs gracefully

This ensures the FTS index stays synchronized even when:
- Entity events are disabled during import
- Notes are re-imported (upsert operations)
- Blobs are deduplicated across notes
- Notes are created before their content blobs

The solution works entirely at the database level through triggers,
removing the need for application-level workarounds.

fix(search): consolidate FTS trigger fixes into migration 234

- Merged improved trigger logic from migration 235 into 234
- Deleted unnecessary migration 235 since DB version is still 234
- Ensures triggers handle all SQL operations (INSERT OR REPLACE, upserts)
- Fixes FTS indexing for imported notes by handling missing blobs
- Schema.sql and migration 234 now have identical trigger implementations
2025-08-30 20:39:40 +00:00
389 changed files with 25622 additions and 138625 deletions

View File

@@ -61,7 +61,6 @@
"panzoom": "9.4.3",
"preact": "10.28.1",
"react-i18next": "16.5.0",
"react-window": "2.2.3",
"reveal.js": "5.2.1",
"svg-pan-zoom": "3.6.2",
"tabulator-tables": "6.3.1",

View File

@@ -1,18 +1,17 @@
import "autocomplete.js/index_jquery.js";
import type ElectronRemote from "@electron/remote";
import type Electron from "electron";
import appContext from "./components/app_context.js";
import electronContextMenu from "./menus/electron_context_menu.js";
import utils from "./services/utils.js";
import noteTooltipService from "./services/note_tooltip.js";
import bundleService from "./services/bundle.js";
import toastService from "./services/toast.js";
import noteAutocompleteService from "./services/note_autocomplete.js";
import electronContextMenu from "./menus/electron_context_menu.js";
import glob from "./services/glob.js";
import { t } from "./services/i18n.js";
import noteAutocompleteService from "./services/note_autocomplete.js";
import noteTooltipService from "./services/note_tooltip.js";
import options from "./services/options.js";
import toastService from "./services/toast.js";
import utils from "./services/utils.js";
import type ElectronRemote from "@electron/remote";
import type Electron from "electron";
import "boxicons/css/boxicons.min.css";
import "autocomplete.js/index_jquery.js";
await appContext.earlyInit();

View File

@@ -582,10 +582,6 @@ export default class FNote {
}
getIcon() {
return `tn-icon ${this.#getIconInternal()}`;
}
#getIconInternal() {
const iconClassLabels = this.getLabels("iconClass");
const workspaceIconClass = this.getWorkspaceIconClass();

Binary file not shown.

View File

@@ -1,8 +1,8 @@
import "autocomplete.js/index_jquery.js";
import appContext from "./components/app_context.js";
import glob from "./services/glob.js";
import noteAutocompleteService from "./services/note_autocomplete.js";
import glob from "./services/glob.js";
import "boxicons/css/boxicons.min.css";
import "autocomplete.js/index_jquery.js";
glob.setupGlobs();

View File

@@ -85,15 +85,13 @@ async function remove<T>(url: string, componentId?: string) {
return await call<T>("DELETE", url, componentId);
}
async function upload(url: string, fileToUpload: File, componentId?: string) {
async function upload(url: string, fileToUpload: File) {
const formData = new FormData();
formData.append("upload", fileToUpload);
return await $.ajax({
url: window.glob.baseApiUrl + url,
headers: await getHeaders(componentId ? {
"trilium-component-id": componentId
} : undefined),
headers: await getHeaders(),
data: formData,
type: "PUT",
timeout: 60 * 60 * 1000,

View File

@@ -1,498 +0,0 @@
.bx-ul
{
margin-left: 2em;
padding-left: 0;
list-style: none;
}
.bx-ul > li
{
position: relative;
}
.bx-ul .bx
{
font-size: inherit;
line-height: inherit;
position: absolute;
left: -2em;
width: 2em;
text-align: center;
}
@-webkit-keyframes spin
{
0%
{
-webkit-transform: rotate(0);
transform: rotate(0);
}
100%
{
-webkit-transform: rotate(359deg);
transform: rotate(359deg);
}
}
@keyframes spin
{
0%
{
-webkit-transform: rotate(0);
transform: rotate(0);
}
100%
{
-webkit-transform: rotate(359deg);
transform: rotate(359deg);
}
}
@-webkit-keyframes burst
{
0%
{
-webkit-transform: scale(1);
transform: scale(1);
opacity: 1;
}
90%
{
-webkit-transform: scale(1.5);
transform: scale(1.5);
opacity: 0;
}
}
@keyframes burst
{
0%
{
-webkit-transform: scale(1);
transform: scale(1);
opacity: 1;
}
90%
{
-webkit-transform: scale(1.5);
transform: scale(1.5);
opacity: 0;
}
}
@-webkit-keyframes flashing
{
0%
{
opacity: 1;
}
45%
{
opacity: 0;
}
90%
{
opacity: 1;
}
}
@keyframes flashing
{
0%
{
opacity: 1;
}
45%
{
opacity: 0;
}
90%
{
opacity: 1;
}
}
@-webkit-keyframes fade-left
{
0%
{
-webkit-transform: translateX(0);
transform: translateX(0);
opacity: 1;
}
75%
{
-webkit-transform: translateX(-20px);
transform: translateX(-20px);
opacity: 0;
}
}
@keyframes fade-left
{
0%
{
-webkit-transform: translateX(0);
transform: translateX(0);
opacity: 1;
}
75%
{
-webkit-transform: translateX(-20px);
transform: translateX(-20px);
opacity: 0;
}
}
@-webkit-keyframes fade-right
{
0%
{
-webkit-transform: translateX(0);
transform: translateX(0);
opacity: 1;
}
75%
{
-webkit-transform: translateX(20px);
transform: translateX(20px);
opacity: 0;
}
}
@keyframes fade-right
{
0%
{
-webkit-transform: translateX(0);
transform: translateX(0);
opacity: 1;
}
75%
{
-webkit-transform: translateX(20px);
transform: translateX(20px);
opacity: 0;
}
}
@-webkit-keyframes fade-up
{
0%
{
-webkit-transform: translateY(0);
transform: translateY(0);
opacity: 1;
}
75%
{
-webkit-transform: translateY(-20px);
transform: translateY(-20px);
opacity: 0;
}
}
@keyframes fade-up
{
0%
{
-webkit-transform: translateY(0);
transform: translateY(0);
opacity: 1;
}
75%
{
-webkit-transform: translateY(-20px);
transform: translateY(-20px);
opacity: 0;
}
}
@-webkit-keyframes fade-down
{
0%
{
-webkit-transform: translateY(0);
transform: translateY(0);
opacity: 1;
}
75%
{
-webkit-transform: translateY(20px);
transform: translateY(20px);
opacity: 0;
}
}
@keyframes fade-down
{
0%
{
-webkit-transform: translateY(0);
transform: translateY(0);
opacity: 1;
}
75%
{
-webkit-transform: translateY(20px);
transform: translateY(20px);
opacity: 0;
}
}
@-webkit-keyframes tada
{
from
{
-webkit-transform: scale3d(1, 1, 1);
transform: scale3d(1, 1, 1);
}
10%,
20%
{
-webkit-transform: scale3d(.95, .95, .95) rotate3d(0, 0, 1, -10deg);
transform: scale3d(.95, .95, .95) rotate3d(0, 0, 1, -10deg);
}
30%,
50%,
70%,
90%
{
-webkit-transform: scale3d(1, 1, 1) rotate3d(0, 0, 1, 10deg);
transform: scale3d(1, 1, 1) rotate3d(0, 0, 1, 10deg);
}
40%,
60%,
80%
{
-webkit-transform: scale3d(1, 1, 1) rotate3d(0, 0, 1, -10deg);
transform: scale3d(1, 1, 1) rotate3d(0, 0, 1, -10deg);
}
to
{
-webkit-transform: scale3d(1, 1, 1);
transform: scale3d(1, 1, 1);
}
}
@keyframes tada
{
from
{
-webkit-transform: scale3d(1, 1, 1);
transform: scale3d(1, 1, 1);
}
10%,
20%
{
-webkit-transform: scale3d(.95, .95, .95) rotate3d(0, 0, 1, -10deg);
transform: scale3d(.95, .95, .95) rotate3d(0, 0, 1, -10deg);
}
30%,
50%,
70%,
90%
{
-webkit-transform: scale3d(1, 1, 1) rotate3d(0, 0, 1, 10deg);
transform: scale3d(1, 1, 1) rotate3d(0, 0, 1, 10deg);
}
40%,
60%,
80%
{
-webkit-transform: rotate3d(0, 0, 1, -10deg);
transform: rotate3d(0, 0, 1, -10deg);
}
to
{
-webkit-transform: scale3d(1, 1, 1);
transform: scale3d(1, 1, 1);
}
}
.bx-spin
{
-webkit-animation: spin 2s linear infinite;
animation: spin 2s linear infinite;
}
.bx-spin-hover:hover
{
-webkit-animation: spin 2s linear infinite;
animation: spin 2s linear infinite;
}
.bx-tada
{
-webkit-animation: tada 1.5s ease infinite;
animation: tada 1.5s ease infinite;
}
.bx-tada-hover:hover
{
-webkit-animation: tada 1.5s ease infinite;
animation: tada 1.5s ease infinite;
}
.bx-flashing
{
-webkit-animation: flashing 1.5s infinite linear;
animation: flashing 1.5s infinite linear;
}
.bx-flashing-hover:hover
{
-webkit-animation: flashing 1.5s infinite linear;
animation: flashing 1.5s infinite linear;
}
.bx-burst
{
-webkit-animation: burst 1.5s infinite linear;
animation: burst 1.5s infinite linear;
}
.bx-burst-hover:hover
{
-webkit-animation: burst 1.5s infinite linear;
animation: burst 1.5s infinite linear;
}
.bx-fade-up
{
-webkit-animation: fade-up 1.5s infinite linear;
animation: fade-up 1.5s infinite linear;
}
.bx-fade-up-hover:hover
{
-webkit-animation: fade-up 1.5s infinite linear;
animation: fade-up 1.5s infinite linear;
}
.bx-fade-down
{
-webkit-animation: fade-down 1.5s infinite linear;
animation: fade-down 1.5s infinite linear;
}
.bx-fade-down-hover:hover
{
-webkit-animation: fade-down 1.5s infinite linear;
animation: fade-down 1.5s infinite linear;
}
.bx-fade-left
{
-webkit-animation: fade-left 1.5s infinite linear;
animation: fade-left 1.5s infinite linear;
}
.bx-fade-left-hover:hover
{
-webkit-animation: fade-left 1.5s infinite linear;
animation: fade-left 1.5s infinite linear;
}
.bx-fade-right
{
-webkit-animation: fade-right 1.5s infinite linear;
animation: fade-right 1.5s infinite linear;
}
.bx-fade-right-hover:hover
{
-webkit-animation: fade-right 1.5s infinite linear;
animation: fade-right 1.5s infinite linear;
}
.bx-xs
{
font-size: 1rem!important;
}
.bx-sm
{
font-size: 1.55rem!important;
}
.bx-md
{
font-size: 2.25rem!important;
}
.bx-lg
{
font-size: 3.0rem!important;
}
.bx-fw
{
font-size: 1.2857142857em;
line-height: .8em;
width: 1.2857142857em;
height: .8em;
margin-top: -.2em!important;
vertical-align: middle;
}
.bx-pull-left
{
float: left;
margin-right: .3em!important;
}
.bx-pull-right
{
float: right;
margin-left: .3em!important;
}
.bx-rotate-90
{
transform: rotate(90deg);
-ms-filter: 'progid:DXImageTransform.Microsoft.BasicImage(rotation=1)';
}
.bx-rotate-180
{
transform: rotate(180deg);
-ms-filter: 'progid:DXImageTransform.Microsoft.BasicImage(rotation=2)';
}
.bx-rotate-270
{
transform: rotate(270deg);
-ms-filter: 'progid:DXImageTransform.Microsoft.BasicImage(rotation=3)';
}
.bx-flip-horizontal
{
transform: scaleX(-1);
-ms-filter: 'progid:DXImageTransform.Microsoft.BasicImage(rotation=0, mirror=1)';
}
.bx-flip-vertical
{
transform: scaleY(-1);
-ms-filter: 'progid:DXImageTransform.Microsoft.BasicImage(rotation=2, mirror=1)';
}
.bx-border
{
padding: .25em;
border: .07em solid rgba(0,0,0,.1);
border-radius: .25em;
}
.bx-border-circle
{
padding: .25em;
border: .07em solid rgba(0,0,0,.1);
border-radius: 50%;
}
/** Custom icon **/
.bx-empty {
width: 1em;
display: inline-block;
}

View File

@@ -1,5 +1,3 @@
@import "./boxicons-compat.css";
@font-face {
font-family: Montserrat;
src: url(../fonts/Montserrat-Light.ttf);
@@ -1130,6 +1128,11 @@ a.external:not(.no-arrow):after, a[href^="http://"]:not(.no-arrow):after, a[href
border-color: var(--main-border-color) !important;
}
.bx-empty {
width: 1em;
display: inline-block;
}
.modal-header {
padding: 0.5rem 1rem 0.5rem 1rem !important; /* make modal header padding slightly smaller */
}
@@ -1796,7 +1799,7 @@ button.close:hover {
display: none;
}
.reference-link .tn-icon {
.reference-link .bx {
position: relative;
top: 1px;
margin-inline-end: 3px;
@@ -2415,7 +2418,7 @@ footer.webview-footer button {
gap: 5px;
}
.right-pane-tab .tab-title .tn-icon {
.right-pane-tab .tab-title .bx {
font-size: 1.1em;
}
@@ -2543,11 +2546,18 @@ footer.webview-footer button {
inset-inline-end: 10px;
}
.content-floating-buttons button.tn-icon {
.content-floating-buttons button.bx {
font-size: 130%;
padding: 1px 10px 1px 10px;
}
/* Customized icons */
.bx-tn-toc::before {
content: "\ec24";
transform: rotate(180deg);
}
/* CK Editor */
/* Insert text snippet: limit the width of the listed items to avoid overly long names */

View File

@@ -134,7 +134,7 @@ body.backdrop-effects-disabled {
white-space-collapse: discard;
}
.dropdown-menu.tn-dropdown-menu .dropdown-item .tn-icon {
.dropdown-menu.tn-dropdown-menu .bx {
margin-inline-end: 6px;
}
@@ -249,7 +249,7 @@ html body .dropdown-item[disabled] {
}
/* Menu item icon */
.dropdown-item .tn-icon {
.dropdown-item .bx {
translate: 0 var(--menu-item-icon-vert-offset);
color: var(--menu-item-icon-color) !important;
font-size: 1.1em;
@@ -496,7 +496,7 @@ li.dropdown-item a.dropdown-item-button {
border: unset;
}
li.dropdown-item a.dropdown-item-button.tn-icon {
li.dropdown-item a.dropdown-item-button.bx {
color: var(--menu-text-color) !important;
}
@@ -557,13 +557,13 @@ li.dropdown-item a.dropdown-item-button:focus-visible {
padding-top: 0;
}
#toast-container .toast:not(.no-title) .tn-icon {
#toast-container .toast:not(.no-title) .bx {
margin-inline-end: 0.5em;
font-size: 1.1em;
opacity: 0.85;
}
#toast-container .toast.no-title .tn-icon {
#toast-container .toast.no-title .bx {
margin-inline-end: 0;
font-size: 1.3em;
}
@@ -754,7 +754,7 @@ li.dropdown-item a.dropdown-item-button:focus-visible {
margin-bottom: 0;
}
.note-list-wrapper .note-book-card .tn-icon {
.note-list-wrapper .note-book-card .bx {
color: var(--left-pane-icon-color) !important;
}

View File

@@ -423,6 +423,6 @@ div.tn-tool-dialog {
font-size: unset;
}
.note-type-chooser-dialog div.note-type-dropdown .dropdown-item span.tn-icon {
.note-type-chooser-dialog div.note-type-dropdown .dropdown-item span.bx {
margin-inline-end: .25em;
}
}

View File

@@ -62,10 +62,10 @@ button.ck.ck-button:is(.ck-button-action, .ck-button-save, .ck-button-cancel, .c
}
/* Button's icon */
button.btn.btn-primary span.tn-icon,
button.btn.btn-secondary span.tn-icon,
button.btn.btn-sm span.tn-icon,
button.btn.btn-success span.tn-icon {
button.btn.btn-primary span.bx,
button.btn.btn-secondary span.bx,
button.btn.btn-sm span.bx,
button.btn.btn-success span.bx {
color: var(--cmd-button-icon-color);
padding-inline-end: 0.35em;
font-size: 1.2em;

View File

@@ -151,11 +151,6 @@
--options-title-font-size: .75rem;
--options-title-offset: 13px;
}
.note-split.options {
--preferred-max-content-width: var(--options-card-max-width);
}
/* Create a gap at the top of the option pages */
.note-detail-content-widget-content.options>*:first-child {
margin-top: var(--options-first-item-top-margin, 1em);
@@ -190,6 +185,10 @@ body.experimental-feature-new-layout .note-detail-content-widget-content.options
padding: var(--options-card-padding);
}
body.prefers-centered-content .options-section:not(.tn-no-card) {
margin-inline: auto;
}
body.desktop .options-section:not(.tn-no-card) {
min-width: var(--options-card-min-width);
max-width: var(--options-card-max-width);

View File

@@ -497,7 +497,7 @@ div.bookmark-folder-widget .note-link:hover a {
}
/* The item's icon */
div.bookmark-folder-widget .note-link .tn-icon {
div.bookmark-folder-widget .note-link .bx {
color: var(--menu-item-icon-color);
font-size: 1.2em;
}
@@ -1259,12 +1259,6 @@ body.layout-horizontal #rest-pane > .classic-toolbar-widget {
#center-pane .note-split {
padding-top: 2px;
background-color: var(--note-split-background-color, var(--main-background-color));
transition: border-color 250ms ease-in;
border: 1px solid transparent;
&.active {
border-color: var(--link-selection-outline-color);
}
}
body:not(.background-effects) #center-pane .note-split {

View File

@@ -229,11 +229,11 @@ span.fancytree-node.archived {
opacity: 0.6;
}
.fancytree-node:hover .tn-icon.tree-item-button {
.fancytree-node:hover .bx.tree-item-button {
display: inline-block;
}
.tn-icon.tree-item-button {
.bx.tree-item-button {
display: none;
font-size: 120%;
cursor: pointer;
@@ -243,7 +243,7 @@ span.fancytree-node.archived {
border-radius: 5px;
}
.unhoist-button.tn-icon.tree-item-button {
.unhoist-button.bx.tree-item-button {
margin-inline-start: 0; /* unhoist button is on the left and doesn't need more margin */
display: block; /* keep always visible */
}

View File

@@ -223,6 +223,7 @@
"backlink_other": ""
},
"note_icon": {
"category": "الفئة:",
"search": "بحث:",
"change_note_icon": "تغيير ايقونة الملاحظة",
"reset-default": "اعادة تعيين الى الايقونة الافتراضية"

View File

@@ -146,6 +146,7 @@
"relation": "relació"
},
"note_icon": {
"category": "Categoria:",
"search": "Cerca:"
},
"basic_properties": {

View File

@@ -764,15 +764,9 @@
},
"note_icon": {
"change_note_icon": "更改笔记图标",
"category": "类别:",
"search": "搜索:",
"reset-default": "重置为默认图标",
"search_placeholder_other": "在 {{count}} 个图标包中搜索 {{number}} 个图标",
"search_placeholder_filtered": "在 {{name}} 中搜索 {{number}} 个图标",
"filter": "筛选",
"filter-none": "所有图标",
"filter-default": "默认图标",
"icon_tooltip": "{{name}}\n图标包{{iconPack}}",
"no_results": "没有找到图标。"
"reset-default": "重置为默认图标"
},
"basic_properties": {
"note_type": "笔记类型",
@@ -1452,7 +1446,7 @@
"will_be_deleted_in": "此附件将在 {{time}} 后自动删除",
"will_be_deleted_soon": "该附件在不久后将被自动删除",
"deletion_reason": ",因为该附件未链接在笔记的内容中。为防止被删除,请将附件链接重新添加到内容中或将附件转换为笔记。",
"role_and_size": "角色:{{role}},大小:{{size}},文件类型:{{- mimeType}}",
"role_and_size": "角色:{{role}},大小:{{size}}",
"link_copied": "附件链接已复制到剪贴板。",
"unrecognized_role": "无法识别的附件角色 '{{role}}'。"
},

View File

@@ -21,10 +21,7 @@
},
"bundle-error": {
"title": "Benutzerdefiniertes Skript konnte nicht geladen werden",
"message": "Skript konnte nicht ausgeführt werden wegen:\n\n{{message}}"
},
"widget-list-error": {
"title": "Abruf der Liste von Widgets vom Server ist fehlgeschlagen"
"message": "Skript aus der Notiz \"{{title}}\" mit der ID \"{{id}}\", konnte nicht ausgeführt werden wegen:\n\n{{message}}"
}
},
"add_link": {
@@ -749,6 +746,7 @@
},
"note_icon": {
"change_note_icon": "Notiz-Icon ändern",
"category": "Kategorie:",
"search": "Suche:",
"reset-default": "Standard wiederherstellen"
},

View File

@@ -765,16 +765,9 @@
},
"note_icon": {
"change_note_icon": "Change note icon",
"category": "Category:",
"search": "Search:",
"search_placeholder_one": "Search {{number}} icons across {{count}} packs",
"search_placeholder_other": "Search {{number}} icons across {{count}} packs",
"search_placeholder_filtered": "Search {{number}} icons in {{name}}",
"reset-default": "Reset to default icon",
"filter": "Filter",
"filter-none": "All icons",
"filter-default": "Default icons",
"icon_tooltip": "{{name}}\nIcon pack: {{iconPack}}",
"no_results": "No icons found."
"reset-default": "Reset to default icon"
},
"basic_properties": {
"note_type": "Note type",
@@ -1620,7 +1613,7 @@
"will_be_deleted_in": "This attachment will be automatically deleted in {{time}}",
"will_be_deleted_soon": "This attachment will be automatically deleted soon",
"deletion_reason": ", because the attachment is not linked in the note's content. To prevent deletion, add the attachment link back into the content or convert the attachment into note.",
"role_and_size": "Role: {{role}}, size: {{size}}, MIME: {{- mimeType}}",
"role_and_size": "Role: {{role}}, Size: {{size}}",
"link_copied": "Attachment link copied to clipboard.",
"unrecognized_role": "Unrecognized attachment role '{{role}}'."
},

View File

@@ -749,6 +749,7 @@
},
"note_icon": {
"change_note_icon": "Cambiar icono de nota",
"category": "Categoría:",
"search": "Búsqueda:",
"reset-default": "Restablecer a icono por defecto"
},

View File

@@ -756,6 +756,7 @@
},
"note_icon": {
"change_note_icon": "Changer l'icône de note",
"category": "Catégorie :",
"search": "Recherche :",
"reset-default": "Réinitialiser l'icône par défaut"
},

View File

@@ -16,22 +16,13 @@
},
"bundle-error": {
"title": "Non si è riusciti a caricare uno script personalizzato",
"message": "Impossibile eseguire lo script a causa di:\n\n{{message}}"
"message": "Lo script della nota con ID \"{{id}}\", dal titolo \"{{title}}\" non è stato inizializzato a causa di:\n\n{{message}}"
},
"widget-error": {
"title": "Impossibile inizializzare un widget",
"message-custom": "Il widget personalizzato dalla nota con ID “{{id}}”, intitolato “{{title}}”, non è stato possibile inizializzare a causa di:\n\n{{message}}",
"message-unknown": "Un widget sconosciuto non è stato inizializzato a causa di:\n\n{{message}}"
},
"widget-list-error": {
"title": "Impossibile ottenere l'elenco dei widget dal server"
},
"widget-render-error": {
"title": "Impossibile eseguire il rendering di un widget React personalizzato"
},
"widget-missing-parent": "Il widget personalizzato non ha la proprietà obbligatoria '{{property}}' definita.\n\nSe questo script deve essere eseguito senza un elemento dell'interfaccia utente, utilizzare invece '#run=frontendStartup'.",
"open-script-note": "Apri script note",
"scripting-error": "Errore script personalizzato: {{title}}"
}
},
"add_link": {
"add_link": "Aggiungi un collegamento",
@@ -1342,17 +1333,9 @@
},
"note_icon": {
"change_note_icon": "Cambia icona nota",
"category": "Categoria:",
"search": "Ricerca:",
"reset-default": "Ripristina l'icona predefinita",
"search_placeholder_one": "Cerca {{number}} icona in {{count}} pacchetto",
"search_placeholder_many": "Cerca {{number}} icone in {{count}} pacchetti",
"search_placeholder_other": "Cerca {{number}} icone in {{count}} pacchetti",
"search_placeholder_filtered": "Cerca {{number}} icone in {{name}}",
"filter": "Filtro",
"filter-none": "Tutte le icone",
"filter-default": "Icone predefinite",
"icon_tooltip": "{{name}}\nPacchetto icone: {{iconPack}}",
"no_results": "Nessuna icona trovata."
"reset-default": "Ripristina l'icona predefinita"
},
"basic_properties": {
"note_type": "Tipo di nota",
@@ -1810,7 +1793,7 @@
"will_be_deleted_in": "Questo allegato verrà eliminato automaticamente tra {{time}}",
"will_be_deleted_soon": "Questo allegato verrà eliminato automaticamente a breve",
"deletion_reason": ", perché l'allegato non è collegato al contenuto della nota. Per impedirne l'eliminazione, aggiungi nuovamente il collegamento all'allegato nel contenuto o converti l'allegato in nota.",
"role_and_size": "Ruolo: {{role}}, dimensione: {{size}}, MIME: {{- mimeType}}",
"role_and_size": "Ruolo: {{role}}, Dimensione: {{size}}",
"link_copied": "Link all'allegato copiato negli appunti.",
"unrecognized_role": "Ruolo di allegato non riconosciuto '{{role}}'."
},
@@ -1904,13 +1887,7 @@
"note_detail": {
"could_not_find_typewidget": "Impossibile trovare typeWidget per il tipo '{{type}}'",
"printing": "Stampa in corso...",
"printing_pdf": "Esportazione in PDF in corso...",
"print_report_title": "Stampa rapporto",
"print_report_collection_content_one": "{{count}} la note nella raccolta non può essere stampata perché non è supportata o è protetta.",
"print_report_collection_content_many": "{{count}} le note nella raccolta non possono essere stampate perché non sono supportate o sono protette.",
"print_report_collection_content_other": "{{count}} le note nella raccolta non possono essere stampate perché non sono supportate o sono protette.",
"print_report_collection_details_button": "Vedi dettagli",
"print_report_collection_details_ignored_notes": "Note ignorate"
"printing_pdf": "Esportazione in PDF in corso..."
},
"note_title": {
"placeholder": "scrivi qui il titolo della nota...",
@@ -1920,8 +1897,7 @@
"note_type_switcher_others": "Altro tipo di nota",
"note_type_switcher_templates": "Modello",
"note_type_switcher_collection": "Collezione",
"edited_notes": "Note modificate in questo giorno",
"promoted_attributes": "Attributi promossi"
"edited_notes": "Note modificate"
},
"search_result": {
"no_notes_found": "Non sono state trovate note per i parametri di ricerca specificati.",

View File

@@ -152,22 +152,16 @@
},
"note_icon": {
"change_note_icon": "ノートアイコンの変更",
"category": "カテゴリー:",
"search": "検索:",
"reset-default": "アイコンをデフォルトに戻す",
"search_placeholder_other": "{{count}} 個のパックから {{number}} 個のアイコンを検索",
"search_placeholder_filtered": "{{name}} で {{number}} 個のアイコンを検索",
"filter": "フィルター",
"filter-none": "すべてのアイコン",
"filter-default": "デフォルトアイコン",
"icon_tooltip": "{{name}}\nアイコンパック: {{iconPack}}",
"no_results": "アイコンが見つかりません。"
"reset-default": "アイコンをデフォルトに戻す"
},
"basic_properties": {
"note_type": "ノートタイプ",
"editable": "編集可能",
"basic_properties": "基本プロパティ",
"language": "言語",
"configure_code_notes": "コードノートを設定..."
"configure_code_notes": "コードノートを設定しています..."
},
"i18n": {
"title": "ローカライゼーション",
@@ -2136,7 +2130,7 @@
"will_be_deleted_in": "この添付ファイルは {{time}} 後に自動的に削除されます",
"will_be_deleted_soon": "この添付ファイルはすぐに自動的に削除されます",
"deletion_reason": "、添付ファイルがノートのコンテンツにリンクされていないためです。削除されないようにするには、添付ファイルのリンクをコンテンツに再度追加するか、添付ファイルをノートに変換してください。",
"role_and_size": "ロール: {{role}},サイズ: {{size}}, MIME: {{- mimeType}}",
"role_and_size": "ロール: {{role}},サイズ: {{size}}",
"link_copied": "添付ファイルのリンクをクリップボードにコピーしました。",
"unrecognized_role": "添付ファイルのロール「{{role}}」は認識されません。"
},

View File

@@ -1286,6 +1286,7 @@
},
"note_icon": {
"change_note_icon": "Zmień ikonę notatki",
"category": "Kategoria:",
"search": "Szukaj:",
"reset-default": "Przywróć domyślną ikonę"
},

View File

@@ -724,6 +724,7 @@
},
"note_icon": {
"change_note_icon": "Alterar ícone da nota",
"category": "Categoria:",
"search": "Pesquisa:",
"reset-default": "Redefinir para o ícone padrão"
},

View File

@@ -1008,6 +1008,7 @@
},
"note_icon": {
"change_note_icon": "Alterar ícone da nota",
"category": "Categoria:",
"search": "Busca:",
"reset-default": "Redefinir para o ícone padrão"
},

View File

@@ -1483,6 +1483,7 @@
},
"note_icon": {
"change_note_icon": "Schimbă iconița notiței",
"category": "Categorie:",
"reset-default": "Resetează la iconița implicită",
"search": "Căutare:"
},

View File

@@ -1010,18 +1010,10 @@
"backlink_many": "{{count}} обратных ссылок"
},
"note_icon": {
"category": "Категория:",
"search": "Поиск:",
"change_note_icon": "Изменить иконку заметки",
"reset-default": "Сбросить к значку по умолчанию",
"no_results": "Иконки не найдены.",
"icon_tooltip": "{{name}}\nНабор иконок: {{iconPack}}",
"filter-default": "Иконки по-умолчанию",
"filter-none": "Все иконки",
"filter": "Фильтр",
"search_placeholder_filtered": "Поиск {{number}} иконок в {{name}}",
"search_placeholder_one": "Поиск {{number}} иконки среди {{count}} наборов",
"search_placeholder_few": "Поиск {{number}} иконок среди {{count}} наборов",
"search_placeholder_many": "Поиск {{number}} иконок среди {{count}} наборов"
"reset-default": "Сбросить к значку по умолчанию"
},
"basic_properties": {
"editable": "Изменяемое",
@@ -2034,7 +2026,7 @@
"lost-websocket-connection-message": "Проверьте конфигурацию обратного прокси (например, nginx или Apache), чтобы убедиться, что соединения WebSocket должным образом разрешены и не заблокированы."
},
"attachment_detail_2": {
"role_and_size": "Роль: {{role}}, размер: {{size}}, MIME: {{- mimeType}}",
"role_and_size": "Роль: {{role}}, Размер: {{size}}",
"unrecognized_role": "Нераспознанная роль вложения '{{role}}'.",
"link_copied": "Ссылка на вложение скопирована в буфер обмена.",
"will_be_deleted_soon": "Это вложение скоро будет автоматически удалено",

View File

@@ -21,7 +21,7 @@
},
"bundle-error": {
"title": "載入自訂腳本失敗",
"message": "腳本因以下原因無法執行:\n\n{{message}}"
"message": "來自 ID 為 \"{{id}}\"、標題為 \"{{title}}\" 的筆記的腳本因以下原因無法執行:\n\n{{message}}"
},
"widget-list-error": {
"title": "無法從伺服器取得元件清單"
@@ -761,6 +761,7 @@
},
"note_icon": {
"change_note_icon": "更改筆記圖標",
"category": "類別:",
"search": "搜尋:",
"reset-default": "重置為預設圖標"
},

View File

@@ -849,6 +849,7 @@
},
"note_icon": {
"change_note_icon": "Змінити значок нотатки",
"category": "Категорія:",
"search": "Пошук:",
"reset-default": "Скинути значок до стандартного значення"
},

View File

@@ -17,3 +17,5 @@ declare module "*?raw" {
var content: string;
export default content;
}
declare module "boxicons/css/boxicons.min.css" { }

View File

@@ -1,3 +0,0 @@
interface Window {
TRILIUM_VIEW_HISTORY_STORE?: object;
}

View File

@@ -1,5 +1,3 @@
import { IconRegistry } from "@triliumnext/commons";
import appContext, { AppContext } from "./components/app_context";
import type FNote from "./entities/fnote";
import type { PrintReport } from "./print";
@@ -48,7 +46,6 @@ interface CustomGlobals {
linter: typeof lint;
hasNativeTitleBar: boolean;
isRtl: boolean;
iconRegistry: IconRegistry;
}
type RequireMethod = (moduleName: string) => any;

View File

@@ -142,7 +142,7 @@ function ShowTocWidgetButton({ note, noteContext, isDefaultViewMode }: FloatingB
return isEnabled && <FloatingButton
text={t("show_toc_widget_button.show_toc")}
icon="bx bx-spreadsheet bx-rotate-180"
icon="bx bx-tn-toc"
onClick={() => {
if (noteContext?.viewScope && noteContext.noteId) {
noteContext.viewScope.tocTemporarilyHidden = false;

View File

@@ -12,7 +12,7 @@ body.prefers-centered-content .note-list-widget:not(.full-height) {
}
.note-list-widget .note-list {
padding-block: 10px;
padding: 10px;
}
.note-list-widget.full-height,

View File

@@ -11,8 +11,7 @@ import froca from "../../services/froca";
import { subscribeToMessages, unsubscribeToMessage as unsubscribeFromMessage } from "../../services/ws";
import { useNoteContext, useNoteLabel, useNoteLabelBoolean, useNoteProperty, useTriliumEvent } from "../react/hooks";
import { allViewTypes, ViewModeMedia, ViewModeProps, ViewTypeOptions } from "./interface";
import ViewModeStorage, { type ViewModeStorageType } from "./view_mode_storage";
import ViewModeStorage from "./view_mode_storage";
interface NoteListProps {
note: FNote | null | undefined;
notePath: string | null | undefined;
@@ -216,7 +215,7 @@ export function useNoteIds(note: FNote | null | undefined, viewType: ViewTypeOpt
return noteIds;
}
export function useViewModeConfig<T extends object>(note: FNote | null | undefined, viewType: ViewModeStorageType | undefined) {
export function useViewModeConfig<T extends object>(note: FNote | null | undefined, viewType: ViewTypeOptions | undefined) {
const [ viewConfig, setViewConfig ] = useState<{
config: T | undefined;
storeFn: (data: T) => void;

View File

@@ -40,7 +40,7 @@
z-index: -1;
}
.geo-map-container .leaflet-div-icon .tn-icon {
.geo-map-container .leaflet-div-icon .bx {
position: absolute;
top: 3px;
inset-inline-start: 2px;

View File

@@ -74,11 +74,11 @@ describe("Presentation model", () => {
});
it("rewrites links to other slides", () => {
expect(data.slides[1].content.__html).toStrictEqual(`<div class="ck-content"><p>Go to&nbsp;<a class="reference-link" href="#/slide-slide1"><span><span class="tn-icon bx bx-folder"></span>First slide</span></a>.</p></div>`);
expect(data.slides[1].verticalSlides![0].content.__html).toStrictEqual(`<div class="ck-content"><p>Go to&nbsp;<a class="reference-link" href="#/slide-slide2"><span><span class="tn-icon bx bx-note"></span>First-sub</span></a>.</p></div>`);
expect(data.slides[1].content.__html).toStrictEqual(`<div class="ck-content"><p>Go to&nbsp;<a class="reference-link" href="#/slide-slide1"><span><span class="bx bx-folder"></span>First slide</span></a>.</p></div>`);
expect(data.slides[1].verticalSlides![0].content.__html).toStrictEqual(`<div class="ck-content"><p>Go to&nbsp;<a class="reference-link" href="#/slide-slide2"><span><span class="bx bx-note"></span>First-sub</span></a>.</p></div>`);
});
it("rewrites links even if they are not part of the slideshow", () => {
expect(data.slides[0].verticalSlides![0].content.__html).toStrictEqual(`<div class="ck-content"><p>Go to&nbsp;<a class="reference-link" href="#/slide-other"><span><span class="tn-icon bx bx-note"></span>Other note</span></a>.</p></div>`);
expect(data.slides[0].verticalSlides![0].content.__html).toStrictEqual(`<div class="ck-content"><p>Go to&nbsp;<a class="reference-link" href="#/slide-other"><span><span class="bx bx-note"></span>Other note</span></a>.</p></div>`);
});
});

View File

@@ -4,16 +4,14 @@ import { ViewTypeOptions } from "../collections/interface";
const ATTACHMENT_ROLE = "viewConfig";
export type ViewModeStorageType = ViewTypeOptions | "pdfHistory";
export default class ViewModeStorage<T extends object> {
private note: FNote;
private attachmentName: string;
constructor(note: FNote, viewType: ViewModeStorageType) {
constructor(note: FNote, viewType: ViewTypeOptions) {
this.note = note;
this.attachmentName = `${viewType}.json`;
this.attachmentName = viewType + ".json";
}
async store(data: T) {

View File

@@ -2,13 +2,6 @@
overflow: auto;
scroll-behavior: smooth;
position: relative;
> .inline-title,
> .note-detail > .note-detail-editable-text,
> .note-list-widget:not(.full-height) {
padding-inline: 24px;
}
}
.note-split.type-code:not(.mime-text-x-sqlite) {

View File

@@ -1,11 +1,10 @@
import FlexContainer from "./flex_container.js";
import appContext, { type CommandData, type CommandListenerData, type EventData, type EventNames, type NoteSwitchedContext } from "../../components/app_context.js";
import type BasicWidget from "../basic_widget.js";
import Component from "../../components/component.js";
import NoteContext from "../../components/note_context.js";
import splitService from "../../services/resizer.js";
import { isMobile } from "../../services/utils.js";
import type BasicWidget from "../basic_widget.js";
import NoteContextAwareWidget from "../note_context_aware_widget.js";
import FlexContainer from "./flex_container.js";
import NoteContext from "../../components/note_context.js";
interface SplitNoteWidget extends BasicWidget {
hasBeenAlreadyShown?: boolean;
@@ -75,7 +74,7 @@ export default class SplitNoteContainer extends FlexContainer<SplitNoteWidget> {
const subContexts = activeContext.getSubContexts();
let noteContext: NoteContext | undefined;
let noteContext: NoteContext | undefined = undefined;
if (isMobile() && subContexts.length > 1) {
noteContext = subContexts.find(s => s.ntxId !== ntxId);
}
@@ -202,11 +201,6 @@ export default class SplitNoteContainer extends FlexContainer<SplitNoteWidget> {
async refresh() {
this.toggleExt(true);
// Mark the active note context.
for (const child of this.children as NoteContextAwareWidget[]) {
child.$widget.toggleClass("active", !!child.noteContext?.isActive());
}
}
toggleInt(show: boolean) {} // not needed
@@ -245,16 +239,16 @@ export default class SplitNoteContainer extends FlexContainer<SplitNoteWidget> {
widget.hasBeenAlreadyShown = true;
return [widget.handleEvent("noteSwitched", noteSwitchedContext), this.refreshNotShown(noteSwitchedContext)];
} else {
return Promise.resolve();
}
return Promise.resolve();
}
if (name === "activeContextChanged") {
return this.refreshNotShown(data as EventData<"activeContextChanged">);
} else {
return super.handleEventInChildren(name, data);
}
return super.handleEventInChildren(name, data);
}
refreshNotShown(data: NoteSwitchedContext | EventData<"activeContextChanged">) {

View File

@@ -88,7 +88,6 @@ export default function PopupEditor() {
onHidden={() => setShown(false)}
keepInDom // needed for faster loading
noFocus // automatic focus breaks block popup
stackable
>
{!isNewLayout && <ReadOnlyNoteInfoBar />}
<PromotedAttributes />

File diff suppressed because it is too large Load Diff

View File

@@ -35,7 +35,7 @@
align-items: center;
min-width: 0;
.tn-icon {
.bx {
margin-inline: 6px;
}
@@ -55,7 +55,7 @@
.icon-action {
font-size: .9rem !important;
&.breadcrumb-separator {
.bxs-chevron-right {
transform: translateY(8%);
&::before {

View File

@@ -191,7 +191,7 @@ function BreadcrumbSeparator(props: BreadcrumbSeparatorProps) {
<Dropdown
text={<Icon icon="bx bxs-chevron-right" />}
noSelectButtonStyle
buttonClassName="icon-action breadcrumb-separator"
buttonClassName="icon-action"
hideToggleArrow
dropdownContainerClassName="tn-dropdown-menu-scrollable breadcrumb-child-list"
dropdownOptions={{ popperConfig: { strategy: "fixed", placement: "top" } }}

View File

@@ -10,6 +10,7 @@
max-width: var(--max-content-width);
container-type: inline-size;
padding-top: 20px;
padding-inline-start: 24px;
& > .inline-title-row {
--icon-size: 35px;

View File

@@ -4,21 +4,12 @@ body.experimental-feature-new-layout {
}
.title-actions {
--title-actions-padding-start: 12px;
--title-actions-padding-end: 8px;
display: flex;
max-width: var(--max-content-width);
flex-direction: column;
gap: 0.5em;
padding-inline: var(--title-actions-padding-start) var(--title-actions-padding-end);
body.prefers-centered-content .note-split:not(.full-content-width) & {
margin-inline: auto;
}
&:not(:empty) {
padding-block: 0.75em;
padding: 0.75em 15px;
}
.edited-notes {
@@ -49,11 +40,5 @@ body.experimental-feature-new-layout {
padding: 0;
}
}
> .collapsible,
> .note-type-switcher {
padding-inline-start: calc(24px - var(--title-actions-padding-start));
padding-inline-end: calc(24px - var(--title-actions-padding-end));
}
}
}

View File

@@ -9,7 +9,7 @@
background-color: var(--left-pane-background-color);
padding-inline: 0.25em;
font-size: 0.85em;
> .breadcrumb {
flex-grow: 1;
--icon-button-size: 23px;
@@ -104,7 +104,7 @@
/* Note path card */
li {
--border-radius: 6px;
position: relative;
background: var(--card-background-color);
padding: 8px 20px 8px 25px;
@@ -120,7 +120,7 @@
& + li {
margin-top: 2px;
}
/* Current path arrow */
&.path-current::before {
position: absolute;
@@ -180,7 +180,7 @@
&:last-child {
border-radius: 0 0 var(--border-radius) var(--border-radius);
}
/* Card header */
& > span:first-child {
display: block;
@@ -202,7 +202,7 @@
}
/* Note icon */
> .tn-icon {
> .bx {
color: var(--menu-item-icon-color);
}

View File

@@ -32,14 +32,17 @@ div.note-icon-widget {
}
.note-icon-widget .filter-row {
padding: 10px;
padding-top: 10px;
padding-bottom: 10px;
padding-inline-end: 20px;
display: flex;
align-items: center;
gap: 1em;
align-items: baseline;
}
.note-icon-widget .filter-row span {
display: block;
padding-inline-start: 15px;
padding-inline-end: 15px;
font-weight: bold;
}
@@ -72,14 +75,6 @@ div.note-icon-widget {
height: 1em;
}
.note-icon-widget {
.no-results {
padding: 20px;
text-align: center;
color: var(--muted-text-color);
}
}
body.experimental-feature-new-layout {
.note-icon-widget button.note-icon {
--input-focus-outline-color: var(--note-icon-hover-background-color);
@@ -116,4 +111,4 @@ body.experimental-feature-new-layout {
transition: background 200ms ease-out;
}
}
}
}

View File

@@ -1,36 +1,37 @@
import Dropdown from "./react/Dropdown";
import "./note_icon.css";
import { IconRegistry } from "@triliumnext/commons";
import { Dropdown as BootstrapDropdown } from "bootstrap";
import clsx from "clsx";
import { t } from "i18next";
import { CSSProperties, RefObject } from "preact";
import { useEffect, useMemo, useRef, useState } from "preact/hooks";
import { CellComponentProps, Grid } from "react-window";
import { useNoteContext, useNoteLabel } from "./react/hooks";
import { useEffect, useRef, useState } from "preact/hooks";
import server from "../services/server";
import type { Category, Icon } from "./icon_list";
import FormTextBox from "./react/FormTextBox";
import FormSelect from "./react/FormSelect";
import FNote from "../entities/fnote";
import attributes from "../services/attributes";
import server from "../services/server";
import ActionButton from "./react/ActionButton";
import Dropdown from "./react/Dropdown";
import { FormDropdownDivider, FormListItem } from "./react/FormList";
import FormTextBox from "./react/FormTextBox";
import { useNoteContext, useNoteLabel, useStaticTooltip } from "./react/hooks";
import Button from "./react/Button";
interface IconToCountCache {
iconClassToCountMap: Record<string, number>;
}
let iconToCountCache!: Promise<IconToCountCache> | null;
interface IconData {
iconToCount: Record<string, number>;
categories: Category[];
icons: Icon[];
}
type IconWithName = (IconRegistry["sources"][number]["icons"][number] & { iconPack: string });
let fullIconData: {
categories: Category[];
icons: Icon[];
};
let iconToCountCache!: Promise<IconToCountCache> | null;
export default function NoteIcon() {
const { note, viewScope } = useNoteContext();
const [ icon, setIcon ] = useState<string | null | undefined>();
const [ iconClass ] = useNoteLabel(note, "iconClass");
const [ workspaceIconClass ] = useNoteLabel(note, "workspaceIconClass");
const dropdownRef = useRef<BootstrapDropdown>(null);
useEffect(() => {
setIcon(note?.getIcon());
@@ -40,217 +41,128 @@ export default function NoteIcon() {
<Dropdown
className="note-icon-widget"
title={t("note_icon.change_note_icon")}
dropdownRef={dropdownRef}
dropdownContainerStyle={{ width: "620px" }}
dropdownOptions={{ autoClose: "outside" }}
dropdownContainerStyle={{ width: "610px" }}
buttonClassName={`note-icon tn-focusable-button ${icon ?? "bx bx-empty"}`}
hideToggleArrow
disabled={viewScope?.viewMode !== "default"}
>
{ note && <NoteIconList note={note} dropdownRef={dropdownRef} /> }
{ note && <NoteIconList note={note} /> }
</Dropdown>
);
)
}
function NoteIconList({ note, dropdownRef }: {
note: FNote,
dropdownRef: RefObject<BootstrapDropdown>;
}) {
function NoteIconList({ note }: { note: FNote }) {
const searchBoxRef = useRef<HTMLInputElement>(null);
const iconListRef = useRef<HTMLDivElement>(null);
const [ search, setSearch ] = useState<string>();
const [ filterByPrefix, setFilterByPrefix ] = useState<string | null>(null);
useStaticTooltip(iconListRef, {
selector: "span",
customClass: "pre-wrap-text",
animation: false,
title() { return this.getAttribute("title") || ""; },
});
const allIcons = useAllIcons();
const filteredIcons = useFilteredIcons(allIcons, search, filterByPrefix);
return (
<>
<div class="filter-row">
<span>{t("note_icon.search")}</span>
<FormTextBox
inputRef={searchBoxRef}
type="text"
name="icon-search"
placeholder={ filterByPrefix
? t("note_icon.search_placeholder_filtered", {
number: filteredIcons.length ?? 0,
name: glob.iconRegistry.sources.find(s => s.prefix === filterByPrefix)?.name ?? ""
})
: t("note_icon.search_placeholder", { number: filteredIcons.length ?? 0, count: glob.iconRegistry.sources.length })}
currentValue={search} onChange={setSearch}
autoFocus
/>
{getIconLabels(note).length > 0 && (
<div style={{ textAlign: "center" }}>
<ActionButton
icon="bx bx-reset"
text={t("note_icon.reset-default")}
onClick={() => {
if (!note) return;
for (const label of getIconLabels(note)) {
attributes.removeAttributeById(note.noteId, label.attributeId);
}
dropdownRef?.current?.hide();
}}
/>
</div>
)}
{glob.iconRegistry.sources.length > 0 && <Dropdown
buttonClassName="bx bx-filter-alt"
hideToggleArrow
noSelectButtonStyle
noDropdownListStyle
iconAction
title={t("note_icon.filter")}
>
<IconFilterContent filterByPrefix={filterByPrefix} setFilterByPrefix={setFilterByPrefix} />
</Dropdown>}
</div>
<div
class="icon-list"
ref={iconListRef}
onClick={(e) => {
// Make sure we are not clicking on something else than a button.
const clickedTarget = e.target as HTMLElement;
if (!clickedTarget.classList.contains("tn-icon")) return;
const iconClass = Array.from(clickedTarget.classList.values()).filter(c => c !== "tn-icon").join(" ");
if (note) {
const attributeToSet = note.hasOwnedLabel("workspace") ? "workspaceIconClass" : "iconClass";
attributes.setLabel(note.noteId, attributeToSet, iconClass);
}
dropdownRef?.current?.hide();
}}
>
{filteredIcons.length ? (
<Grid
columnCount={12}
columnWidth={48}
rowCount={Math.ceil(filteredIcons.length / 12)}
rowHeight={48}
cellComponent={IconItemCell}
cellProps={{
filteredIcons
}}
/>
) : (
<div class="no-results">{t("note_icon.no_results")}</div>
)}
</div>
</>
);
}
function IconItemCell({ rowIndex, columnIndex, style, filteredIcons }: CellComponentProps<{
filteredIcons: IconWithName[];
}>): React.JSX.Element {
const iconIndex = rowIndex * 12 + columnIndex;
const iconData = filteredIcons[iconIndex] as IconWithName | undefined;
if (!iconData) return <></>;
const { id, terms, iconPack } = iconData;
return (
<span
key={id}
class={clsx(id, "tn-icon")}
title={t("note_icon.icon_tooltip", { name: terms?.[0] ?? id, iconPack })}
style={style as CSSProperties}
/>
);
}
function IconFilterContent({ filterByPrefix, setFilterByPrefix }: {
filterByPrefix: string | null;
setFilterByPrefix: (value: string | null) => void;
}) {
return (
<>
<FormListItem
checked={filterByPrefix === null}
onClick={() => setFilterByPrefix(null)}
>{t("note_icon.filter-none")}</FormListItem>
<FormListItem
checked={filterByPrefix === "bx"}
onClick={() => setFilterByPrefix("bx")}
>{t("note_icon.filter-default")}</FormListItem>
<FormDropdownDivider />
{glob.iconRegistry.sources.map(({ prefix, name, icon }) => (
prefix !== "bx" && <FormListItem
key={prefix}
onClick={() => setFilterByPrefix(prefix)}
icon={icon}
checked={filterByPrefix === prefix}
>{name}</FormListItem>
))}
</>
);
}
function useAllIcons() {
const [ allIcons, setAllIcons ] = useState<IconWithName[]>();
const [ categoryId, setCategoryId ] = useState<string>("0");
const [ iconData, setIconData ] = useState<IconData>();
useEffect(() => {
getIconToCountMap().then((iconsToCount) => {
const allIcons = [
...glob.iconRegistry.sources.flatMap(s => s.icons.map((i) => ({
...i,
iconPack: s.name,
})))
];
async function loadIcons() {
if (!fullIconData) {
fullIconData = (await import("./icon_list.js")).default;
}
// Filter by text and/or category.
let icons: Icon[] = fullIconData.icons;
const processedSearch = search?.trim()?.toLowerCase();
if (processedSearch || categoryId) {
icons = icons.filter((icon) => {
if (categoryId !== "0" && String(icon.category_id) !== categoryId) {
return false;
}
if (processedSearch) {
if (!icon.name.includes(processedSearch) &&
!icon.term?.find((t) => t.includes(processedSearch))) {
return false;
}
}
return true;
});
}
// Sort by count.
if (iconsToCount) {
allIcons.sort((a, b) => {
const countA = iconsToCount[a.id ?? ""] || 0;
const countB = iconsToCount[b.id ?? ""] || 0;
const iconToCount = await getIconToCountMap();
if (iconToCount) {
icons.sort((a, b) => {
const countA = iconToCount[a.className ?? ""] || 0;
const countB = iconToCount[b.className ?? ""] || 0;
return countB - countA;
});
}
setAllIcons(allIcons);
});
}, []);
return allIcons;
}
function useFilteredIcons(allIcons: IconWithName[] | undefined, search: string | undefined, filterByPrefix: string | null) {
// Filter by text and/or icon pack.
const filteredIcons = useMemo(() => {
let icons: IconWithName[] = allIcons ?? [];
const processedSearch = search?.trim()?.toLowerCase();
if (processedSearch || filterByPrefix !== null) {
icons = icons.filter((icon) => {
if (filterByPrefix) {
if (!icon.id?.startsWith(`${filterByPrefix} `)) {
return false;
}
}
if (processedSearch) {
if (!icon.terms?.some((t) => t.includes(processedSearch))) {
return false;
}
}
return true;
});
setIconData({
iconToCount,
icons,
categories: fullIconData.categories
})
}
return icons;
}, [ allIcons, search, filterByPrefix ]);
return filteredIcons;
loadIcons();
}, [ search, categoryId ]);
return (
<>
<div class="filter-row">
<span>{t("note_icon.category")}</span>
<FormSelect
name="icon-category"
values={fullIconData?.categories ?? []}
currentValue={categoryId} onChange={setCategoryId}
keyProperty="id" titleProperty="name"
/>
<span>{t("note_icon.search")}</span>
<FormTextBox
inputRef={searchBoxRef}
type="text"
name="icon-search"
currentValue={search} onChange={setSearch}
autoFocus
/>
</div>
<div
class="icon-list"
onClick={(e) => {
const clickedTarget = e.target as HTMLElement;
if (!clickedTarget.classList.contains("bx")) {
return;
}
const iconClass = Array.from(clickedTarget.classList.values()).join(" ");
if (note) {
const attributeToSet = note.hasOwnedLabel("workspace") ? "workspaceIconClass" : "iconClass";
attributes.setLabel(note.noteId, attributeToSet, iconClass);
}
}}
>
{getIconLabels(note).length > 0 && (
<div style={{ textAlign: "center" }}>
<Button
text={t("note_icon.reset-default")}
onClick={() => {
if (!note) {
return;
}
for (const label of getIconLabels(note)) {
attributes.removeAttributeById(note.noteId, label.attributeId);
}
}}
/>
</div>
)}
{(iconData?.icons ?? []).map(({className, name}) => (
<span class={`bx ${className}`} title={name} />
))}
</div>
</>
);
}
async function getIconToCountMap() {
@@ -268,5 +180,5 @@ function getIconLabels(note: FNote) {
}
return note.getOwnedLabels()
.filter((label) => ["workspaceIconClass", "iconClass"]
.includes(label.name));
.includes(label.name));
}

View File

@@ -92,7 +92,7 @@ body.experimental-feature-new-layout {
height: var(--size);
padding: 0;
.tn-icon {
.bx {
opacity: 1;
margin: 0;
}

View File

@@ -1,39 +1,38 @@
import hoistedNoteService from "../services/hoisted_note.js";
import treeService from "../services/tree.js";
import utils from "../services/utils.js";
import contextMenu from "../menus/context_menu.js";
import froca from "../services/froca.js";
import branchService from "../services/branches.js";
import ws from "../services/ws.js";
import NoteContextAwareWidget from "./note_context_aware_widget.js";
import server from "../services/server.js";
import noteCreateService from "../services/note_create.js";
import toastService from "../services/toast.js";
import appContext, { type CommandListenerData, type EventData } from "../components/app_context.js";
import keyboardActionsService from "../services/keyboard_actions.js";
import clipboard from "../services/clipboard.js";
import protectedSessionService from "../services/protected_session.js";
import linkService from "../services/link.js";
import options from "../services/options.js";
import protectedSessionHolder from "../services/protected_session_holder.js";
import dialogService from "../services/dialog.js";
import shortcutService from "../services/shortcuts.js";
import { t } from "../services/i18n.js";
import type FBranch from "../entities/fbranch.js";
import type LoadResults from "../services/load_results.js";
import type FNote from "../entities/fnote.js";
import type { NoteType } from "../entities/fnote.js";
import type { AttributeRow, BranchRow } from "../services/load_results.js";
import type { SetNoteOpts } from "../components/note_context.js";
import type { TouchBarItem } from "../components/touch_bar.js";
import type { TreeCommandNames } from "../menus/tree_context_menu.js";
import "jquery.fancytree";
import "jquery.fancytree/dist/modules/jquery.fancytree.dnd5.js";
import "jquery.fancytree/dist/modules/jquery.fancytree.clones.js";
import "jquery.fancytree/dist/modules/jquery.fancytree.filter.js";
import "../stylesheets/tree.css";
import appContext, { type CommandListenerData, type EventData } from "../components/app_context.js";
import type { SetNoteOpts } from "../components/note_context.js";
import type { TouchBarItem } from "../components/touch_bar.js";
import type FBranch from "../entities/fbranch.js";
import type FNote from "../entities/fnote.js";
import type { NoteType } from "../entities/fnote.js";
import contextMenu from "../menus/context_menu.js";
import type { TreeCommandNames } from "../menus/tree_context_menu.js";
import branchService from "../services/branches.js";
import clipboard from "../services/clipboard.js";
import dialogService from "../services/dialog.js";
import froca from "../services/froca.js";
import hoistedNoteService from "../services/hoisted_note.js";
import { t } from "../services/i18n.js";
import keyboardActionsService from "../services/keyboard_actions.js";
import linkService from "../services/link.js";
import type LoadResults from "../services/load_results.js";
import type { AttributeRow, BranchRow } from "../services/load_results.js";
import noteCreateService from "../services/note_create.js";
import options from "../services/options.js";
import protectedSessionService from "../services/protected_session.js";
import protectedSessionHolder from "../services/protected_session_holder.js";
import server from "../services/server.js";
import shortcutService from "../services/shortcuts.js";
import toastService from "../services/toast.js";
import treeService from "../services/tree.js";
import utils from "../services/utils.js";
import ws from "../services/ws.js";
import NoteContextAwareWidget from "./note_context_aware_widget.js";
const TPL = /*html*/`
<div class="tree-wrapper">
<style>
@@ -243,7 +242,7 @@ export default class NoteTreeWidget extends NoteContextAwareWidget {
e.preventDefault();
appContext.tabManager.openTabWithNoteWithHoisting(notePath, {
activate: !!e.shiftKey
activate: e.shiftKey ? true : false
});
}
}
@@ -403,7 +402,7 @@ export default class NoteTreeWidget extends NoteContextAwareWidget {
} else if (ctrlKey) {
const notePath = treeService.getNotePath(node);
appContext.tabManager.openTabWithNoteWithHoisting(notePath, {
activate: !!event.shiftKey
activate: event.shiftKey ? true : false
});
} else if (event.altKey) {
node.setSelected(!node.isSelected());
@@ -500,9 +499,9 @@ export default class NoteTreeWidget extends NoteContextAwareWidget {
return ["before", "after"];
} else if (["_lbAvailableLaunchers", "_lbVisibleLaunchers"].includes(node.data.noteId)) {
return ["over"];
} else {
return true;
}
return true;
},
dragDrop: async (node, data) => {
if (
@@ -598,7 +597,7 @@ export default class NoteTreeWidget extends NoteContextAwareWidget {
clones: {
highlightActiveClones: true
},
async enhanceTitle (
enhanceTitle: async function (
event: Event,
data: {
node: Fancytree.FancytreeNode;
@@ -628,7 +627,7 @@ export default class NoteTreeWidget extends NoteContextAwareWidget {
const isHoistedNote = activeNoteContext && activeNoteContext.hoistedNoteId === note.noteId && note.noteId !== "root";
if (note.hasLabel("workspace") && !isHoistedNote) {
const $enterWorkspaceButton = $(`<span class="tree-item-button tn-icon enter-workspace-button bx bx-door-open" title="${t("note_tree.hoist-this-note-workspace")}"></span>`).on(
const $enterWorkspaceButton = $(`<span class="tree-item-button enter-workspace-button bx bx-door-open" title="${t("note_tree.hoist-this-note-workspace")}"></span>`).on(
"click",
cancelClickPropagation
);
@@ -637,7 +636,7 @@ export default class NoteTreeWidget extends NoteContextAwareWidget {
}
if (note.type === "search") {
const $refreshSearchButton = $(`<span class="tree-item-button tn-icon refresh-search-button bx bx-refresh" title="${t("note_tree.refresh-saved-search-results")}"></span>`).on(
const $refreshSearchButton = $(`<span class="tree-item-button refresh-search-button bx bx-refresh" title="${t("note_tree.refresh-saved-search-results")}"></span>`).on(
"click",
cancelClickPropagation
);
@@ -651,7 +650,7 @@ export default class NoteTreeWidget extends NoteContextAwareWidget {
&& !note.isLaunchBarConfig()
&& !note.noteId.startsWith("_help")
) {
const $createChildNoteButton = $(`<span class="tree-item-button tn-icon add-note-button bx bx-plus" title="${t("note_tree.create-child-note")}"></span>`).on(
const $createChildNoteButton = $(`<span class="tree-item-button add-note-button bx bx-plus" title="${t("note_tree.create-child-note")}"></span>`).on(
"click",
cancelClickPropagation
);
@@ -660,7 +659,7 @@ export default class NoteTreeWidget extends NoteContextAwareWidget {
}
if (isHoistedNote) {
const $unhoistButton = $(`<span class="tree-item-button tn-icon unhoist-button bx bx-door-open" title="${t("note_tree.unhoist")}"></span>`).on("click", cancelClickPropagation);
const $unhoistButton = $(`<span class="tree-item-button unhoist-button bx bx-door-open" title="${t("note_tree.unhoist")}"></span>`).on("click", cancelClickPropagation);
$span.append($unhoistButton);
}
@@ -1282,7 +1281,7 @@ export default class NoteTreeWidget extends NoteContextAwareWidget {
// activeNode is supposed to be moved when we find out activeNode is deleted but not all branches are deleted. save it for fixing activeNodePath after all nodes loaded.
let movedActiveNode: Fancytree.FancytreeNode | null = null;
const parentsOfAddedNodes: Fancytree.FancytreeNode[] = [];
let parentsOfAddedNodes: Fancytree.FancytreeNode[] = [];
for (const branchRow of branchRows) {
if (branchRow.noteId) {
@@ -1453,10 +1452,10 @@ export default class NoteTreeWidget extends NoteContextAwareWidget {
if (branchId && branchId.startsWith("virt")) {
// in case of virtual branches there's nothing to update
return;
} else {
logError(`Cannot find branch=${branchId}`);
return;
}
logError(`Cannot find branch=${branchId}`);
return;
}
branch.isExpanded = isExpanded;
@@ -1595,7 +1594,7 @@ export default class NoteTreeWidget extends NoteContextAwareWidget {
// Trigger the event with the selected branch IDs
appContext.triggerEvent("editBranchPrefix", {
selectedOrActiveBranchIds: branchIds,
node
node: node
});
}
@@ -1780,12 +1779,12 @@ export default class NoteTreeWidget extends NoteContextAwareWidget {
#moveLaunchers(selectedOrActiveBranchIds: string[], desktopParent: string, mobileParent: string) {
const desktopLaunchersToMove = selectedOrActiveBranchIds.filter((branchId) => !branchId.startsWith("_lbMobile"));
if (desktopLaunchersToMove) {
branchService.moveToParentNote(desktopLaunchersToMove, `_lbRoot_${ desktopParent}`);
branchService.moveToParentNote(desktopLaunchersToMove, "_lbRoot_" + desktopParent);
}
const mobileLaunchersToMove = selectedOrActiveBranchIds.filter((branchId) => branchId.startsWith("_lbMobile"));
if (mobileLaunchersToMove) {
branchService.moveToParentNote(mobileLaunchersToMove, `_lbMobileRoot_${ mobileParent}`);
branchService.moveToParentNote(mobileLaunchersToMove, "_lbMobileRoot_" + mobileParent);
}
}
@@ -1830,7 +1829,7 @@ export default class NoteTreeWidget extends NoteContextAwareWidget {
selectedOrActiveBranchIds: this.getSelectedOrActiveBranchIds(node),
selectedOrActiveNoteIds: this.getSelectedOrActiveNoteIds(node)
});
};
}
const items: TouchBarItem[] = [
new TouchBar.TouchBarButton({

View File

@@ -22,7 +22,7 @@
}
}
.tn-icon {
.bx {
font-size: 1.2em;
margin-inline-end: 4px;
opacity: .6;

View File

@@ -5,7 +5,7 @@
white-space: normal;
}
span.tn-icon {
span.bx {
flex-shrink: 0;
}

View File

@@ -9,7 +9,7 @@ interface IconProps extends Pick<HTMLAttributes<HTMLSpanElement>, "className" |
export default function Icon({ icon, className, ...restProps }: IconProps) {
return (
<span
class={clsx(icon ?? "bx bx-empty", className, "tn-icon")}
class={clsx(icon ?? "bx bx-empty", className)}
{...restProps}
/>
);

View File

@@ -135,7 +135,7 @@ function RibbonTab({ icon, title, active, onClick, toggleCommand }: { icon: stri
>
<span
ref={iconRef}
className={`ribbon-tab-title-icon tn-icon ${icon}`}
className={`ribbon-tab-title-icon ${icon}`}
/>
&nbsp;
{ active && <span class="ribbon-tab-title-label">{title}</span> }

View File

@@ -38,7 +38,7 @@
padding-top: 2px;
}
.ribbon-tab-title .tn-icon {
.ribbon-tab-title .bx {
font-size: 150%;
position: relative;
}

View File

@@ -28,7 +28,7 @@ body.experimental-feature-new-layout #right-pane {
border-bottom: 0;
}
&.collapsed .card-header > .tn-icon {
&.collapsed .card-header > .bx {
transform: rotate(-90deg);
}
}
@@ -50,7 +50,7 @@ body.experimental-feature-new-layout #right-pane {
padding: 0.75em;
color: var(--muted-text-color);
.tn-icon {
.bx {
font-size: 3em;
}

View File

@@ -131,7 +131,7 @@
width: 20em;
}
.attachment-actions .dropdown-item .tn-icon {
.attachment-actions .dropdown-item .bx {
position: relative;
top: 3px;
font-size: 120%;

View File

@@ -195,11 +195,7 @@ function AttachmentInfo({ attachment, isFullDetail }: { attachment: FAttachment,
) : (attachment.title)}
</h4>
<div className="attachment-details">
{t("attachment_detail_2.role_and_size", {
role: attachment.role,
size: utils.formatSize(attachment.contentLength),
mimeType: attachment.mime
})}
{t("attachment_detail_2.role_and_size", { role: attachment.role, size: utils.formatSize(attachment.contentLength) })}
</div>
<div style="flex: 1 1;" />
</div>

View File

@@ -1,29 +1,27 @@
import { useNoteBlob } from "../react/hooks";
import "./File.css";
import { TypeWidgetProps } from "./type_widget";
import FNote from "../../entities/fnote";
import { t } from "../../services/i18n";
import { getUrlForDownload } from "../../services/open";
import Alert from "../react/Alert";
import { useNoteBlob } from "../react/hooks";
import PdfPreview from "./file/Pdf";
import { TypeWidgetProps } from "./type_widget";
import { t } from "../../services/i18n";
const TEXT_MAX_NUM_CHARS = 5000;
export default function FileTypeWidget({ note, parentComponent, noteContext }: TypeWidgetProps) {
const blob = useNoteBlob(note, parentComponent?.componentId);
export default function File({ note }: TypeWidgetProps) {
const blob = useNoteBlob(note);
if (blob?.content) {
return <TextPreview content={blob.content} />;
return <TextPreview content={blob.content} />
} else if (note.mime === "application/pdf") {
return <PdfPreview blob={blob} note={note} ntxId={noteContext?.ntxId} componentId={parentComponent?.componentId} />;
return <PdfPreview note={note} />
} else if (note.mime.startsWith("video/")) {
return <VideoPreview note={note} />;
return <VideoPreview note={note} />
} else if (note.mime.startsWith("audio/")) {
return <AudioPreview note={note} />;
return <AudioPreview note={note} />
} else {
return <NoPreview />
}
return <NoPreview />;
}
function TextPreview({ content }: { content: string }) {
@@ -39,6 +37,14 @@ function TextPreview({ content }: { content: string }) {
)}
<pre class="file-preview-content">{trimmedContent}</pre>
</>
)
}
function PdfPreview({ note }: { note: FNote }) {
return (
<iframe
class="pdf-preview"
src={getUrlForDownload(`api/notes/${note.noteId}/open`)} />
);
}
@@ -50,7 +56,7 @@ function VideoPreview({ note }: { note: FNote }) {
datatype={note?.mime}
controls
/>
);
)
}
function AudioPreview({ note }: { note: FNote }) {
@@ -60,7 +66,7 @@ function AudioPreview({ note }: { note: FNote }) {
src={getUrlForDownload(`api/notes/${note.noteId}/open-partial`)}
controls
/>
);
)
}
function NoPreview() {

View File

@@ -1,140 +0,0 @@
import { RefObject } from "preact";
import { useCallback, useEffect, useRef } from "preact/hooks";
import appContext from "../../../components/app_context";
import FBlob from "../../../entities/fblob";
import FNote from "../../../entities/fnote";
import server from "../../../services/server";
import { useViewModeConfig } from "../../collections/NoteList";
import { useTriliumOption } from "../../react/hooks";
const VARIABLE_WHITELIST = new Set([
"root-background",
"main-background-color",
"main-border-color",
"main-text-color"
]);
export default function PdfPreview({ note, blob, componentId, ntxId }: {
note: FNote,
blob: FBlob | null | undefined,
componentId: string | undefined;
ntxId: string | null | undefined;
}) {
const iframeRef = useRef<HTMLIFrameElement>(null);
const { onLoad } = useStyleInjection(iframeRef);
const historyConfig = useViewModeConfig(note, "pdfHistory");
const [ locale ] = useTriliumOption("locale");
useEffect(() => {
function handleMessage(event: MessageEvent) {
if (event.data?.type === "pdfjs-viewer-document-modified" && event.data?.data) {
const blob = new Blob([event.data.data], { type: note.mime });
server.upload(`notes/${note.noteId}/file`, new File([blob], note.title, { type: note.mime }), componentId);
}
if (event.data.type === "pdfjs-viewer-save-view-history" && event.data?.data) {
historyConfig?.storeFn(JSON.parse(event.data.data));
}
}
window.addEventListener("message", handleMessage);
return () => {
window.removeEventListener("message", handleMessage);
};
}, [ note, historyConfig, componentId, blob ]);
// Refresh when blob changes.
useEffect(() => {
if (iframeRef.current?.contentWindow) {
iframeRef.current.contentWindow.location.reload();
}
}, [ blob ]);
// Trigger focus when iframe content is clicked (iframe focus doesn't bubble)
useEffect(() => {
const iframe = iframeRef.current;
if (!iframe) return;
const handleIframeClick = () => {
if (ntxId) {
appContext.tabManager.activateNoteContext(ntxId);
}
};
// Listen for clicks on the iframe's content window
const iframeDoc = iframe.contentWindow?.document;
if (iframeDoc) {
iframeDoc.addEventListener('click', handleIframeClick);
return () => iframeDoc.removeEventListener('click', handleIframeClick);
}
}, [ iframeRef.current?.contentWindow, ntxId ]);
return (historyConfig &&
<iframe
tabIndex={300}
ref={iframeRef}
class="pdf-preview"
src={`pdfjs/web/viewer.html?file=../../api/notes/${note.noteId}/open&lang=${locale}`}
onLoad={() => {
const win = iframeRef.current?.contentWindow;
if (win) {
win.TRILIUM_VIEW_HISTORY_STORE = historyConfig.config;
}
onLoad();
}}
/>
);
}
function useStyleInjection(iframeRef: RefObject<HTMLIFrameElement>) {
const styleRef = useRef<HTMLStyleElement | null>(null);
// First load.
const onLoad = useCallback(() => {
const doc = iframeRef.current?.contentDocument;
if (!doc) return;
const style = doc.createElement('style');
style.id = 'client-root-vars';
style.textContent = cssVarsToString(getRootCssVariables());
styleRef.current = style;
doc.head.appendChild(style);
}, [ iframeRef ]);
// React to changes.
useEffect(() => {
const listener = () => {
styleRef.current!.textContent = cssVarsToString(getRootCssVariables());
};
const media = window.matchMedia("(prefers-color-scheme: dark)");
media.addEventListener("change", listener);
return () => media.removeEventListener("change", listener);
}, [ iframeRef ]);
return {
onLoad
};
}
function getRootCssVariables() {
const styles = getComputedStyle(document.documentElement);
const vars: Record<string, string> = {};
for (let i = 0; i < styles.length; i++) {
const prop = styles[i];
if (prop.startsWith('--') && VARIABLE_WHITELIST.has(prop.substring(2))) {
vars[`--tn-${prop.substring(2)}`] = styles.getPropertyValue(prop).trim();
}
}
return vars;
}
function cssVarsToString(vars: Record<string, string>) {
return `:root {\n${Object.entries(vars)
.map(([k, v]) => ` ${k}: ${v};`)
.join('\n')}\n}`;
}

View File

@@ -20,7 +20,7 @@
margin-bottom: 5px;
}
.tn-icon {
.bx {
margin: 4px 0;
font-size: 12px;
opacity: 0.5;
@@ -78,7 +78,7 @@
.ribbon {
padding: 0 5px;
.tn-icon {
.bx {
font-size: 10px;
}

View File

@@ -1,5 +1,6 @@
.note-detail-editable-text {
font-family: var(--detail-font-family);
padding-inline-start: 14px;
height: 100%;
}
@@ -48,8 +49,4 @@ body.heading-style-underline .note-detail-editable-text h6 { border-bottom: 1px
box-shadow: none !important;
min-height: 50px;
height: 100%;
}
.note-detail-editable-text .ck.ck-editor__editable_inline {
padding: 0;
}

View File

@@ -35,5 +35,5 @@ describe("CK config", () => {
expect(config.translations, locale.id).toHaveLength(2);
}
}
}, 10_000);
});
});

View File

@@ -1 +0,0 @@
*.zip

View File

@@ -1,112 +0,0 @@
Boxicons Free License
Free Icons offered by Boxicons is open source. You can use them for your personal and commercial projects.
Icons
----------------------------------------------------------------------------------------------
# Icons are free under — CC 4.0 License
You are free to:
- Share — copy and redistribute the material in any medium or format for any purpose, even commercially.
- Adapt — remix, transform, and build upon the material for any purpose, even commercially.
- The licensor cannot revoke these freedoms as long as you follow the license terms.
Under the following terms:
- Attribution — You must give appropriate credit , provide a link to the license, and indicate if changes were made . You may do so in any reasonable manner, but not in any way that suggests the licensor endorses you or your use.
- No additional restrictions — You may not apply legal terms or technological measures that legally restrict others from doing anything the license permits.
Notices:
You do not have to comply with the license for elements of the material in the public domain or where your use is permitted by an applicable exception or limitation .
No warranties are given. The license may not give you all of the permissions necessary for your intended use. For example, other rights such as publicity, privacy, or moral rights may limit how you use the material.
----------------------------------------------------------------------------------------------
# Fonts
Fonts are free under - SIL OFL 1.1 License:
PREAMBLE
The goals of the Open Font License (OFL) are to stimulate worldwide development of collaborative font projects, to support the font creation efforts of academic and linguistic communities, and to provide a free and open framework in which fonts may be shared and improved in partnership
with others.
The OFL allows the licensed fonts to be used, studied, modified and redistributed freely as long as they are not sold by themselves. The fonts, including any derivative works, can be bundled, embedded, redistributed and/or sold with any software provided that any reserved
names are not used by derivative works. The fonts and derivatives, however, cannot be released under any other type of license. The requirement for fonts to remain under this license does not apply to any document created using the fonts or their derivatives.
DEFINITIONS
“Font Software” refers to the set of files released by the Copyright Holder(s) under this license and clearly marked as such. This may include source files, build scripts and documentation.
“Reserved Font Name” refers to any names specified as such after the copyright statement(s).
“Original Version” refers to the collection of Font Software components as distributed by the Copyright Holder(s).
“Modified Version” refers to any derivative made by adding to, deleting, or substituting in part or in whole any of the components of the Original Version, by changing formats or by porting the Font Software to a new environment.
“Author” refers to any designer, engineer, programmer, technical writer or other person who contributed to the Font Software.
PERMISSION & CONDITIONS
Permission is hereby granted, free of charge, to any person obtaining a copy of the Font Software, to use, study, copy, merge, embed, modify, redistribute, and sell modified and unmodified copies of the Font Software, subject to the following conditions:
1 - Neither the Font Software nor any of its individual components, in Original or Modified Versions, may be sold by itself.
2 - Original or Modified Versions of the Font Software may be bundled, redistributed and/or sold with any software, provided that each copy contains the above copyright notice and this license. These can be included either as stand-alone text files, human-readable headers or in the appropriate machine-readable metadata fields within text or binary files as long as those fields can be easily viewed by the user.
3 - No Modified Version of the Font Software may use the Reserved Font Name(s) unless explicit written permission is granted by the corresponding Copyright Holder. This restriction only applies to the primary font name as presented to the users.
4 - The name(s) of the Copyright Holder(s) or the Author(s) of the Font Software shall not be used to promote, endorse or advertise any Modified Version, except to acknowledge the contribution(s) of the Copyright Holder(s) and the Author(s) or with their explicit written permission.
5 - The Font Software, modified or unmodified, in part or in whole, must be distributed entirely under this license, and must not be distributed under any other license. The requirement for fonts to remain under this license does not apply to any document created using the Font Software.
TERMINATION
This license becomes null and void if any of the above conditions are
not met.
DISCLAIMER
THE FONT SOFTWARE IS PROVIDED “AS IS”, WITHOUT WARRANTY OF ANY KIND,
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO ANY WARRANTIES OF
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT
OF COPYRIGHT, PATENT, TRADEMARK, OR OTHER RIGHT. IN NO EVENT SHALL THE
COPYRIGHT HOLDER BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
INCLUDING ANY GENERAL, SPECIAL, INDIRECT, INCIDENTAL, OR CONSEQUENTIAL
DAMAGES, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
FROM, OUT OF THE USE OR INABILITY TO USE THE FONT SOFTWARE OR FROM
OTHER DEALINGS IN THE FONT SOFTWARE.
----------------------------------------------------------------------------------------------
# Code is free under - MIT License
Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the “Software”), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED “AS IS”, WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
----------------------------------------------------------------------------------------------
# Attribution
Attribution is required according to MIT,SIL OFL 1.1 and CC 4.0 License.
They are already included in the Free icons , you are not required to take any actions
----------------------------------------------------------------------------------------------
# Brand Icons License
The brand icons offered in Boxicons are trademarks of their respective brands. Their usage here is solely intended to represent the respective brands and does not imply any affiliation or endorsement.
Copyright 2025 Boxicons.

View File

@@ -1,515 +0,0 @@
@-webkit-keyframes spin
{
0%
{
-webkit-transform: rotate(0);
transform: rotate(0);
}
100%
{
-webkit-transform: rotate(359deg);
transform: rotate(359deg);
}
}
@keyframes spin
{
0%
{
-webkit-transform: rotate(0);
transform: rotate(0);
}
100%
{
-webkit-transform: rotate(359deg);
transform: rotate(359deg);
}
}
@-webkit-keyframes burst
{
0%
{
-webkit-transform: scale(1);
transform: scale(1);
opacity: 1;
}
90%
{
-webkit-transform: scale(1.5);
transform: scale(1.5);
opacity: 0;
}
}
@keyframes burst
{
0%
{
-webkit-transform: scale(1);
transform: scale(1);
opacity: 1;
}
90%
{
-webkit-transform: scale(1.5);
transform: scale(1.5);
opacity: 0;
}
}
@-webkit-keyframes flashing
{
0%
{
opacity: 1;
}
45%
{
opacity: 0;
}
90%
{
opacity: 1;
}
}
@keyframes flashing
{
0%
{
opacity: 1;
}
45%
{
opacity: 0;
}
90%
{
opacity: 1;
}
}
@-webkit-keyframes fade-left
{
0%
{
-webkit-transform: translateX(0);
transform: translateX(0);
opacity: 1;
}
75%
{
-webkit-transform: translateX(-20px);
transform: translateX(-20px);
opacity: 0;
}
}
@keyframes fade-left
{
0%
{
-webkit-transform: translateX(0);
transform: translateX(0);
opacity: 1;
}
75%
{
-webkit-transform: translateX(-20px);
transform: translateX(-20px);
opacity: 0;
}
}
@-webkit-keyframes fade-right
{
0%
{
-webkit-transform: translateX(0);
transform: translateX(0);
opacity: 1;
}
75%
{
-webkit-transform: translateX(20px);
transform: translateX(20px);
opacity: 0;
}
}
@keyframes fade-right
{
0%
{
-webkit-transform: translateX(0);
transform: translateX(0);
opacity: 1;
}
75%
{
-webkit-transform: translateX(20px);
transform: translateX(20px);
opacity: 0;
}
}
@-webkit-keyframes fade-up
{
0%
{
-webkit-transform: translateY(0);
transform: translateY(0);
opacity: 1;
}
75%
{
-webkit-transform: translateY(-20px);
transform: translateY(-20px);
opacity: 0;
}
}
@keyframes fade-up
{
0%
{
-webkit-transform: translateY(0);
transform: translateY(0);
opacity: 1;
}
75%
{
-webkit-transform: translateY(-20px);
transform: translateY(-20px);
opacity: 0;
}
}
@-webkit-keyframes fade-down
{
0%
{
-webkit-transform: translateY(0);
transform: translateY(0);
opacity: 1;
}
75%
{
-webkit-transform: translateY(20px);
transform: translateY(20px);
opacity: 0;
}
}
@keyframes fade-down
{
0%
{
-webkit-transform: translateY(0);
transform: translateY(0);
opacity: 1;
}
75%
{
-webkit-transform: translateY(20px);
transform: translateY(20px);
opacity: 0;
}
}
@-webkit-keyframes tada
{
from
{
-webkit-transform: scale3d(1, 1, 1);
transform: scale3d(1, 1, 1);
}
10%,
20%
{
-webkit-transform: scale3d(.95, .95, .95) rotate3d(0, 0, 1, -10deg);
transform: scale3d(.95, .95, .95) rotate3d(0, 0, 1, -10deg);
}
30%,
50%,
70%,
90%
{
-webkit-transform: scale3d(1, 1, 1) rotate3d(0, 0, 1, 10deg);
transform: scale3d(1, 1, 1) rotate3d(0, 0, 1, 10deg);
}
40%,
60%,
80%
{
-webkit-transform: scale3d(1, 1, 1) rotate3d(0, 0, 1, -10deg);
transform: scale3d(1, 1, 1) rotate3d(0, 0, 1, -10deg);
}
to
{
-webkit-transform: scale3d(1, 1, 1);
transform: scale3d(1, 1, 1);
}
}
@keyframes tada
{
from
{
-webkit-transform: scale3d(1, 1, 1);
transform: scale3d(1, 1, 1);
}
10%,
20%
{
-webkit-transform: scale3d(.95, .95, .95) rotate3d(0, 0, 1, -10deg);
transform: scale3d(.95, .95, .95) rotate3d(0, 0, 1, -10deg);
}
30%,
50%,
70%,
90%
{
-webkit-transform: scale3d(1, 1, 1) rotate3d(0, 0, 1, 10deg);
transform: scale3d(1, 1, 1) rotate3d(0, 0, 1, 10deg);
}
40%,
60%,
80%
{
-webkit-transform: rotate3d(0, 0, 1, -10deg);
transform: rotate3d(0, 0, 1, -10deg);
}
to
{
-webkit-transform: scale3d(1, 1, 1);
transform: scale3d(1, 1, 1);
}
}
@keyframes beat {
to {
-webkit-transform: scale(1.4);
transform: scale(1.4);
}
}
@keyframes bounce
{
from
{
-webkit-transform: scale(1.1,1);
transform: scale(1.1,1);
}
25%
{
-webkit-transform: scale(0.9,1) translateY(-.25em);
transform: scale(0.9,1) translateY(-.25em);
}
50%
{
-webkit-transform: scale(1.1,0.9);
transform: scale(1.1,0.9);
}
75%
{
-webkit-transform: scale(1, 1);
transform: scale(1, 1);
}
87.5%
{
-webkit-transform: scale(1, 1) translateY(-.1em);
transform: scale(1, 1) translateY(-.1em);
}
to
{
-webkit-transform: scale(1, 1);
transform: scale(1, 1);
}
}
@keyframes breathe {
from{
-webkit-transform: scale(1);
transform: scale(1);
opacity: 1;
}
50%{
-webkit-transform: scale(1.4);
transform: scale(1.4);
opacity: 0.4;
}
to {
-webkit-transform: scale(1);
transform: scale(1);
opacity: 1;
}
}
@keyframes wiggle {
from{
-webkit-transform: translateX(0);
transform:translateX(0);
}
30%{
-webkit-transform: translateX(0.075em);
transform: translateX(0.075em);
}
60%{
-webkit-transform: translateX(-0.075em);
transform: translateX(-0.075em);
}
75%{
-webkit-transform: translateX(0.025em);
transform: translateX(0.025em);
}
90%{
-webkit-transform: translateX(-0.025em);
transform: translateX(-0.025em);
}
to {
-webkit-transform: translateX(0);
transform:translateX(0);
}
}
.bx-wiggle{
-webkit-animation: wiggle 1s infinite;
animation: wiggle 1s infinite;
animation-timing-function:cubic-bezier(.23,.57,.79,.58);
}
.bx-wiggle-hover:hover{
-webkit-animation: wiggle 1s infinite;
animation: wiggle 1s infinite;
animation-timing-function:cubic-bezier(.23,.57,.79,.58);
}
.bx-breathe{
-webkit-animation: breathe 3s infinite;
animation: breathe 3s infinite ease-in-out;
}
.bx-breathe-hover:hover
{
-webkit-animation: breathe 3s infinite;
animation: breathe 3s infinite ease-in-out;
}
.bx-bounce{
-webkit-animation: bounce 1s infinite;
animation: bounce 1s infinite;
animation-timing-function: cubic-bezier(.98,.97,.64,1.62);
}
.bx-bounce-hover:hover
{
-webkit-animation: bounce 1s infinite;
animation: bounce 1s infinite;
animation-timing-function: cubic-bezier(.98,.97,.64,1.62);
}
.bx-beat
{
-webkit-animation: beat .5s infinite alternate;
animation: beat .5s infinite alternate;
animation-timing-function: cubic-bezier(.19,.96,.65,1);
transform-origin: center;
}
.bx-spin
{
-webkit-animation: spin 2s linear infinite;
animation: spin 2s linear infinite;
}
.bx-spin-hover:hover
{
-webkit-animation: spin 2s linear infinite;
animation: spin 2s linear infinite;
}
.bx-tada
{
-webkit-animation: tada 1.5s ease infinite;
animation: tada 1.5s ease infinite;
}
.bx-tada-hover:hover
{
-webkit-animation: tada 1.5s ease infinite;
animation: tada 1.5s ease infinite;
}
.bx-flashing
{
-webkit-animation: flashing 1.5s infinite linear;
animation: flashing 1.5s infinite linear;
}
.bx-flashing-hover:hover
{
-webkit-animation: flashing 1.5s infinite linear;
animation: flashing 1.5s infinite linear;
}
.bx-burst
{
-webkit-animation: burst 1.5s infinite linear;
animation: burst 1.5s infinite linear;
}
.bx-burst-hover:hover
{
-webkit-animation: burst 1.5s infinite linear;
animation: burst 1.5s infinite linear;
}
.bx-fade-up
{
-webkit-animation: fade-up 1.5s infinite linear;
animation: fade-up 1.5s infinite linear;
}
.bx-fade-up-hover:hover
{
-webkit-animation: fade-up 1.5s infinite linear;
animation: fade-up 1.5s infinite linear;
}
.bx-fade-down
{
-webkit-animation: fade-down 1.5s infinite linear;
animation: fade-down 1.5s infinite linear;
}
.bx-fade-down-hover:hover
{
-webkit-animation: fade-down 1.5s infinite linear;
animation: fade-down 1.5s infinite linear;
}
.bx-fade-left
{
-webkit-animation: fade-left 1.5s infinite linear;
animation: fade-left 1.5s infinite linear;
}
.bx-fade-left-hover:hover
{
-webkit-animation: fade-left 1.5s infinite linear;
animation: fade-left 1.5s infinite linear;
}
.bx-fade-right
{
-webkit-animation: fade-right 1.5s infinite linear;
animation: fade-right 1.5s infinite linear;
}
.bx-fade-right-hover:hover
{
-webkit-animation: fade-right 1.5s infinite linear;
animation: fade-right 1.5s infinite linear;
}

File diff suppressed because one or more lines are too long

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because one or more lines are too long

View File

@@ -1,515 +0,0 @@
@-webkit-keyframes spin
{
0%
{
-webkit-transform: rotate(0);
transform: rotate(0);
}
100%
{
-webkit-transform: rotate(359deg);
transform: rotate(359deg);
}
}
@keyframes spin
{
0%
{
-webkit-transform: rotate(0);
transform: rotate(0);
}
100%
{
-webkit-transform: rotate(359deg);
transform: rotate(359deg);
}
}
@-webkit-keyframes burst
{
0%
{
-webkit-transform: scale(1);
transform: scale(1);
opacity: 1;
}
90%
{
-webkit-transform: scale(1.5);
transform: scale(1.5);
opacity: 0;
}
}
@keyframes burst
{
0%
{
-webkit-transform: scale(1);
transform: scale(1);
opacity: 1;
}
90%
{
-webkit-transform: scale(1.5);
transform: scale(1.5);
opacity: 0;
}
}
@-webkit-keyframes flashing
{
0%
{
opacity: 1;
}
45%
{
opacity: 0;
}
90%
{
opacity: 1;
}
}
@keyframes flashing
{
0%
{
opacity: 1;
}
45%
{
opacity: 0;
}
90%
{
opacity: 1;
}
}
@-webkit-keyframes fade-left
{
0%
{
-webkit-transform: translateX(0);
transform: translateX(0);
opacity: 1;
}
75%
{
-webkit-transform: translateX(-20px);
transform: translateX(-20px);
opacity: 0;
}
}
@keyframes fade-left
{
0%
{
-webkit-transform: translateX(0);
transform: translateX(0);
opacity: 1;
}
75%
{
-webkit-transform: translateX(-20px);
transform: translateX(-20px);
opacity: 0;
}
}
@-webkit-keyframes fade-right
{
0%
{
-webkit-transform: translateX(0);
transform: translateX(0);
opacity: 1;
}
75%
{
-webkit-transform: translateX(20px);
transform: translateX(20px);
opacity: 0;
}
}
@keyframes fade-right
{
0%
{
-webkit-transform: translateX(0);
transform: translateX(0);
opacity: 1;
}
75%
{
-webkit-transform: translateX(20px);
transform: translateX(20px);
opacity: 0;
}
}
@-webkit-keyframes fade-up
{
0%
{
-webkit-transform: translateY(0);
transform: translateY(0);
opacity: 1;
}
75%
{
-webkit-transform: translateY(-20px);
transform: translateY(-20px);
opacity: 0;
}
}
@keyframes fade-up
{
0%
{
-webkit-transform: translateY(0);
transform: translateY(0);
opacity: 1;
}
75%
{
-webkit-transform: translateY(-20px);
transform: translateY(-20px);
opacity: 0;
}
}
@-webkit-keyframes fade-down
{
0%
{
-webkit-transform: translateY(0);
transform: translateY(0);
opacity: 1;
}
75%
{
-webkit-transform: translateY(20px);
transform: translateY(20px);
opacity: 0;
}
}
@keyframes fade-down
{
0%
{
-webkit-transform: translateY(0);
transform: translateY(0);
opacity: 1;
}
75%
{
-webkit-transform: translateY(20px);
transform: translateY(20px);
opacity: 0;
}
}
@-webkit-keyframes tada
{
from
{
-webkit-transform: scale3d(1, 1, 1);
transform: scale3d(1, 1, 1);
}
10%,
20%
{
-webkit-transform: scale3d(.95, .95, .95) rotate3d(0, 0, 1, -10deg);
transform: scale3d(.95, .95, .95) rotate3d(0, 0, 1, -10deg);
}
30%,
50%,
70%,
90%
{
-webkit-transform: scale3d(1, 1, 1) rotate3d(0, 0, 1, 10deg);
transform: scale3d(1, 1, 1) rotate3d(0, 0, 1, 10deg);
}
40%,
60%,
80%
{
-webkit-transform: scale3d(1, 1, 1) rotate3d(0, 0, 1, -10deg);
transform: scale3d(1, 1, 1) rotate3d(0, 0, 1, -10deg);
}
to
{
-webkit-transform: scale3d(1, 1, 1);
transform: scale3d(1, 1, 1);
}
}
@keyframes tada
{
from
{
-webkit-transform: scale3d(1, 1, 1);
transform: scale3d(1, 1, 1);
}
10%,
20%
{
-webkit-transform: scale3d(.95, .95, .95) rotate3d(0, 0, 1, -10deg);
transform: scale3d(.95, .95, .95) rotate3d(0, 0, 1, -10deg);
}
30%,
50%,
70%,
90%
{
-webkit-transform: scale3d(1, 1, 1) rotate3d(0, 0, 1, 10deg);
transform: scale3d(1, 1, 1) rotate3d(0, 0, 1, 10deg);
}
40%,
60%,
80%
{
-webkit-transform: rotate3d(0, 0, 1, -10deg);
transform: rotate3d(0, 0, 1, -10deg);
}
to
{
-webkit-transform: scale3d(1, 1, 1);
transform: scale3d(1, 1, 1);
}
}
@keyframes beat {
to {
-webkit-transform: scale(1.4);
transform: scale(1.4);
}
}
@keyframes bounce
{
from
{
-webkit-transform: scale(1.1,1);
transform: scale(1.1,1);
}
25%
{
-webkit-transform: scale(0.9,1) translateY(-.25em);
transform: scale(0.9,1) translateY(-.25em);
}
50%
{
-webkit-transform: scale(1.1,0.9);
transform: scale(1.1,0.9);
}
75%
{
-webkit-transform: scale(1, 1);
transform: scale(1, 1);
}
87.5%
{
-webkit-transform: scale(1, 1) translateY(-.1em);
transform: scale(1, 1) translateY(-.1em);
}
to
{
-webkit-transform: scale(1, 1);
transform: scale(1, 1);
}
}
@keyframes breathe {
from{
-webkit-transform: scale(1);
transform: scale(1);
opacity: 1;
}
50%{
-webkit-transform: scale(1.4);
transform: scale(1.4);
opacity: 0.4;
}
to {
-webkit-transform: scale(1);
transform: scale(1);
opacity: 1;
}
}
@keyframes wiggle {
from{
-webkit-transform: translateX(0);
transform:translateX(0);
}
30%{
-webkit-transform: translateX(0.075em);
transform: translateX(0.075em);
}
60%{
-webkit-transform: translateX(-0.075em);
transform: translateX(-0.075em);
}
75%{
-webkit-transform: translateX(0.025em);
transform: translateX(0.025em);
}
90%{
-webkit-transform: translateX(-0.025em);
transform: translateX(-0.025em);
}
to {
-webkit-transform: translateX(0);
transform:translateX(0);
}
}
.bx-wiggle{
-webkit-animation: wiggle 1s infinite;
animation: wiggle 1s infinite;
animation-timing-function:cubic-bezier(.23,.57,.79,.58);
}
.bx-wiggle-hover:hover{
-webkit-animation: wiggle 1s infinite;
animation: wiggle 1s infinite;
animation-timing-function:cubic-bezier(.23,.57,.79,.58);
}
.bx-breathe{
-webkit-animation: breathe 3s infinite;
animation: breathe 3s infinite ease-in-out;
}
.bx-breathe-hover:hover
{
-webkit-animation: breathe 3s infinite;
animation: breathe 3s infinite ease-in-out;
}
.bx-bounce{
-webkit-animation: bounce 1s infinite;
animation: bounce 1s infinite;
animation-timing-function: cubic-bezier(.98,.97,.64,1.62);
}
.bx-bounce-hover:hover
{
-webkit-animation: bounce 1s infinite;
animation: bounce 1s infinite;
animation-timing-function: cubic-bezier(.98,.97,.64,1.62);
}
.bx-beat
{
-webkit-animation: beat .5s infinite alternate;
animation: beat .5s infinite alternate;
animation-timing-function: cubic-bezier(.19,.96,.65,1);
transform-origin: center;
}
.bx-spin
{
-webkit-animation: spin 2s linear infinite;
animation: spin 2s linear infinite;
}
.bx-spin-hover:hover
{
-webkit-animation: spin 2s linear infinite;
animation: spin 2s linear infinite;
}
.bx-tada
{
-webkit-animation: tada 1.5s ease infinite;
animation: tada 1.5s ease infinite;
}
.bx-tada-hover:hover
{
-webkit-animation: tada 1.5s ease infinite;
animation: tada 1.5s ease infinite;
}
.bx-flashing
{
-webkit-animation: flashing 1.5s infinite linear;
animation: flashing 1.5s infinite linear;
}
.bx-flashing-hover:hover
{
-webkit-animation: flashing 1.5s infinite linear;
animation: flashing 1.5s infinite linear;
}
.bx-burst
{
-webkit-animation: burst 1.5s infinite linear;
animation: burst 1.5s infinite linear;
}
.bx-burst-hover:hover
{
-webkit-animation: burst 1.5s infinite linear;
animation: burst 1.5s infinite linear;
}
.bx-fade-up
{
-webkit-animation: fade-up 1.5s infinite linear;
animation: fade-up 1.5s infinite linear;
}
.bx-fade-up-hover:hover
{
-webkit-animation: fade-up 1.5s infinite linear;
animation: fade-up 1.5s infinite linear;
}
.bx-fade-down
{
-webkit-animation: fade-down 1.5s infinite linear;
animation: fade-down 1.5s infinite linear;
}
.bx-fade-down-hover:hover
{
-webkit-animation: fade-down 1.5s infinite linear;
animation: fade-down 1.5s infinite linear;
}
.bx-fade-left
{
-webkit-animation: fade-left 1.5s infinite linear;
animation: fade-left 1.5s infinite linear;
}
.bx-fade-left-hover:hover
{
-webkit-animation: fade-left 1.5s infinite linear;
animation: fade-left 1.5s infinite linear;
}
.bx-fade-right
{
-webkit-animation: fade-right 1.5s infinite linear;
animation: fade-right 1.5s infinite linear;
}
.bx-fade-right-hover:hover
{
-webkit-animation: fade-right 1.5s infinite linear;
animation: fade-right 1.5s infinite linear;
}

View File

@@ -1,906 +0,0 @@
@font-face {
font-family: "boxicons-brands";
src: url("./boxicons-brands.ttf?945bfe89057cc7627be1ecdc648441aa") format("truetype"),
url("./boxicons-brands.woff?945bfe89057cc7627be1ecdc648441aa") format("woff"),
url("./boxicons-brands.woff2?945bfe89057cc7627be1ecdc648441aa") format("woff2");
}
.bxl {
font-family: boxicons-brands !important;
font-style: normal;
font-weight: normal;
font-variant: normal;
text-transform: none;
line-height: 1;
display:inline-block;
speak:none;
-webkit-font-smoothing: antialiased;
-moz-osx-font-smoothing: grayscale;
}
.bxl.variable-selector-00:before {
content: "\fb1e";
}
.bxl.bx-500px:before {
content: "\f101";
}
.bxl.bx-99designs:before {
content: "\f102";
}
.bxl.bx-adobe:before {
content: "\f103";
}
.bxl.bx-airbnb:before {
content: "\f104";
}
.bxl.bx-algolia:before {
content: "\f105";
}
.bxl.bx-amazon:before {
content: "\f106";
}
.bxl.bx-amex:before {
content: "\f107";
}
.bxl.bx-android:before {
content: "\f108";
}
.bxl.bx-angular:before {
content: "\f109";
}
.bxl.bx-anthropic:before {
content: "\f10a";
}
.bxl.bx-apple-music:before {
content: "\f10b";
}
.bxl.bx-apple:before {
content: "\f10c";
}
.bxl.bx-arc-browser:before {
content: "\f10d";
}
.bxl.bx-artstation:before {
content: "\f10e";
}
.bxl.bx-asana:before {
content: "\f10f";
}
.bxl.bx-atlassian:before {
content: "\f110";
}
.bxl.bx-atom-editor:before {
content: "\f111";
}
.bxl.bx-audible:before {
content: "\f112";
}
.bxl.bx-auth0:before {
content: "\f113";
}
.bxl.bx-autodesk:before {
content: "\f114";
}
.bxl.bx-aws:before {
content: "\f115";
}
.bxl.bx-baidu:before {
content: "\f116";
}
.bxl.bx-bash:before {
content: "\f117";
}
.bxl.bx-behance:before {
content: "\f118";
}
.bxl.bx-better-auth:before {
content: "\f119";
}
.bxl.bx-bing:before {
content: "\f11a";
}
.bxl.bx-bitcoin-logo:before {
content: "\f11b";
}
.bxl.bx-blender:before {
content: "\f11c";
}
.bxl.bx-blogger:before {
content: "\f11d";
}
.bxl.bx-bluesky:before {
content: "\f11e";
}
.bxl.bx-bolt-b:before {
content: "\f11f";
}
.bxl.bx-bootstrap:before {
content: "\f120";
}
.bxl.bx-boxicons:before {
content: "\f121";
}
.bxl.bx-brave-browser:before {
content: "\f122";
}
.bxl.bx-bun:before {
content: "\f123";
}
.bxl.bx-buy-me-a-coffee:before {
content: "\f124";
}
.bxl.bx-c-plus-plus:before {
content: "\f125";
}
.bxl.bx-c-sharp:before {
content: "\f126";
}
.bxl.bx-c:before {
content: "\f127";
}
.bxl.bx-canva:before {
content: "\f128";
}
.bxl.bx-chess-com:before {
content: "\f129";
}
.bxl.bx-chrome:before {
content: "\f12a";
}
.bxl.bx-claude-ai:before {
content: "\f12b";
}
.bxl.bx-clerk:before {
content: "\f12c";
}
.bxl.bx-cloudflare:before {
content: "\f12d";
}
.bxl.bx-codepen:before {
content: "\f12e";
}
.bxl.bx-convex:before {
content: "\f12f";
}
.bxl.bx-creative-commons:before {
content: "\f130";
}
.bxl.bx-crunchyroll:before {
content: "\f131";
}
.bxl.bx-css3:before {
content: "\f132";
}
.bxl.bx-cursor-ai:before {
content: "\f133";
}
.bxl.bx-dailymotion:before {
content: "\f134";
}
.bxl.bx-deepmind:before {
content: "\f135";
}
.bxl.bx-deepseek:before {
content: "\f136";
}
.bxl.bx-deezer:before {
content: "\f137";
}
.bxl.bx-deno:before {
content: "\f138";
}
.bxl.bx-dev-to:before {
content: "\f139";
}
.bxl.bx-deviantart:before {
content: "\f13a";
}
.bxl.bx-devpost:before {
content: "\f13b";
}
.bxl.bx-digg:before {
content: "\f13c";
}
.bxl.bx-digitalocean:before {
content: "\f13d";
}
.bxl.bx-discord-alt:before {
content: "\f13e";
}
.bxl.bx-discord:before {
content: "\f13f";
}
.bxl.bx-discourse:before {
content: "\f140";
}
.bxl.bx-discover:before {
content: "\f141";
}
.bxl.bx-django:before {
content: "\f142";
}
.bxl.bx-docker:before {
content: "\f143";
}
.bxl.bx-dot-env:before {
content: "\f144";
}
.bxl.bx-dribbble:before {
content: "\f145";
}
.bxl.bx-drizzle-orm:before {
content: "\f146";
}
.bxl.bx-dropbox:before {
content: "\f147";
}
.bxl.bx-ebay:before {
content: "\f148";
}
.bxl.bx-edge:before {
content: "\f149";
}
.bxl.bx-ember-js:before {
content: "\f14a";
}
.bxl.bx-etsy:before {
content: "\f14b";
}
.bxl.bx-expo:before {
content: "\f14c";
}
.bxl.bx-express-js:before {
content: "\f14d";
}
.bxl.bx-facebook-circle:before {
content: "\f14e";
}
.bxl.bx-facebook-square:before {
content: "\f14f";
}
.bxl.bx-facebook:before {
content: "\f150";
}
.bxl.bx-fastapi:before {
content: "\f151";
}
.bxl.bx-fastify:before {
content: "\f152";
}
.bxl.bx-figma-alt:before {
content: "\f153";
}
.bxl.bx-figma:before {
content: "\f154";
}
.bxl.bx-firebase:before {
content: "\f155";
}
.bxl.bx-firefox:before {
content: "\f156";
}
.bxl.bx-fiverr:before {
content: "\f157";
}
.bxl.bx-flask-old:before {
content: "\f158";
}
.bxl.bx-flask:before {
content: "\f159";
}
.bxl.bx-flickr-square:before {
content: "\f15a";
}
.bxl.bx-flickr:before {
content: "\f15b";
}
.bxl.bx-flutter:before {
content: "\f15c";
}
.bxl.bx-foursquare:before {
content: "\f15d";
}
.bxl.bx-framer:before {
content: "\f15e";
}
.bxl.bx-gatsby-js:before {
content: "\f15f";
}
.bxl.bx-gemini:before {
content: "\f160";
}
.bxl.bx-git:before {
content: "\f161";
}
.bxl.bx-github-copilot:before {
content: "\f162";
}
.bxl.bx-github:before {
content: "\f163";
}
.bxl.bx-gitlab:before {
content: "\f164";
}
.bxl.bx-gmail:before {
content: "\f165";
}
.bxl.bx-go-lang:before {
content: "\f166";
}
.bxl.bx-google-antigravity:before {
content: "\f167";
}
.bxl.bx-google-cloud:before {
content: "\f168";
}
.bxl.bx-google-pay:before {
content: "\f169";
}
.bxl.bx-google:before {
content: "\f16a";
}
.bxl.bx-graphql:before {
content: "\f16b";
}
.bxl.bx-grok:before {
content: "\f16c";
}
.bxl.bx-groq-ai:before {
content: "\f16d";
}
.bxl.bx-gsap:before {
content: "\f16e";
}
.bxl.bx-gumroad:before {
content: "\f16f";
}
.bxl.bx-hashnode:before {
content: "\f170";
}
.bxl.bx-hcaptcha:before {
content: "\f171";
}
.bxl.bx-heroku:before {
content: "\f172";
}
.bxl.bx-hono-js:before {
content: "\f173";
}
.bxl.bx-html5:before {
content: "\f174";
}
.bxl.bx-hugo:before {
content: "\f175";
}
.bxl.bx-ibm:before {
content: "\f176";
}
.bxl.bx-imdb:before {
content: "\f177";
}
.bxl.bx-instagram-alt:before {
content: "\f178";
}
.bxl.bx-instagram:before {
content: "\f179";
}
.bxl.bx-internet-explorer:before {
content: "\f17a";
}
.bxl.bx-invision:before {
content: "\f17b";
}
.bxl.bx-java:before {
content: "\f17c";
}
.bxl.bx-javascript:before {
content: "\f17d";
}
.bxl.bx-joomla:before {
content: "\f17e";
}
.bxl.bx-jquery:before {
content: "\f17f";
}
.bxl.bx-jsfiddle:before {
content: "\f180";
}
.bxl.bx-jwt:before {
content: "\f181";
}
.bxl.bx-kick:before {
content: "\f182";
}
.bxl.bx-kickstarter:before {
content: "\f183";
}
.bxl.bx-kotlin:before {
content: "\f184";
}
.bxl.bx-kubernetes:before {
content: "\f185";
}
.bxl.bx-laravel:before {
content: "\f186";
}
.bxl.bx-leetcode:before {
content: "\f187";
}
.bxl.bx-lemon-squeezy:before {
content: "\f188";
}
.bxl.bx-less:before {
content: "\f189";
}
.bxl.bx-letterboxd:before {
content: "\f18a";
}
.bxl.bx-lichess:before {
content: "\f18b";
}
.bxl.bx-line-chat:before {
content: "\f18c";
}
.bxl.bx-linear-app:before {
content: "\f18d";
}
.bxl.bx-linkedin-square:before {
content: "\f18e";
}
.bxl.bx-linkedin:before {
content: "\f18f";
}
.bxl.bx-linktree:before {
content: "\f190";
}
.bxl.bx-loom:before {
content: "\f191";
}
.bxl.bx-lottie-files:before {
content: "\f192";
}
.bxl.bx-lottie-lab:before {
content: "\f193";
}
.bxl.bx-lovable:before {
content: "\f194";
}
.bxl.bx-lyft:before {
content: "\f195";
}
.bxl.bx-magento:before {
content: "\f196";
}
.bxl.bx-mailchimp:before {
content: "\f197";
}
.bxl.bx-markdown:before {
content: "\f198";
}
.bxl.bx-mastercard:before {
content: "\f199";
}
.bxl.bx-mastodon:before {
content: "\f19a";
}
.bxl.bx-mcp:before {
content: "\f19b";
}
.bxl.bx-medium-old:before {
content: "\f19c";
}
.bxl.bx-medium-square:before {
content: "\f19d";
}
.bxl.bx-medium:before {
content: "\f19e";
}
.bxl.bx-messenger:before {
content: "\f19f";
}
.bxl.bx-meta:before {
content: "\f1a0";
}
.bxl.bx-microsoft-teams:before {
content: "\f1a1";
}
.bxl.bx-microsoft-windows:before {
content: "\f1a2";
}
.bxl.bx-microsoft:before {
content: "\f1a3";
}
.bxl.bx-midjourney:before {
content: "\f1a4";
}
.bxl.bx-mongodb:before {
content: "\f1a5";
}
.bxl.bx-motion-js:before {
content: "\f1a6";
}
.bxl.bx-mozilla:before {
content: "\f1a7";
}
.bxl.bx-my-sql:before {
content: "\f1a8";
}
.bxl.bx-neon-tech:before {
content: "\f1a9";
}
.bxl.bx-neovim:before {
content: "\f1aa";
}
.bxl.bx-nest-js:before {
content: "\f1ab";
}
.bxl.bx-netlify:before {
content: "\f1ac";
}
.bxl.bx-next-js:before {
content: "\f1ad";
}
.bxl.bx-nodejs:before {
content: "\f1ae";
}
.bxl.bx-notion:before {
content: "\f1af";
}
.bxl.bx-npm:before {
content: "\f1b0";
}
.bxl.bx-nuxt-js:before {
content: "\f1b1";
}
.bxl.bx-ok-ru:before {
content: "\f1b2";
}
.bxl.bx-ollama:before {
content: "\f1b3";
}
.bxl.bx-openai:before {
content: "\f1b4";
}
.bxl.bx-opensea:before {
content: "\f1b5";
}
.bxl.bx-opera:before {
content: "\f1b6";
}
.bxl.bx-paddle-p:before {
content: "\f1b7";
}
.bxl.bx-paper-design:before {
content: "\f1b8";
}
.bxl.bx-patreon:before {
content: "\f1b9";
}
.bxl.bx-payload-cms:before {
content: "\f1ba";
}
.bxl.bx-paypal:before {
content: "\f1bb";
}
.bxl.bx-periscope:before {
content: "\f1bc";
}
.bxl.bx-perplexity-ai:before {
content: "\f1bd";
}
.bxl.bx-php:before {
content: "\f1be";
}
.bxl.bx-pinterest-alt:before {
content: "\f1bf";
}
.bxl.bx-pinterest:before {
content: "\f1c0";
}
.bxl.bx-planetscale:before {
content: "\f1c1";
}
.bxl.bx-play-store:before {
content: "\f1c2";
}
.bxl.bx-playstation:before {
content: "\f1c3";
}
.bxl.bx-pocket:before {
content: "\f1c4";
}
.bxl.bx-polar:before {
content: "\f1c5";
}
.bxl.bx-postgresql:before {
content: "\f1c6";
}
.bxl.bx-prisma-orm:before {
content: "\f1c7";
}
.bxl.bx-product-hunt:before {
content: "\f1c8";
}
.bxl.bx-python:before {
content: "\f1c9";
}
.bxl.bx-qdrant:before {
content: "\f1ca";
}
.bxl.bx-qq:before {
content: "\f1cb";
}
.bxl.bx-quora:before {
content: "\f1cc";
}
.bxl.bx-radix-ui:before {
content: "\f1cd";
}
.bxl.bx-railway:before {
content: "\f1ce";
}
.bxl.bx-rasberry-pi:before {
content: "\f1cf";
}
.bxl.bx-react-query:before {
content: "\f1d0";
}
.bxl.bx-react-router:before {
content: "\f1d1";
}
.bxl.bx-react:before {
content: "\f1d2";
}
.bxl.bx-redbubble:before {
content: "\f1d3";
}
.bxl.bx-reddit:before {
content: "\f1d4";
}
.bxl.bx-redux:before {
content: "\f1d5";
}
.bxl.bx-remix-js:before {
content: "\f1d6";
}
.bxl.bx-replit:before {
content: "\f1d7";
}
.bxl.bx-resend:before {
content: "\f1d8";
}
.bxl.bx-roblox:before {
content: "\f1d9";
}
.bxl.bx-sanity:before {
content: "\f1da";
}
.bxl.bx-sass:before {
content: "\f1db";
}
.bxl.bx-sentry:before {
content: "\f1dc";
}
.bxl.bx-shadcn-ui:before {
content: "\f1dd";
}
.bxl.bx-shopify:before {
content: "\f1de";
}
.bxl.bx-sketch:before {
content: "\f1df";
}
.bxl.bx-skype:before {
content: "\f1e0";
}
.bxl.bx-slack-old:before {
content: "\f1e1";
}
.bxl.bx-slack:before {
content: "\f1e2";
}
.bxl.bx-snapchat:before {
content: "\f1e3";
}
.bxl.bx-socket-io:before {
content: "\f1e4";
}
.bxl.bx-soundcloud:before {
content: "\f1e5";
}
.bxl.bx-spotify:before {
content: "\f1e6";
}
.bxl.bx-spring-boot:before {
content: "\f1e7";
}
.bxl.bx-squarespace:before {
content: "\f1e8";
}
.bxl.bx-sst:before {
content: "\f1e9";
}
.bxl.bx-stack-overflow:before {
content: "\f1ea";
}
.bxl.bx-stackblitz:before {
content: "\f1eb";
}
.bxl.bx-steam:before {
content: "\f1ec";
}
.bxl.bx-stripe:before {
content: "\f1ed";
}
.bxl.bx-supabase:before {
content: "\f1ee";
}
.bxl.bx-svelte:before {
content: "\f1ef";
}
.bxl.bx-tailwind-css:before {
content: "\f1f0";
}
.bxl.bx-telegram:before {
content: "\f1f1";
}
.bxl.bx-terraform:before {
content: "\f1f2";
}
.bxl.bx-threads:before {
content: "\f1f3";
}
.bxl.bx-three-js:before {
content: "\f1f4";
}
.bxl.bx-tiktok:before {
content: "\f1f5";
}
.bxl.bx-trello:before {
content: "\f1f6";
}
.bxl.bx-trip-advisor:before {
content: "\f1f7";
}
.bxl.bx-trpc:before {
content: "\f1f8";
}
.bxl.bx-trustpilot:before {
content: "\f1f9";
}
.bxl.bx-tumblr:before {
content: "\f1fa";
}
.bxl.bx-tux:before {
content: "\f1fb";
}
.bxl.bx-twitch:before {
content: "\f1fc";
}
.bxl.bx-twitter-x:before {
content: "\f1fd";
}
.bxl.bx-twitter:before {
content: "\f1fe";
}
.bxl.bx-typescript:before {
content: "\f1ff";
}
.bxl.bx-uber:before {
content: "\f200";
}
.bxl.bx-ubuntu:before {
content: "\f201";
}
.bxl.bx-udacity:before {
content: "\f202";
}
.bxl.bx-union-pay:before {
content: "\f203";
}
.bxl.bx-unity:before {
content: "\f204";
}
.bxl.bx-unsplash:before {
content: "\f205";
}
.bxl.bx-upi:before {
content: "\f206";
}
.bxl.bx-upwork:before {
content: "\f207";
}
.bxl.bx-v0:before {
content: "\f208";
}
.bxl.bx-venmo:before {
content: "\f209";
}
.bxl.bx-vercel:before {
content: "\f20a";
}
.bxl.bx-vimeo:before {
content: "\f20b";
}
.bxl.bx-visa:before {
content: "\f20c";
}
.bxl.bx-visual-studio:before {
content: "\f20d";
}
.bxl.bx-vite-js:before {
content: "\f20e";
}
.bxl.bx-vk:before {
content: "\f20f";
}
.bxl.bx-vuejs:before {
content: "\f210";
}
.bxl.bx-waze:before {
content: "\f211";
}
.bxl.bx-web-components:before {
content: "\f212";
}
.bxl.bx-webflow:before {
content: "\f213";
}
.bxl.bx-wechat:before {
content: "\f214";
}
.bxl.bx-weibo:before {
content: "\f215";
}
.bxl.bx-whatsapp-square:before {
content: "\f216";
}
.bxl.bx-whatsapp:before {
content: "\f217";
}
.bxl.bx-wikipedia:before {
content: "\f218";
}
.bxl.bx-windsurf:before {
content: "\f219";
}
.bxl.bx-wix:before {
content: "\f21a";
}
.bxl.bx-wordpress:before {
content: "\f21b";
}
.bxl.bx-work-os:before {
content: "\f21c";
}
.bxl.bx-xai:before {
content: "\f21d";
}
.bxl.bx-xbox:before {
content: "\f21e";
}
.bxl.bx-xing:before {
content: "\f21f";
}
.bxl.bx-yahoo:before {
content: "\f220";
}
.bxl.bx-yarn:before {
content: "\f221";
}
.bxl.bx-yelp:before {
content: "\f222";
}
.bxl.bx-youtube-music:before {
content: "\f223";
}
.bxl.bx-youtube:before {
content: "\f224";
}
.bxl.bx-zen-browser:before {
content: "\f225";
}
.bxl.bx-zoom-workplace:before {
content: "\f226";
}

View File

@@ -1,297 +0,0 @@
{
"variable-selector-00": 64286,
"bx-500px": 61697,
"bx-99designs": 61698,
"bx-adobe": 61699,
"bx-airbnb": 61700,
"bx-algolia": 61701,
"bx-amazon": 61702,
"bx-amex": 61703,
"bx-android": 61704,
"bx-angular": 61705,
"bx-anthropic": 61706,
"bx-apple-music": 61707,
"bx-apple": 61708,
"bx-arc-browser": 61709,
"bx-artstation": 61710,
"bx-asana": 61711,
"bx-atlassian": 61712,
"bx-atom-editor": 61713,
"bx-audible": 61714,
"bx-auth0": 61715,
"bx-autodesk": 61716,
"bx-aws": 61717,
"bx-baidu": 61718,
"bx-bash": 61719,
"bx-behance": 61720,
"bx-better-auth": 61721,
"bx-bing": 61722,
"bx-bitcoin-logo": 61723,
"bx-blender": 61724,
"bx-blogger": 61725,
"bx-bluesky": 61726,
"bx-bolt-b": 61727,
"bx-bootstrap": 61728,
"bx-boxicons": 61729,
"bx-brave-browser": 61730,
"bx-bun": 61731,
"bx-buy-me-a-coffee": 61732,
"bx-c-plus-plus": 61733,
"bx-c-sharp": 61734,
"bx-c": 61735,
"bx-canva": 61736,
"bx-chess-com": 61737,
"bx-chrome": 61738,
"bx-claude-ai": 61739,
"bx-clerk": 61740,
"bx-cloudflare": 61741,
"bx-codepen": 61742,
"bx-convex": 61743,
"bx-creative-commons": 61744,
"bx-crunchyroll": 61745,
"bx-css3": 61746,
"bx-cursor-ai": 61747,
"bx-dailymotion": 61748,
"bx-deepmind": 61749,
"bx-deepseek": 61750,
"bx-deezer": 61751,
"bx-deno": 61752,
"bx-dev-to": 61753,
"bx-deviantart": 61754,
"bx-devpost": 61755,
"bx-digg": 61756,
"bx-digitalocean": 61757,
"bx-discord-alt": 61758,
"bx-discord": 61759,
"bx-discourse": 61760,
"bx-discover": 61761,
"bx-django": 61762,
"bx-docker": 61763,
"bx-dot-env": 61764,
"bx-dribbble": 61765,
"bx-drizzle-orm": 61766,
"bx-dropbox": 61767,
"bx-ebay": 61768,
"bx-edge": 61769,
"bx-ember-js": 61770,
"bx-etsy": 61771,
"bx-expo": 61772,
"bx-express-js": 61773,
"bx-facebook-circle": 61774,
"bx-facebook-square": 61775,
"bx-facebook": 61776,
"bx-fastapi": 61777,
"bx-fastify": 61778,
"bx-figma-alt": 61779,
"bx-figma": 61780,
"bx-firebase": 61781,
"bx-firefox": 61782,
"bx-fiverr": 61783,
"bx-flask-old": 61784,
"bx-flask": 61785,
"bx-flickr-square": 61786,
"bx-flickr": 61787,
"bx-flutter": 61788,
"bx-foursquare": 61789,
"bx-framer": 61790,
"bx-gatsby-js": 61791,
"bx-gemini": 61792,
"bx-git": 61793,
"bx-github-copilot": 61794,
"bx-github": 61795,
"bx-gitlab": 61796,
"bx-gmail": 61797,
"bx-go-lang": 61798,
"bx-google-antigravity": 61799,
"bx-google-cloud": 61800,
"bx-google-pay": 61801,
"bx-google": 61802,
"bx-graphql": 61803,
"bx-grok": 61804,
"bx-groq-ai": 61805,
"bx-gsap": 61806,
"bx-gumroad": 61807,
"bx-hashnode": 61808,
"bx-hcaptcha": 61809,
"bx-heroku": 61810,
"bx-hono-js": 61811,
"bx-html5": 61812,
"bx-hugo": 61813,
"bx-ibm": 61814,
"bx-imdb": 61815,
"bx-instagram-alt": 61816,
"bx-instagram": 61817,
"bx-internet-explorer": 61818,
"bx-invision": 61819,
"bx-java": 61820,
"bx-javascript": 61821,
"bx-joomla": 61822,
"bx-jquery": 61823,
"bx-jsfiddle": 61824,
"bx-jwt": 61825,
"bx-kick": 61826,
"bx-kickstarter": 61827,
"bx-kotlin": 61828,
"bx-kubernetes": 61829,
"bx-laravel": 61830,
"bx-leetcode": 61831,
"bx-lemon-squeezy": 61832,
"bx-less": 61833,
"bx-letterboxd": 61834,
"bx-lichess": 61835,
"bx-line-chat": 61836,
"bx-linear-app": 61837,
"bx-linkedin-square": 61838,
"bx-linkedin": 61839,
"bx-linktree": 61840,
"bx-loom": 61841,
"bx-lottie-files": 61842,
"bx-lottie-lab": 61843,
"bx-lovable": 61844,
"bx-lyft": 61845,
"bx-magento": 61846,
"bx-mailchimp": 61847,
"bx-markdown": 61848,
"bx-mastercard": 61849,
"bx-mastodon": 61850,
"bx-mcp": 61851,
"bx-medium-old": 61852,
"bx-medium-square": 61853,
"bx-medium": 61854,
"bx-messenger": 61855,
"bx-meta": 61856,
"bx-microsoft-teams": 61857,
"bx-microsoft-windows": 61858,
"bx-microsoft": 61859,
"bx-midjourney": 61860,
"bx-mongodb": 61861,
"bx-motion-js": 61862,
"bx-mozilla": 61863,
"bx-my-sql": 61864,
"bx-neon-tech": 61865,
"bx-neovim": 61866,
"bx-nest-js": 61867,
"bx-netlify": 61868,
"bx-next-js": 61869,
"bx-nodejs": 61870,
"bx-notion": 61871,
"bx-npm": 61872,
"bx-nuxt-js": 61873,
"bx-ok-ru": 61874,
"bx-ollama": 61875,
"bx-openai": 61876,
"bx-opensea": 61877,
"bx-opera": 61878,
"bx-paddle-p": 61879,
"bx-paper-design": 61880,
"bx-patreon": 61881,
"bx-payload-cms": 61882,
"bx-paypal": 61883,
"bx-periscope": 61884,
"bx-perplexity-ai": 61885,
"bx-php": 61886,
"bx-pinterest-alt": 61887,
"bx-pinterest": 61888,
"bx-planetscale": 61889,
"bx-play-store": 61890,
"bx-playstation": 61891,
"bx-pocket": 61892,
"bx-polar": 61893,
"bx-postgresql": 61894,
"bx-prisma-orm": 61895,
"bx-product-hunt": 61896,
"bx-python": 61897,
"bx-qdrant": 61898,
"bx-qq": 61899,
"bx-quora": 61900,
"bx-radix-ui": 61901,
"bx-railway": 61902,
"bx-rasberry-pi": 61903,
"bx-react-query": 61904,
"bx-react-router": 61905,
"bx-react": 61906,
"bx-redbubble": 61907,
"bx-reddit": 61908,
"bx-redux": 61909,
"bx-remix-js": 61910,
"bx-replit": 61911,
"bx-resend": 61912,
"bx-roblox": 61913,
"bx-sanity": 61914,
"bx-sass": 61915,
"bx-sentry": 61916,
"bx-shadcn-ui": 61917,
"bx-shopify": 61918,
"bx-sketch": 61919,
"bx-skype": 61920,
"bx-slack-old": 61921,
"bx-slack": 61922,
"bx-snapchat": 61923,
"bx-socket-io": 61924,
"bx-soundcloud": 61925,
"bx-spotify": 61926,
"bx-spring-boot": 61927,
"bx-squarespace": 61928,
"bx-sst": 61929,
"bx-stack-overflow": 61930,
"bx-stackblitz": 61931,
"bx-steam": 61932,
"bx-stripe": 61933,
"bx-supabase": 61934,
"bx-svelte": 61935,
"bx-tailwind-css": 61936,
"bx-telegram": 61937,
"bx-terraform": 61938,
"bx-threads": 61939,
"bx-three-js": 61940,
"bx-tiktok": 61941,
"bx-trello": 61942,
"bx-trip-advisor": 61943,
"bx-trpc": 61944,
"bx-trustpilot": 61945,
"bx-tumblr": 61946,
"bx-tux": 61947,
"bx-twitch": 61948,
"bx-twitter-x": 61949,
"bx-twitter": 61950,
"bx-typescript": 61951,
"bx-uber": 61952,
"bx-ubuntu": 61953,
"bx-udacity": 61954,
"bx-union-pay": 61955,
"bx-unity": 61956,
"bx-unsplash": 61957,
"bx-upi": 61958,
"bx-upwork": 61959,
"bx-v0": 61960,
"bx-venmo": 61961,
"bx-vercel": 61962,
"bx-vimeo": 61963,
"bx-visa": 61964,
"bx-visual-studio": 61965,
"bx-vite-js": 61966,
"bx-vk": 61967,
"bx-vuejs": 61968,
"bx-waze": 61969,
"bx-web-components": 61970,
"bx-webflow": 61971,
"bx-wechat": 61972,
"bx-weibo": 61973,
"bx-whatsapp-square": 61974,
"bx-whatsapp": 61975,
"bx-wikipedia": 61976,
"bx-windsurf": 61977,
"bx-wix": 61978,
"bx-wordpress": 61979,
"bx-work-os": 61980,
"bx-xai": 61981,
"bx-xbox": 61982,
"bx-xing": 61983,
"bx-yahoo": 61984,
"bx-yarn": 61985,
"bx-yelp": 61986,
"bx-youtube-music": 61987,
"bx-youtube": 61988,
"bx-zen-browser": 61989,
"bx-zoom-workplace": 61990
}

File diff suppressed because one or more lines are too long

View File

@@ -1,108 +0,0 @@
.bx-rotate-90
{
transform: rotate(90deg);
-ms-filter: 'progid:DXImageTransform.Microsoft.BasicImage(rotation=1)';
}
.bx-rotate-180
{
transform: rotate(180deg);
-ms-filter: 'progid:DXImageTransform.Microsoft.BasicImage(rotation=2)';
}
.bx-rotate-270
{
transform: rotate(270deg);
-ms-filter: 'progid:DXImageTransform.Microsoft.BasicImage(rotation=3)';
}
.bx-flip-horizontal
{
transform: scaleX(-1);
-ms-filter: 'progid:DXImageTransform.Microsoft.BasicImage(rotation=0, mirror=1)';
}
.bx-flip-vertical
{
transform: scaleY(-1);
-ms-filter: 'progid:DXImageTransform.Microsoft.BasicImage(rotation=2, mirror=1)';
}
.bx-xs
{
font-size: 1rem!important;
}
.bx-sm
{
font-size: 1.55rem!important;
}
.bx-md
{
font-size: 2.25rem!important;
}
.bx-lg
{
font-size: 3.0rem!important;
}
.bx-fw
{
font-size: 1.2857142857em;
line-height: .8em;
width: 1.2857142857em;
height: .8em;
margin-top: -.2em!important;
vertical-align: middle;
}
.bx-pull-left
{
float: left;
margin-right: .3em!important;
}
.bx-pull-right
{
float: right;
margin-left: .3em!important;
}
.bx-border
{
padding: .25em;
border: .07em solid rgba(0,0,0,.1);
border-radius: .25em;
}
.bx-border-circle
{
padding: .25em;
border: .07em solid rgba(0,0,0,.1);
border-radius: 50%;
}
.bx-ul
{
margin-left: 2em;
padding-left: 0;
list-style: none;
}
.bx-ul > li
{
position: relative;
}
.bx-ul .bx,.bx-ul .bxr,.bx-ul .bxs
{
font-size: inherit;
line-height: inherit;
position: absolute;
left: -2em;
width: 2em;
text-align: center;
}

View File

@@ -1,108 +0,0 @@
.bx-rotate-90
{
transform: rotate(90deg);
-ms-filter: 'progid:DXImageTransform.Microsoft.BasicImage(rotation=1)';
}
.bx-rotate-180
{
transform: rotate(180deg);
-ms-filter: 'progid:DXImageTransform.Microsoft.BasicImage(rotation=2)';
}
.bx-rotate-270
{
transform: rotate(270deg);
-ms-filter: 'progid:DXImageTransform.Microsoft.BasicImage(rotation=3)';
}
.bx-flip-horizontal
{
transform: scaleX(-1);
-ms-filter: 'progid:DXImageTransform.Microsoft.BasicImage(rotation=0, mirror=1)';
}
.bx-flip-vertical
{
transform: scaleY(-1);
-ms-filter: 'progid:DXImageTransform.Microsoft.BasicImage(rotation=2, mirror=1)';
}
.bx-xs
{
font-size: 1rem!important;
}
.bx-sm
{
font-size: 1.55rem!important;
}
.bx-md
{
font-size: 2.25rem!important;
}
.bx-lg
{
font-size: 3.0rem!important;
}
.bx-fw
{
font-size: 1.2857142857em;
line-height: .8em;
width: 1.2857142857em;
height: .8em;
margin-top: -.2em!important;
vertical-align: middle;
}
.bx-pull-left
{
float: left;
margin-right: .3em!important;
}
.bx-pull-right
{
float: right;
margin-left: .3em!important;
}
.bx-border
{
padding: .25em;
border: .07em solid rgba(0,0,0,.1);
border-radius: .25em;
}
.bx-border-circle
{
padding: .25em;
border: .07em solid rgba(0,0,0,.1);
border-radius: 50%;
}
.bx-ul
{
margin-left: 2em;
padding-left: 0;
list-style: none;
}
.bx-ul > li
{
position: relative;
}
.bx-ul .bx,.bx-ul .bxr,.bx-ul .bxs
{
font-size: inherit;
line-height: inherit;
position: absolute;
left: -2em;
width: 2em;
text-align: center;
}

View File

@@ -1 +0,0 @@
.bx-rotate-90{transform:rotate(90deg);-ms-filter:'progid:DXImageTransform.Microsoft.BasicImage(rotation=1)';}.bx-rotate-180{transform:rotate(180deg);-ms-filter:'progid:DXImageTransform.Microsoft.BasicImage(rotation=2)';}.bx-rotate-270{transform:rotate(270deg);-ms-filter:'progid:DXImageTransform.Microsoft.BasicImage(rotation=3)';}.bx-flip-horizontal{transform:scaleX(-1);-ms-filter:'progid:DXImageTransform.Microsoft.BasicImage(rotation=0, mirror=1)';}.bx-flip-vertical{transform:scaleY(-1);-ms-filter:'progid:DXImageTransform.Microsoft.BasicImage(rotation=2, mirror=1)';}.bx-xs{font-size:1rem!important;}.bx-sm{font-size:1.55rem!important;}.bx-md{font-size:2.25rem!important;}.bx-lg{font-size:3.0rem!important;}.bx-fw{font-size:1.2857142857em;line-height:.8em;width:1.2857142857em;height:.8em;margin-top:-.2em!important;vertical-align:middle;}.bx-pull-left{float:left;margin-right:.3em!important;}.bx-pull-right{float:right;margin-left:.3em!important;}.bx-border{padding:.25em;border:.07em solid rgba(0,0,0,.1);border-radius:.25em;}.bx-border-circle{padding:.25em;border:.07em solid rgba(0,0,0,.1);border-radius:50%;}.bx-ul{margin-left:2em;padding-left:0;list-style:none;}.bx-ul > li{position:relative;}.bx-ul .bx,.bx-ul .bxr,.bx-ul .bxs{font-size:inherit;line-height:inherit;position:absolute;left:-2em;width:2em;text-align:center;}

View File

@@ -1,14 +0,0 @@
{
"name": "@triliumnext/icon-pack-builder",
"version": "1.0.0",
"description": "",
"main": "src/index.ts",
"scripts": {
"start": "tsx ."
},
"keywords": [],
"devDependencies": {
"@mdi/font": "7.4.47",
"@phosphor-icons/web": "2.1.2"
}
}

View File

@@ -1,76 +0,0 @@
import { createWriteStream, mkdirSync } from "node:fs";
import { join } from "node:path";
import cls from "@triliumnext/server/src/services/cls.js";
import type { IconPackData } from "./provider";
import boxicons3 from "./providers/boxicons3";
import mdi from "./providers/mdi";
import phosphor from "./providers/phosphor";
process.env.TRILIUM_INTEGRATION_TEST = "memory-no-store";
process.env.TRILIUM_RESOURCE_DIR = "../server/src";
process.env.NODE_ENV = "development";
async function main() {
const outputDir = join(__dirname, "output");
mkdirSync(outputDir, { recursive: true });
const i18n = await import("@triliumnext/server/src/services/i18n.js");
await i18n.initializeTranslations();
const sqlInit = (await import("../../server/src/services/sql_init.js")).default;
await sqlInit.createInitialDatabase(true);
// Wait for becca to be loaded before importing data
const beccaLoader = await import("../../server/src/becca/becca_loader.js");
await beccaLoader.beccaLoaded;
const notesService = (await import("../../server/src/services/notes.js")).default;
async function buildIconPack(iconPack: IconPackData) {
// Create the icon pack note.
const { note, branch } = notesService.createNewNote({
parentNoteId: "root",
type: "file",
title: iconPack.name,
mime: "application/json",
content: JSON.stringify(iconPack.manifest)
});
note.setLabel("iconPack", iconPack.prefix);
note.setLabel("iconClass", iconPack.icon);
// Add the attachment.
note.saveAttachment({
role: "file",
title: iconPack.fontFile.name,
mime: iconPack.fontFile.mime,
content: iconPack.fontFile.content
});
// Export to zip.
const zipFilePath = join(outputDir, `${iconPack.name}.zip`);
const fileOutputStream = createWriteStream(zipFilePath);
const { exportToZip } = (await import("@triliumnext/server/src/services/export/zip.js")).default;
const taskContext = new (await import("@triliumnext/server/src/services/task_context.js")).default(
"no-progress-reporting", "export", null
);
await exportToZip(taskContext, branch, "html", fileOutputStream, false, { skipExtraFiles: true });
await new Promise<void>((resolve) => { fileOutputStream.on("finish", resolve); });
console.log(`Built icon pack: ${iconPack.name} (${zipFilePath})`);
}
const builtIconPacks = [
boxicons3("basic"),
boxicons3("brands"),
mdi(),
phosphor("regular"),
phosphor("fill")
];
await Promise.all(builtIconPacks.map(buildIconPack));
}
cls.init(() => {
main();
});

View File

@@ -1,13 +0,0 @@
import type { IconPackManifest } from "@triliumnext/server/src/services/icon_packs";
export interface IconPackData {
name: string;
prefix: string;
manifest: IconPackManifest;
icon: string;
fontFile: {
name: string;
mime: string;
content: Buffer;
}
}

View File

@@ -1,42 +0,0 @@
import { readFileSync } from "fs";
import { join } from "path";
import { IconPackData } from "../provider";
export default function buildIcons(pack: "basic" | "brands"): IconPackData {
const inputDir = join(__dirname, "../../boxicons-free/fonts");
const fileName = pack === "basic" ? "boxicons" : `boxicons-${pack}`;
const jsonPath = `${inputDir}/${pack}/${fileName}.json`;
const inputData = JSON.parse(readFileSync(jsonPath, "utf-8"));
const icons = {};
for (const [ key, value ] of Object.entries(inputData)) {
if (key.startsWith("variable-selector")) continue;
let name = key;
if (name.startsWith('bx-')) {
name = name.slice(3);
}
if (name.startsWith('bxs-')) {
name = name.slice(4);
}
icons[key] = {
glyph: String.fromCodePoint(value as number),
terms: [ name ]
};
}
return {
name: pack === "basic" ? "Boxicons 3 (Basic)" : "Boxicons 3 (Brands)",
prefix: pack === "basic" ? "bx3" : "bxl3",
icon: pack === "basic" ? "bx3 bx-cube" : "bxl3 bxl-boxicons",
fontFile: {
name: `${fileName}.woff2`,
mime: "font/woff2",
content: readFileSync(join(inputDir, pack, `${fileName}.woff2`))
},
manifest: {
icons
}
};
}

View File

@@ -1,26 +0,0 @@
import { readFileSync } from "fs";
import { join } from "path";
import type { IconPackData } from "../provider";
import { extractClassNamesFromCss, getModulePath } from "../utils";
export default function buildIcons(): IconPackData {
const baseDir = getModulePath("@mdi/font");
const cssFilePath = join(baseDir, "css", "materialdesignicons.min.css");
const cssFileContent = readFileSync(cssFilePath, "utf-8");
return {
name: "Material Design Icons",
prefix: "mdi",
icon: "mdi mdi-material-design",
manifest: {
icons: extractClassNamesFromCss(cssFileContent, "mdi"),
},
fontFile: {
name: "materialdesignicons-webfont.woff2",
mime: "font/woff2",
content: readFileSync(join(baseDir, "fonts", "materialdesignicons-webfont.woff2"))
}
};
}

View File

@@ -1,46 +0,0 @@
import { readdirSync, readFileSync } from "node:fs";
import { join } from "node:path";
import { IconPackData } from "../provider";
import { getModulePath } from "../utils";
export default function buildIcons(packName: "regular" | "fill"): IconPackData {
const baseDir = join(getModulePath("@phosphor-icons/web"), "src", packName);
const iconIndex = JSON.parse(readFileSync(join(baseDir, "selection.json"), "utf-8"));
const icons: IconPackData["manifest"]["icons"] = {};
function removeSuffix(name: string) {
if (name.endsWith(`-${packName}`)) {
name = name.split("-").slice(0, -1).join("-");
}
return name;
}
for (const icon of iconIndex.icons) {
const terms = icon.properties.name.split(", ").map((t: string) => removeSuffix(t));
const name = removeSuffix(icon.icon.tags[0]);
const id = `ph-${name}`;
icons[id] = {
glyph: `${String.fromCharCode(icon.properties.code)}`,
terms
};
}
const fontFile = readdirSync(baseDir).find(f => f.endsWith(".woff2"));
const prefix = packName === "regular" ? "ph" : `ph-${packName}`;
return {
name: `Phosphor Icons (${packName.charAt(0).toUpperCase() + packName.slice(1)})`,
prefix,
icon: `${prefix} ph-phosphor-logo`,
manifest: {
icons
},
fontFile: {
name: fontFile!,
mime: "font/woff2",
content: readFileSync(join(baseDir, fontFile!))
}
};
}

View File

@@ -1,26 +0,0 @@
import { join } from "path";
import { IconPackManifest } from "../../server/src/services/icon_packs";
export function extractClassNamesFromCss(css: string, prefix: string): IconPackManifest["icons"] {
const regex = /\.([a-zA-Z0-9-]+)::before\s*\{\s*content:\s*"\\([A-Fa-f0-9]+)"\s*\}/g;
const icons: IconPackManifest["icons"] = {};
let match: string[];
while ((match = regex.exec(css)) !== null) {
let name = match[1];
if (prefix && name.startsWith(`${prefix}-`)) {
name = name.substring(prefix.length + 1);
}
icons[match[1]] = {
glyph: String.fromCodePoint(parseInt(match[2], 16)),
terms: [ name ]
};
}
return icons;
}
export function getModulePath(moduleName: string): string {
return join(__dirname, "../../../node_modules", moduleName);
}

View File

@@ -1,36 +0,0 @@
{
"extends": "../../tsconfig.base.json",
"compilerOptions": {
"module": "ESNext",
"moduleResolution": "bundler",
"target": "ES2020",
"outDir": "dist",
"strict": false,
"types": [
"node",
"express"
],
"rootDir": "src",
"tsBuildInfoFile": "dist/tsconfig.app.tsbuildinfo"
},
"include": [
"src/**/*.ts",
"../server/src/*.d.ts"
],
"exclude": [
"eslint.config.js",
"eslint.config.cjs",
"eslint.config.mjs"
],
"references": [
{
"path": "../server/tsconfig.app.json"
},
{
"path": "../desktop/tsconfig.app.json"
},
{
"path": "../client/tsconfig.app.json"
}
]
}

View File

@@ -1,15 +0,0 @@
{
"extends": "../../tsconfig.base.json",
"include": [],
"references": [
{
"path": "../server"
},
{
"path": "../client"
},
{
"path": "./tsconfig.app.json"
}
]
}

View File

@@ -341,7 +341,7 @@ paths:
post:
description: >
Create a branch (clone a note to a different location in the tree).
In case there is a branch between parent note and child note already,
In case there is a branch between parent note and child note already,
then this will update the existing branch with prefix, notePosition and isExpanded.
operationId: postBranch
requestBody:
@@ -416,7 +416,7 @@ paths:
$ref: "#/components/schemas/Error"
delete:
description: >
deletes a branch based on the branchId supplied. If this is the last branch of the (child) note,
deletes a branch based on the branchId supplied. If this is the last branch of the (child) note,
then the note is deleted as well.
operationId: deleteBranchById
responses:
@@ -627,8 +627,8 @@ paths:
$ref: "#/components/schemas/EntityId"
post:
description: >
notePositions in branches are not automatically pushed to connected clients and need a specific instruction.
If you want your changes to be in effect immediately, call this service after setting branches' notePosition.
notePositions in branches are not automatically pushed to connected clients and need a specific instruction.
If you want your changes to be in effect immediately, call this service after setting branches' notePosition.
Note that you need to supply "parentNoteId" of branch(es) with changed positions.
operationId: postRefreshNoteOrdering
responses:
@@ -692,20 +692,18 @@ paths:
application/json; charset=utf-8:
schema:
$ref: "#/components/schemas/Error"
/calendar/weeks/{week}:
/calendar/weeks/{date}:
get:
summary: Get a week note
description: Returns a week note for a given ISO week (format YYYY-Www, e.g., 2025-W01). The note is created if it doesn't exist.
operationId: getWeekNote
description: returns a week note for a given date. Gets created if doesn't exist.
operationId: getWeekFirstDayNote
parameters:
- name: week
- name: date
in: path
required: true
description: The ISO 8601 week identifier (YYYY-Www).
schema:
type: string
pattern: "[0-9]{4}-W[0-9]{2}"
example: "2025-W01"
format: date
example: 2022-02-22
responses:
"200":
description: week note
@@ -861,8 +859,8 @@ components:
type: http
scheme: basic
description: >
Basic Auth where username is arbitrary string (e.g. "trilium", not checked),
username is the ETAPI token.
Basic Auth where username is arbitrary string (e.g. "trilium", not checked),
username is the ETAPI token.
To emphasize, do not use Trilium password here (won't work), only the generated
ETAPI token (from Options -> ETAPI)
schemas:
@@ -899,13 +897,13 @@ components:
notePosition:
type: integer
description: >
Position of the note in the parent. Normal ordering is 10, 20, 30 ...
Position of the note in the parent. Normal ordering is 10, 20, 30 ...
So if you want to create a note on the first position, use e.g. 5, for second position 15, for last e.g. 1000000
prefix:
type: string
description: >
Prefix is branch (placement) specific title prefix for the note.
Let's say you have your note placed into two different places in the tree,
Prefix is branch (placement) specific title prefix for the note.
Let's say you have your note placed into two different places in the tree,
but you want to change the title a bit in one of the placements. For this you can use prefix.
isExpanded:
type: boolean
@@ -932,24 +930,7 @@ components:
type: string
type:
type: string
enum:
[
text,
code,
render,
file,
image,
search,
relationMap,
book,
noteMap,
mermaid,
webView,
shortcut,
doc,
contentWidget,
launcher,
]
enum: [text, code, render, file, image, search, relationMap, book, noteMap, mermaid, webView, shortcut, doc, contentWidget, launcher]
mime:
type: string
isProtected:

View File

@@ -20,21 +20,353 @@ describe("etapi/search", () => {
content = randomUUID();
await createNote(app, token, content);
}, 30000); // Increase timeout to 30 seconds for app initialization
describe("Basic Search", () => {
it("finds by content", async () => {
const response = await supertest(app)
.get(`/etapi/notes?search=${content}&debug=true`)
.auth(USER, token, { "type": "basic"})
.expect(200);
expect(response.body.results).toHaveLength(1);
});
it("does not find by content when fast search is on", async () => {
const response = await supertest(app)
.get(`/etapi/notes?search=${content}&debug=true&fastSearch=true`)
.auth(USER, token, { "type": "basic"})
.expect(200);
expect(response.body.results).toHaveLength(0);
});
it("returns proper response structure", async () => {
const response = await supertest(app)
.get(`/etapi/notes?search=${content}`)
.auth(USER, token, { "type": "basic"})
.expect(200);
expect(response.body).toHaveProperty("results");
expect(Array.isArray(response.body.results)).toBe(true);
if (response.body.results.length > 0) {
const note = response.body.results[0];
expect(note).toHaveProperty("noteId");
expect(note).toHaveProperty("title");
expect(note).toHaveProperty("type");
}
});
it("returns debug info when requested", async () => {
const response = await supertest(app)
.get(`/etapi/notes?search=${content}&debug=true`)
.auth(USER, token, { "type": "basic"})
.expect(200);
expect(response.body).toHaveProperty("debugInfo");
expect(response.body.debugInfo).toBeTruthy();
});
it("returns 400 for missing search parameter", async () => {
await supertest(app)
.get("/etapi/notes")
.auth(USER, token, { "type": "basic"})
.expect(400);
});
it("returns 400 for empty search parameter", async () => {
await supertest(app)
.get("/etapi/notes?search=")
.auth(USER, token, { "type": "basic"})
.expect(400);
});
});
it("finds by content", async () => {
const response = await supertest(app)
.get(`/etapi/notes?search=${content}&debug=true`)
.auth(USER, token, { "type": "basic"})
.expect(200);
expect(response.body.results).toHaveLength(1);
describe("Search Parameters", () => {
let testNoteId: string;
beforeAll(async () => {
// Create a test note with unique content
const uniqueContent = `test-${randomUUID()}`;
testNoteId = await createNote(app, token, uniqueContent);
}, 10000);
it("respects fastSearch parameter", async () => {
// Fast search should not find by content
const fastResponse = await supertest(app)
.get(`/etapi/notes?search=${content}&fastSearch=true`)
.auth(USER, token, { "type": "basic"})
.expect(200);
expect(fastResponse.body.results).toHaveLength(0);
// Regular search should find by content
const regularResponse = await supertest(app)
.get(`/etapi/notes?search=${content}&fastSearch=false`)
.auth(USER, token, { "type": "basic"})
.expect(200);
expect(regularResponse.body.results.length).toBeGreaterThan(0);
});
it("respects includeArchivedNotes parameter", async () => {
// Default should include archived notes
const withArchivedResponse = await supertest(app)
.get(`/etapi/notes?search=*&includeArchivedNotes=true`)
.auth(USER, token, { "type": "basic"})
.expect(200);
const withoutArchivedResponse = await supertest(app)
.get(`/etapi/notes?search=*&includeArchivedNotes=false`)
.auth(USER, token, { "type": "basic"})
.expect(200);
// Note: Actual behavior depends on whether there are archived notes
expect(withArchivedResponse.body.results).toBeDefined();
expect(withoutArchivedResponse.body.results).toBeDefined();
});
it("respects limit parameter", async () => {
const limit = 5;
const response = await supertest(app)
.get(`/etapi/notes?search=*&limit=${limit}`)
.auth(USER, token, { "type": "basic"})
.expect(200);
expect(response.body.results.length).toBeLessThanOrEqual(limit);
});
it("handles fuzzyAttributeSearch parameter", async () => {
const response = await supertest(app)
.get(`/etapi/notes?search=*&fuzzyAttributeSearch=true`)
.auth(USER, token, { "type": "basic"})
.expect(200);
expect(response.body.results).toBeDefined();
});
});
it("does not find by content when fast search is on", async () => {
const response = await supertest(app)
.get(`/etapi/notes?search=${content}&debug=true&fastSearch=true`)
.auth(USER, token, { "type": "basic"})
.expect(200);
expect(response.body.results).toHaveLength(0);
describe("Search Queries", () => {
let titleNoteId: string;
let labelNoteId: string;
beforeAll(async () => {
// Create test notes with specific attributes
const uniqueTitle = `SearchTest-${randomUUID()}`;
// Create note with specific title
const titleResponse = await supertest(app)
.post("/etapi/create-note")
.auth(USER, token, { "type": "basic"})
.send({
"parentNoteId": "root",
"title": uniqueTitle,
"type": "text",
"content": "Title test content"
})
.expect(201);
titleNoteId = titleResponse.body.note.noteId;
// Create note with label
const labelResponse = await supertest(app)
.post("/etapi/create-note")
.auth(USER, token, { "type": "basic"})
.send({
"parentNoteId": "root",
"title": "Label Test",
"type": "text",
"content": "Label test content"
})
.expect(201);
labelNoteId = labelResponse.body.note.noteId;
// Add label to note
await supertest(app)
.post("/etapi/attributes")
.auth(USER, token, { "type": "basic"})
.send({
"noteId": labelNoteId,
"type": "label",
"name": "testlabel",
"value": "testvalue"
})
.expect(201);
}, 15000); // 15 second timeout for setup
it("searches by title", async () => {
// Get the title we created
const noteResponse = await supertest(app)
.get(`/etapi/notes/${titleNoteId}`)
.auth(USER, token, { "type": "basic"})
.expect(200);
const title = noteResponse.body.title;
const searchResponse = await supertest(app)
.get(`/etapi/notes?search=${encodeURIComponent(title)}`)
.auth(USER, token, { "type": "basic"})
.expect(200);
expect(searchResponse.body.results.length).toBeGreaterThan(0);
const foundNote = searchResponse.body.results.find((n: any) => n.noteId === titleNoteId);
expect(foundNote).toBeTruthy();
});
it("searches by label", async () => {
const searchResponse = await supertest(app)
.get(`/etapi/notes?search=${encodeURIComponent("#testlabel")}`)
.auth(USER, token, { "type": "basic"})
.expect(200);
expect(searchResponse.body.results.length).toBeGreaterThan(0);
const foundNote = searchResponse.body.results.find((n: any) => n.noteId === labelNoteId);
expect(foundNote).toBeTruthy();
});
it("searches by label with value", async () => {
const searchResponse = await supertest(app)
.get(`/etapi/notes?search=${encodeURIComponent("#testlabel=testvalue")}`)
.auth(USER, token, { "type": "basic"})
.expect(200);
expect(searchResponse.body.results.length).toBeGreaterThan(0);
const foundNote = searchResponse.body.results.find((n: any) => n.noteId === labelNoteId);
expect(foundNote).toBeTruthy();
});
it("handles complex queries with AND operator", async () => {
const searchResponse = await supertest(app)
.get(`/etapi/notes?search=${encodeURIComponent("#testlabel AND note.type=text")}`)
.auth(USER, token, { "type": "basic"})
.expect(200);
expect(searchResponse.body.results).toBeDefined();
});
it("handles queries with OR operator", async () => {
const searchResponse = await supertest(app)
.get(`/etapi/notes?search=${encodeURIComponent("#testlabel OR #nonexistent")}`)
.auth(USER, token, { "type": "basic"})
.expect(200);
expect(searchResponse.body.results.length).toBeGreaterThan(0);
});
it("handles queries with NOT operator", async () => {
const searchResponse = await supertest(app)
.get(`/etapi/notes?search=${encodeURIComponent("#testlabel NOT #nonexistent")}`)
.auth(USER, token, { "type": "basic"})
.expect(200);
expect(searchResponse.body.results.length).toBeGreaterThan(0);
});
it("handles wildcard searches", async () => {
const searchResponse = await supertest(app)
.get(`/etapi/notes?search=note.type%3Dtext&limit=10`)
.auth(USER, token, { "type": "basic"})
.expect(200);
expect(searchResponse.body.results).toBeDefined();
// Should return results if any text notes exist
expect(Array.isArray(searchResponse.body.results)).toBe(true);
});
it("handles empty results gracefully", async () => {
const nonexistentQuery = `nonexistent-${randomUUID()}`;
const searchResponse = await supertest(app)
.get(`/etapi/notes?search=${encodeURIComponent(nonexistentQuery)}`)
.auth(USER, token, { "type": "basic"})
.expect(200);
expect(searchResponse.body.results).toHaveLength(0);
});
});
describe("Error Handling", () => {
it("handles invalid query syntax gracefully", async () => {
const response = await supertest(app)
.get(`/etapi/notes?search=${encodeURIComponent("(((")}`)
.auth(USER, token, { "type": "basic"})
.expect(200);
// Should return empty results or handle error gracefully
expect(response.body.results).toBeDefined();
});
it("requires authentication", async () => {
await supertest(app)
.get(`/etapi/notes?search=test`)
.expect(401);
});
it("rejects invalid authentication", async () => {
await supertest(app)
.get(`/etapi/notes?search=test`)
.auth(USER, "invalid-token", { "type": "basic"})
.expect(401);
});
});
describe("Performance", () => {
it("handles large result sets", async () => {
const startTime = Date.now();
const response = await supertest(app)
.get(`/etapi/notes?search=*&limit=100`)
.auth(USER, token, { "type": "basic"})
.expect(200);
const endTime = Date.now();
const duration = endTime - startTime;
expect(response.body.results).toBeDefined();
// Search should complete in reasonable time (5 seconds)
expect(duration).toBeLessThan(5000);
});
it("handles queries efficiently", async () => {
const startTime = Date.now();
await supertest(app)
.get(`/etapi/notes?search=${encodeURIComponent("#*")}`)
.auth(USER, token, { "type": "basic"})
.expect(200);
const endTime = Date.now();
const duration = endTime - startTime;
// Attribute search should be fast
expect(duration).toBeLessThan(3000);
});
});
describe("Special Characters", () => {
it("handles special characters in search", async () => {
const specialChars = "test@#$%";
const response = await supertest(app)
.get(`/etapi/notes?search=${encodeURIComponent(specialChars)}`)
.auth(USER, token, { "type": "basic"})
.expect(200);
expect(response.body.results).toBeDefined();
});
it("handles unicode characters", async () => {
const unicode = "测试";
const response = await supertest(app)
.get(`/etapi/notes?search=${encodeURIComponent(unicode)}`)
.auth(USER, token, { "type": "basic"})
.expect(200);
expect(response.body.results).toBeDefined();
});
it("handles quotes in search", async () => {
const quoted = '"test phrase"';
const response = await supertest(app)
.get(`/etapi/notes?search=${encodeURIComponent(quoted)}`)
.auth(USER, token, { "type": "basic"})
.expect(200);
expect(response.body.results).toBeDefined();
});
});
});

View File

@@ -146,9 +146,289 @@ CREATE INDEX IDX_notes_blobId on notes (blobId);
CREATE INDEX IDX_revisions_blobId on revisions (blobId);
CREATE INDEX IDX_attachments_blobId on attachments (blobId);
-- Strategic Performance Indexes from migration 234
-- NOTES TABLE INDEXES
CREATE INDEX IDX_notes_search_composite
ON notes (isDeleted, type, mime, dateModified DESC);
CREATE INDEX IDX_notes_metadata_covering
ON notes (noteId, isDeleted, type, mime, title, dateModified, isProtected);
CREATE INDEX IDX_notes_protected_deleted
ON notes (isProtected, isDeleted)
WHERE isProtected = 1;
-- BRANCHES TABLE INDEXES
CREATE INDEX IDX_branches_tree_traversal
ON branches (parentNoteId, isDeleted, notePosition);
CREATE INDEX IDX_branches_covering
ON branches (noteId, parentNoteId, isDeleted, notePosition, prefix);
CREATE INDEX IDX_branches_note_parents
ON branches (noteId, isDeleted)
WHERE isDeleted = 0;
-- ATTRIBUTES TABLE INDEXES
CREATE INDEX IDX_attributes_search_composite
ON attributes (name, value, isDeleted);
CREATE INDEX IDX_attributes_covering
ON attributes (noteId, name, value, type, isDeleted, position);
CREATE INDEX IDX_attributes_inheritable
ON attributes (isInheritable, isDeleted)
WHERE isInheritable = 1 AND isDeleted = 0;
CREATE INDEX IDX_attributes_labels
ON attributes (type, name, value)
WHERE type = 'label' AND isDeleted = 0;
CREATE INDEX IDX_attributes_relations
ON attributes (type, name, value)
WHERE type = 'relation' AND isDeleted = 0;
-- BLOBS TABLE INDEXES
CREATE INDEX IDX_blobs_content_size
ON blobs (blobId, LENGTH(content));
-- ATTACHMENTS TABLE INDEXES
CREATE INDEX IDX_attachments_composite
ON attachments (ownerId, role, isDeleted, position);
-- REVISIONS TABLE INDEXES
CREATE INDEX IDX_revisions_note_date
ON revisions (noteId, utcDateCreated DESC);
-- ENTITY_CHANGES TABLE INDEXES
CREATE INDEX IDX_entity_changes_sync
ON entity_changes (isSynced, utcDateChanged);
-- RECENT_NOTES TABLE INDEXES
CREATE INDEX IDX_recent_notes_date
ON recent_notes (utcDateCreated DESC);
CREATE TABLE IF NOT EXISTS sessions (
id TEXT PRIMARY KEY,
data TEXT,
expires INTEGER
);
-- FTS5 Full-Text Search Support
-- Create FTS5 virtual table with trigram tokenizer
-- Trigram tokenizer provides language-agnostic substring matching:
-- 1. Fast substring matching (50-100x speedup for LIKE queries without wildcards)
-- 2. Case-insensitive search without custom collation
-- 3. No language-specific stemming assumptions (works for all languages)
-- 4. Boolean operators (AND, OR, NOT) and phrase matching with quotes
--
-- IMPORTANT: Trigram requires minimum 3-character tokens for matching
-- detail='full' enables phrase queries (required for exact match with = operator)
-- and provides position info for highlight() function
-- Note: Using detail='full' instead of detail='none' increases index size by ~50%
-- but is necessary to support phrase queries like "exact phrase"
CREATE VIRTUAL TABLE notes_fts USING fts5(
noteId UNINDEXED,
title,
content,
tokenize = 'trigram',
detail = 'full'
);
-- Triggers to keep FTS table synchronized with notes
-- IMPORTANT: These triggers must handle all SQL operations including:
-- - Regular INSERT/UPDATE/DELETE
-- - INSERT OR REPLACE
-- - INSERT ... ON CONFLICT ... DO UPDATE (upsert)
-- - Cases where notes are created before blobs (import scenarios)
-- Trigger for INSERT operations on notes
-- Handles: INSERT, INSERT OR REPLACE, INSERT OR IGNORE, and the INSERT part of upsert
CREATE TRIGGER notes_fts_insert
AFTER INSERT ON notes
WHEN NEW.type IN ('text', 'code', 'mermaid', 'canvas', 'mindMap')
AND NEW.isDeleted = 0
AND NEW.isProtected = 0
BEGIN
-- First delete any existing FTS entry (in case of INSERT OR REPLACE)
DELETE FROM notes_fts WHERE noteId = NEW.noteId;
-- Then insert the new entry, using LEFT JOIN to handle missing blobs
INSERT INTO notes_fts (noteId, title, content)
SELECT
NEW.noteId,
NEW.title,
COALESCE(b.content, '') -- Use empty string if blob doesn't exist yet
FROM (SELECT NEW.noteId) AS note_select
LEFT JOIN blobs b ON b.blobId = NEW.blobId;
END;
-- Trigger for UPDATE operations on notes table
-- Handles: Regular UPDATE and the UPDATE part of upsert (ON CONFLICT DO UPDATE)
-- Fires for ANY update to searchable notes to ensure FTS stays in sync
CREATE TRIGGER notes_fts_update
AFTER UPDATE ON notes
WHEN NEW.type IN ('text', 'code', 'mermaid', 'canvas', 'mindMap')
-- Fire on any change, not just specific columns, to handle all upsert scenarios
BEGIN
-- Always delete the old entry
DELETE FROM notes_fts WHERE noteId = NEW.noteId;
-- Insert new entry if note is not deleted and not protected
INSERT INTO notes_fts (noteId, title, content)
SELECT
NEW.noteId,
NEW.title,
COALESCE(b.content, '') -- Use empty string if blob doesn't exist yet
FROM (SELECT NEW.noteId) AS note_select
LEFT JOIN blobs b ON b.blobId = NEW.blobId
WHERE NEW.isDeleted = 0
AND NEW.isProtected = 0;
END;
-- Trigger for UPDATE operations on blobs
-- Handles: Regular UPDATE and the UPDATE part of upsert (ON CONFLICT DO UPDATE)
-- IMPORTANT: Uses INSERT OR REPLACE for efficiency with deduplicated blobs
CREATE TRIGGER notes_fts_blob_update
AFTER UPDATE ON blobs
BEGIN
-- Use INSERT OR REPLACE for atomic update of all notes sharing this blob
-- This is more efficient than DELETE + INSERT when many notes share the same blob
INSERT OR REPLACE INTO notes_fts (noteId, title, content)
SELECT
n.noteId,
n.title,
NEW.content
FROM notes n
WHERE n.blobId = NEW.blobId
AND n.type IN ('text', 'code', 'mermaid', 'canvas', 'mindMap')
AND n.isDeleted = 0
AND n.isProtected = 0;
END;
-- Trigger for DELETE operations
CREATE TRIGGER notes_fts_delete
AFTER DELETE ON notes
BEGIN
DELETE FROM notes_fts WHERE noteId = OLD.noteId;
END;
-- Trigger for soft delete (isDeleted = 1)
CREATE TRIGGER notes_fts_soft_delete
AFTER UPDATE ON notes
WHEN OLD.isDeleted = 0 AND NEW.isDeleted = 1
BEGIN
DELETE FROM notes_fts WHERE noteId = NEW.noteId;
END;
-- Trigger for notes becoming protected
-- Remove from FTS when a note becomes protected
CREATE TRIGGER notes_fts_protect
AFTER UPDATE ON notes
WHEN OLD.isProtected = 0 AND NEW.isProtected = 1
BEGIN
DELETE FROM notes_fts WHERE noteId = NEW.noteId;
END;
-- Trigger for notes becoming unprotected
-- Add to FTS when a note becomes unprotected (if eligible)
CREATE TRIGGER notes_fts_unprotect
AFTER UPDATE ON notes
WHEN OLD.isProtected = 1 AND NEW.isProtected = 0
AND NEW.type IN ('text', 'code', 'mermaid', 'canvas', 'mindMap')
AND NEW.isDeleted = 0
BEGIN
DELETE FROM notes_fts WHERE noteId = NEW.noteId;
INSERT INTO notes_fts (noteId, title, content)
SELECT
NEW.noteId,
NEW.title,
COALESCE(b.content, '')
FROM (SELECT NEW.noteId) AS note_select
LEFT JOIN blobs b ON b.blobId = NEW.blobId;
END;
-- Trigger for INSERT operations on blobs
-- Handles: INSERT, INSERT OR REPLACE, and the INSERT part of upsert
-- Updates all notes that reference this blob (common during import and deduplication)
CREATE TRIGGER notes_fts_blob_insert
AFTER INSERT ON blobs
BEGIN
-- Use INSERT OR REPLACE to handle both new and existing FTS entries
-- This is crucial for blob deduplication where multiple notes may already
-- exist that reference this blob before the blob itself is created
INSERT OR REPLACE INTO notes_fts (noteId, title, content)
SELECT
n.noteId,
n.title,
NEW.content
FROM notes n
WHERE n.blobId = NEW.blobId
AND n.type IN ('text', 'code', 'mermaid', 'canvas', 'mindMap')
AND n.isDeleted = 0
AND n.isProtected = 0;
END;
-- =====================================================
-- FTS5 Full-Text Search Index for Attributes
-- =====================================================
-- This FTS5 table enables fast full-text searching of attribute names and values
-- Benefits:
-- - Fast free-text searches like ="somevalue" (10-50ms vs 1-2 seconds)
-- - Scales well with large attribute counts (650K+ attributes)
-- - Consistent performance with notes_fts
--
-- Uses trigram tokenizer with detail='full' for:
-- 1. Substring matching (3+ characters)
-- 2. Phrase query support (exact matches with word boundaries)
-- 3. Multi-language support without stemming assumptions
CREATE VIRTUAL TABLE attributes_fts USING fts5(
attributeId UNINDEXED,
noteId UNINDEXED,
name,
value,
tokenize = 'trigram',
detail = 'full'
);
-- Triggers to keep attributes_fts synchronized with attributes table
-- Trigger for INSERT operations
CREATE TRIGGER attributes_fts_insert
AFTER INSERT ON attributes
WHEN NEW.isDeleted = 0
BEGIN
INSERT INTO attributes_fts (attributeId, noteId, name, value)
VALUES (NEW.attributeId, NEW.noteId, NEW.name, COALESCE(NEW.value, ''));
END;
-- Trigger for UPDATE operations
CREATE TRIGGER attributes_fts_update
AFTER UPDATE ON attributes
BEGIN
-- Remove old entry
DELETE FROM attributes_fts WHERE attributeId = OLD.attributeId;
-- Add new entry if not deleted
INSERT INTO attributes_fts (attributeId, noteId, name, value)
SELECT NEW.attributeId, NEW.noteId, NEW.name, COALESCE(NEW.value, '')
WHERE NEW.isDeleted = 0;
END;
-- Trigger for DELETE operations
CREATE TRIGGER attributes_fts_delete
AFTER DELETE ON attributes
BEGIN
DELETE FROM attributes_fts WHERE attributeId = OLD.attributeId;
END;
-- Trigger for soft delete (isDeleted = 1)
CREATE TRIGGER attributes_fts_soft_delete
AFTER UPDATE ON attributes
WHEN OLD.isDeleted = 0 AND NEW.isDeleted = 1
BEGIN
DELETE FROM attributes_fts WHERE attributeId = NEW.attributeId;
END;

File diff suppressed because one or more lines are too long

Some files were not shown because too many files have changed in this diff Show More