diff --git a/README.md b/README.md index 30f4dc124..bb6e45836 100644 --- a/README.md +++ b/README.md @@ -16,7 +16,7 @@ See [screenshots](https://triliumnext.github.io/Docs/Wiki/screenshot-tour) for q ### Migrating from Trilium? -There are no special migration steps to migrate from a zadam/Trilium instance to a TriliumNext/Notes instance. Just upgrade your Trilium instance to the latest version and [install TriliumNext/Notes as usual](#-installation) +There are no special migration steps to migrate from a zadam/Trilium instance to a TriliumNext/Notes instance. Simply [install TriliumNext/Notes](#-installation) as usual and it will use your existing database. Versions up to and including [v0.90.4](https://github.com/TriliumNext/Notes/releases/tag/v0.90.4) are compatible with the latest zadam/trilium version of [v0.63.7](https://github.com/zadam/trilium/releases/tag/v0.63.7). Any later versions of TriliumNext have their sync versions incremented. @@ -66,17 +66,7 @@ To use TriliumNext on your desktop machine (Linux, MacOS, and Windows) you have * Download the binary release for your platform from the [latest release page](https://github.com/TriliumNext/Notes/releases/latest), unzip the package and run the ```trilium``` executable. * Access TriliumNext via the web interface of a server installation (see below) * Currently only the latest versions of Chrome & Firefox are supported (and tested). -* (Coming Soon) TriliumNext will also be provided as a Flatpak - -#### MacOS -Currently when running TriliumNext/Notes on MacOS, you may get the following error: -> Apple could not verify "TriliumNext Notes" is free of malware and may harm your Mac or compromise your privacy. - -You will need to run the command on your shell to resolve the error (documented [here](https://github.com/TriliumNext/Notes/issues/329#issuecomment-2287164137)): - -```bash -xattr -c "/path/to/Trilium Next.app" -``` +* TriliumNext is also provided as a Flatpak, but not yet published on FlatHub. ### Mobile diff --git a/docs/Release Notes/!!!meta.json b/docs/Release Notes/!!!meta.json index c9e9b98a2..9a5c041f1 100644 --- a/docs/Release Notes/!!!meta.json +++ b/docs/Release Notes/!!!meta.json @@ -61,7 +61,7 @@ "hD3V4hiu2VW4", "VN3xnce1vLkX" ], - "title": "v0.92.8-beta", + "title": "v0.93.0", "notePosition": 10, "prefix": null, "isExpanded": false, @@ -69,7 +69,7 @@ "mime": "text/html", "attributes": [], "format": "markdown", - "dataFileName": "v0.92.8-beta.md", + "dataFileName": "v0.93.0.md", "attachments": [] }, { diff --git a/docs/Release Notes/Release Notes/v0.92.8-beta.md b/docs/Release Notes/Release Notes/v0.93.0.md similarity index 75% rename from docs/Release Notes/Release Notes/v0.92.8-beta.md rename to docs/Release Notes/Release Notes/v0.93.0.md index 58b5e6188..fd13d57b4 100644 --- a/docs/Release Notes/Release Notes/v0.92.8-beta.md +++ b/docs/Release Notes/Release Notes/v0.93.0.md @@ -1,4 +1,4 @@ -# v0.92.8-beta +# v0.93.0 ## 💡 Key highlights * … @@ -11,6 +11,10 @@ * Note tree not closing when selecting some of the menu actions. * [Most tree context menu on mobile are broken](https://github.com/TriliumNext/Notes/issues/671) * [Quick search launch bar item does nothing in vertical layout](https://github.com/TriliumNext/Notes/issues/1680) +* [Note background is gray in 0.92.7 (light theme)](https://github.com/TriliumNext/Notes/issues/1689) +* [config.Session.cookieMaxAge is ignored](https://github.com/TriliumNext/Notes/issues/1709) by @pano9000 +* [Return correct HTTP status code on failed login attempts instead of 200](https://github.com/TriliumNext/Notes/issues/1707) by @pano9000 +* [Calendar stops displaying notes after adding a Day Note](https://github.com/TriliumNext/Notes/issues/1705) ## ✨ Improvements @@ -29,6 +33,8 @@ * [Center Search results under quick search bar](https://github.com/TriliumNext/Notes/issues/1679) * Native ARM builds for Windows are now back. * Basic Touch Bar support for macOS. +* [Support Bearer Token](https://github.com/TriliumNext/Notes/issues/1701) +* The tab bar is now scrollable when there are many tabs by @SiriusXT ## 🌍 Internationalization diff --git a/docs/User Guide/!!!meta.json b/docs/User Guide/!!!meta.json index b548e1846..cd9f21f61 100644 --- a/docs/User Guide/!!!meta.json +++ b/docs/User Guide/!!!meta.json @@ -9636,6 +9636,13 @@ "isInheritable": false, "position": 10 }, + { + "type": "relation", + "name": "internalLink", + "value": "habiZ3HU8Kw8", + "isInheritable": false, + "position": 20 + }, { "type": "relation", "name": "internalLink", @@ -9649,13 +9656,6 @@ "value": "default-note-title", "isInheritable": false, "position": 30 - }, - { - "type": "relation", - "name": "internalLink", - "value": "habiZ3HU8Kw8", - "isInheritable": false, - "position": 20 } ], "format": "markdown", @@ -10014,6 +10014,13 @@ "isInheritable": false, "position": 40 }, + { + "type": "relation", + "name": "internalLink", + "value": "habiZ3HU8Kw8", + "isInheritable": false, + "position": 50 + }, { "type": "relation", "name": "internalLink", @@ -10027,13 +10034,6 @@ "value": "bx bx-list-plus", "isInheritable": false, "position": 10 - }, - { - "type": "relation", - "name": "internalLink", - "value": "habiZ3HU8Kw8", - "isInheritable": false, - "position": 50 } ], "format": "markdown", @@ -11066,32 +11066,32 @@ "mime": "text/markdown", "attributes": [ { - "type": "label", - "name": "shareAlias", - "value": "script-api", + "type": "relation", + "name": "internalLink", + "value": "CdNpE2pqjmI6", "isInheritable": false, "position": 10 }, { "type": "relation", "name": "internalLink", - "value": "CdNpE2pqjmI6", + "value": "Q2z6av6JZVWm", "isInheritable": false, "position": 20 }, { "type": "relation", "name": "internalLink", - "value": "Q2z6av6JZVWm", + "value": "MEtfsqa5VwNi", "isInheritable": false, "position": 30 }, { - "type": "relation", - "name": "internalLink", - "value": "MEtfsqa5VwNi", + "type": "label", + "name": "shareAlias", + "value": "script-api", "isInheritable": false, - "position": 40 + "position": 10 } ], "format": "markdown", diff --git a/docs/User Guide/User Guide/Advanced Usage/ETAPI (REST API).md b/docs/User Guide/User Guide/Advanced Usage/ETAPI (REST API).md index b9c4db4f3..c24200049 100644 --- a/docs/User Guide/User Guide/Advanced Usage/ETAPI (REST API).md +++ b/docs/User Guide/User Guide/Advanced Usage/ETAPI (REST API).md @@ -9,16 +9,26 @@ As an alternative to calling the API directly, there are client libraries to sim * [trilium-py](https://github.com/Nriver/trilium-py), you can use Python to communicate with Trilium. +## Obtaining a token + +All operations with the REST API have to be authenticated using a token. You can get this token either from Options -> ETAPI or programmatically using the `/auth/login` REST call (see the [spec](https://github.com/TriliumNext/Notes/blob/master/src/etapi/etapi.openapi.yaml)). + ## Authentication -All operations have to be authenticated using a token. You can get this token either from Options -> ETAPI or programmatically using the `/auth/login` REST call (see the [spec](https://github.com/TriliumNext/Notes/blob/master/src/etapi/etapi.openapi.yaml)): +### Via the `Authorization` header ``` GET https://myserver.com/etapi/app-info Authorization: ETAPITOKEN ``` -Alternatively, since 0.56 you can also use basic auth format: +where `ETAPITOKEN` is the token obtained in the previous step. + +For compatibility with various tools, it's also possible to specify the value of the `Authorization` header in the format `Bearer ETAPITOKEN` (since 0.93.0). + +### Basic authentication + +Since v0.56 you can also use basic auth format: ``` GET https://myserver.com/etapi/app-info diff --git a/package-lock.json b/package-lock.json index 73632549e..a7f5e8fa1 100644 --- a/package-lock.json +++ b/package-lock.json @@ -197,7 +197,7 @@ "typedoc": "0.28.2", "typedoc-plugin-missing-exports": "4.0.0", "typescript": "5.8.3", - "typescript-eslint": "8.29.1", + "typescript-eslint": "8.30.1", "vanilla-js-wheel-zoom": "9.0.4", "vitest": "3.1.1", "webpack": "5.99.5", @@ -5571,17 +5571,17 @@ } }, "node_modules/@typescript-eslint/eslint-plugin": { - "version": "8.29.1", - "resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-8.29.1.tgz", - "integrity": "sha512-ba0rr4Wfvg23vERs3eB+P3lfj2E+2g3lhWcCVukUuhtcdUx5lSIFZlGFEBHKr+3zizDa/TvZTptdNHVZWAkSBg==", + "version": "8.30.1", + "resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-8.30.1.tgz", + "integrity": "sha512-v+VWphxMjn+1t48/jO4t950D6KR8JaJuNXzi33Ve6P8sEmPr5k6CEXjdGwT6+LodVnEa91EQCtwjWNUCPweo+Q==", "dev": true, "license": "MIT", "dependencies": { "@eslint-community/regexpp": "^4.10.0", - "@typescript-eslint/scope-manager": "8.29.1", - "@typescript-eslint/type-utils": "8.29.1", - "@typescript-eslint/utils": "8.29.1", - "@typescript-eslint/visitor-keys": "8.29.1", + "@typescript-eslint/scope-manager": "8.30.1", + "@typescript-eslint/type-utils": "8.30.1", + "@typescript-eslint/utils": "8.30.1", + "@typescript-eslint/visitor-keys": "8.30.1", "graphemer": "^1.4.0", "ignore": "^5.3.1", "natural-compare": "^1.4.0", @@ -5601,16 +5601,16 @@ } }, "node_modules/@typescript-eslint/parser": { - "version": "8.29.1", - "resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-8.29.1.tgz", - "integrity": "sha512-zczrHVEqEaTwh12gWBIJWj8nx+ayDcCJs06yoNMY0kwjMWDM6+kppljY+BxWI06d2Ja+h4+WdufDcwMnnMEWmg==", + "version": "8.30.1", + "resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-8.30.1.tgz", + "integrity": "sha512-H+vqmWwT5xoNrXqWs/fesmssOW70gxFlgcMlYcBaWNPIEWDgLa4W9nkSPmhuOgLnXq9QYgkZ31fhDyLhleCsAg==", "dev": true, "license": "MIT", "dependencies": { - "@typescript-eslint/scope-manager": "8.29.1", - "@typescript-eslint/types": "8.29.1", - "@typescript-eslint/typescript-estree": "8.29.1", - "@typescript-eslint/visitor-keys": "8.29.1", + "@typescript-eslint/scope-manager": "8.30.1", + "@typescript-eslint/types": "8.30.1", + "@typescript-eslint/typescript-estree": "8.30.1", + "@typescript-eslint/visitor-keys": "8.30.1", "debug": "^4.3.4" }, "engines": { @@ -5626,14 +5626,14 @@ } }, "node_modules/@typescript-eslint/scope-manager": { - "version": "8.29.1", - "resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-8.29.1.tgz", - "integrity": "sha512-2nggXGX5F3YrsGN08pw4XpMLO1Rgtnn4AzTegC2MDesv6q3QaTU5yU7IbS1tf1IwCR0Hv/1EFygLn9ms6LIpDA==", + "version": "8.30.1", + "resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-8.30.1.tgz", + "integrity": "sha512-+C0B6ChFXZkuaNDl73FJxRYT0G7ufVPOSQkqkpM/U198wUwUFOtgo1k/QzFh1KjpBitaK7R1tgjVz6o9HmsRPg==", "dev": true, "license": "MIT", "dependencies": { - "@typescript-eslint/types": "8.29.1", - "@typescript-eslint/visitor-keys": "8.29.1" + "@typescript-eslint/types": "8.30.1", + "@typescript-eslint/visitor-keys": "8.30.1" }, "engines": { "node": "^18.18.0 || ^20.9.0 || >=21.1.0" @@ -5644,14 +5644,14 @@ } }, "node_modules/@typescript-eslint/type-utils": { - "version": "8.29.1", - "resolved": "https://registry.npmjs.org/@typescript-eslint/type-utils/-/type-utils-8.29.1.tgz", - "integrity": "sha512-DkDUSDwZVCYN71xA4wzySqqcZsHKic53A4BLqmrWFFpOpNSoxX233lwGu/2135ymTCR04PoKiEEEvN1gFYg4Tw==", + "version": "8.30.1", + "resolved": "https://registry.npmjs.org/@typescript-eslint/type-utils/-/type-utils-8.30.1.tgz", + "integrity": "sha512-64uBF76bfQiJyHgZISC7vcNz3adqQKIccVoKubyQcOnNcdJBvYOILV1v22Qhsw3tw3VQu5ll8ND6hycgAR5fEA==", "dev": true, "license": "MIT", "dependencies": { - "@typescript-eslint/typescript-estree": "8.29.1", - "@typescript-eslint/utils": "8.29.1", + "@typescript-eslint/typescript-estree": "8.30.1", + "@typescript-eslint/utils": "8.30.1", "debug": "^4.3.4", "ts-api-utils": "^2.0.1" }, @@ -5668,9 +5668,9 @@ } }, "node_modules/@typescript-eslint/types": { - "version": "8.29.1", - "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-8.29.1.tgz", - "integrity": "sha512-VT7T1PuJF1hpYC3AGm2rCgJBjHL3nc+A/bhOp9sGMKfi5v0WufsX/sHCFBfNTx2F+zA6qBc/PD0/kLRLjdt8mQ==", + "version": "8.30.1", + "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-8.30.1.tgz", + "integrity": "sha512-81KawPfkuulyWo5QdyG/LOKbspyyiW+p4vpn4bYO7DM/hZImlVnFwrpCTnmNMOt8CvLRr5ojI9nU1Ekpw4RcEw==", "dev": true, "license": "MIT", "engines": { @@ -5682,14 +5682,14 @@ } }, "node_modules/@typescript-eslint/typescript-estree": { - "version": "8.29.1", - "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-8.29.1.tgz", - "integrity": "sha512-l1enRoSaUkQxOQnbi0KPUtqeZkSiFlqrx9/3ns2rEDhGKfTa+88RmXqedC1zmVTOWrLc2e6DEJrTA51C9iLH5g==", + "version": "8.30.1", + "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-8.30.1.tgz", + "integrity": "sha512-kQQnxymiUy9tTb1F2uep9W6aBiYODgq5EMSk6Nxh4Z+BDUoYUSa029ISs5zTzKBFnexQEh71KqwjKnRz58lusQ==", "dev": true, "license": "MIT", "dependencies": { - "@typescript-eslint/types": "8.29.1", - "@typescript-eslint/visitor-keys": "8.29.1", + "@typescript-eslint/types": "8.30.1", + "@typescript-eslint/visitor-keys": "8.30.1", "debug": "^4.3.4", "fast-glob": "^3.3.2", "is-glob": "^4.0.3", @@ -5735,16 +5735,16 @@ } }, "node_modules/@typescript-eslint/utils": { - "version": "8.29.1", - "resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-8.29.1.tgz", - "integrity": "sha512-QAkFEbytSaB8wnmB+DflhUPz6CLbFWE2SnSCrRMEa+KnXIzDYbpsn++1HGvnfAsUY44doDXmvRkO5shlM/3UfA==", + "version": "8.30.1", + "resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-8.30.1.tgz", + "integrity": "sha512-T/8q4R9En2tcEsWPQgB5BQ0XJVOtfARcUvOa8yJP3fh9M/mXraLxZrkCfGb6ChrO/V3W+Xbd04RacUEqk1CFEQ==", "dev": true, "license": "MIT", "dependencies": { "@eslint-community/eslint-utils": "^4.4.0", - "@typescript-eslint/scope-manager": "8.29.1", - "@typescript-eslint/types": "8.29.1", - "@typescript-eslint/typescript-estree": "8.29.1" + "@typescript-eslint/scope-manager": "8.30.1", + "@typescript-eslint/types": "8.30.1", + "@typescript-eslint/typescript-estree": "8.30.1" }, "engines": { "node": "^18.18.0 || ^20.9.0 || >=21.1.0" @@ -5759,13 +5759,13 @@ } }, "node_modules/@typescript-eslint/visitor-keys": { - "version": "8.29.1", - "resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-8.29.1.tgz", - "integrity": "sha512-RGLh5CRaUEf02viP5c1Vh1cMGffQscyHe7HPAzGpfmfflFg1wUz2rYxd+OZqwpeypYvZ8UxSxuIpF++fmOzEcg==", + "version": "8.30.1", + "resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-8.30.1.tgz", + "integrity": "sha512-aEhgas7aJ6vZnNFC7K4/vMGDGyOiqWcYZPpIWrTKuTAlsvDNKy2GFDqh9smL+iq069ZvR0YzEeq0B8NJlLzjFA==", "dev": true, "license": "MIT", "dependencies": { - "@typescript-eslint/types": "8.29.1", + "@typescript-eslint/types": "8.30.1", "eslint-visitor-keys": "^4.2.0" }, "engines": { @@ -20458,15 +20458,15 @@ } }, "node_modules/typescript-eslint": { - "version": "8.29.1", - "resolved": "https://registry.npmjs.org/typescript-eslint/-/typescript-eslint-8.29.1.tgz", - "integrity": "sha512-f8cDkvndhbQMPcysk6CUSGBWV+g1utqdn71P5YKwMumVMOG/5k7cHq0KyG4O52nB0oKS4aN2Tp5+wB4APJGC+w==", + "version": "8.30.1", + "resolved": "https://registry.npmjs.org/typescript-eslint/-/typescript-eslint-8.30.1.tgz", + "integrity": "sha512-D7lC0kcehVH7Mb26MRQi64LMyRJsj3dToJxM1+JVTl53DQSV5/7oUGWQLcKl1C1KnoVHxMMU2FNQMffr7F3Row==", "dev": true, "license": "MIT", "dependencies": { - "@typescript-eslint/eslint-plugin": "8.29.1", - "@typescript-eslint/parser": "8.29.1", - "@typescript-eslint/utils": "8.29.1" + "@typescript-eslint/eslint-plugin": "8.30.1", + "@typescript-eslint/parser": "8.30.1", + "@typescript-eslint/utils": "8.30.1" }, "engines": { "node": "^18.18.0 || ^20.9.0 || >=21.1.0" diff --git a/package.json b/package.json index f8dc64f89..fd474a3dd 100644 --- a/package.json +++ b/package.json @@ -254,7 +254,7 @@ "typedoc": "0.28.2", "typedoc-plugin-missing-exports": "4.0.0", "typescript": "5.8.3", - "typescript-eslint": "8.29.1", + "typescript-eslint": "8.30.1", "vanilla-js-wheel-zoom": "9.0.4", "vitest": "3.1.1", "webpack": "5.99.5", diff --git a/src/public/app/doc_notes/en/User Guide/User Guide/Advanced Usage/ETAPI (REST API).html b/src/public/app/doc_notes/en/User Guide/User Guide/Advanced Usage/ETAPI (REST API).html index 0aa779d60..a7ea78e55 100644 --- a/src/public/app/doc_notes/en/User Guide/User Guide/Advanced Usage/ETAPI (REST API).html +++ b/src/public/app/doc_notes/en/User Guide/User Guide/Advanced Usage/ETAPI (REST API).html @@ -8,12 +8,19 @@
  • trilium-py, you can use Python to communicate with Trilium.
  • +

    Obtaining a token

    +

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

    Authentication

    -

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

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

    Via the Authorization header

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

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

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

    where ETAPITOKEN is the token obtained in the previous step.

    +

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

    +

    Basic authentication

    +

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

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

      Running the Docker Container

      Local Access Only

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

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

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

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

        +

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

        There are two APIs:

        In both cases, the API resides in a global variable, api, diff --git a/src/public/app/layouts/desktop_layout.ts b/src/public/app/layouts/desktop_layout.ts index ee6faed63..4ca86197a 100644 --- a/src/public/app/layouts/desktop_layout.ts +++ b/src/public/app/layouts/desktop_layout.ts @@ -122,6 +122,7 @@ export default class DesktopLayout { const rootContainer = new RootContainer(true) .setParent(appContext) + .class((launcherPaneIsHorizontal ? "horizontal" : "vertical") + "-layout") .optChild( fullWidthTabBar, new FlexContainer("row") diff --git a/src/public/app/layouts/mobile_layout.ts b/src/public/app/layouts/mobile_layout.ts index f652e5f23..f6d69f4c6 100644 --- a/src/public/app/layouts/mobile_layout.ts +++ b/src/public/app/layouts/mobile_layout.ts @@ -7,8 +7,6 @@ import ToggleSidebarButtonWidget from "../widgets/mobile_widgets/toggle_sidebar_ import MobileDetailMenuWidget from "../widgets/mobile_widgets/mobile_detail_menu.js"; import ScreenContainer from "../widgets/mobile_widgets/screen_container.js"; import ScrollingContainer from "../widgets/containers/scrolling_container.js"; -import ProtectedSessionPasswordDialog from "../widgets/dialogs/protected_session_password.js"; -import ConfirmDialog from "../widgets/dialogs/confirm.js"; import FilePropertiesWidget from "../widgets/ribbon_widgets/file_properties.js"; import FloatingButtons from "../widgets/floating_buttons/floating_buttons.js"; import EditButton from "../widgets/floating_buttons/edit_button.js"; @@ -118,6 +116,7 @@ export default class MobileLayout { getRootWidget(appContext: typeof AppContext) { const rootContainer = new RootContainer(true) .setParent(appContext) + .class("horizontal-layout") .cssBlock(MOBILE_CSS) .child(new FlexContainer("column").id("mobile-sidebar-container")) .child( diff --git a/src/public/app/types-lib.d.ts b/src/public/app/types-lib.d.ts index 51155abfb..57b810f76 100644 --- a/src/public/app/types-lib.d.ts +++ b/src/public/app/types-lib.d.ts @@ -13,7 +13,7 @@ declare module "draggabilly" { containment: HTMLElement }); element: HTMLElement; - on(event: "pointerDown" | "dragStart" | "dragEnd" | "dragMove", callback: Callback); + on(event: "staticClick" | "dragStart" | "dragEnd" | "dragMove", callback: Callback); dragEnd(); isDragging: boolean; positionDrag: () => void; diff --git a/src/public/app/widgets/containers/root_container.ts b/src/public/app/widgets/containers/root_container.ts index 251a92691..c941cdd88 100644 --- a/src/public/app/widgets/containers/root_container.ts +++ b/src/public/app/widgets/containers/root_container.ts @@ -19,7 +19,6 @@ export default class RootContainer extends FlexContainer { this.id("root-widget"); this.css("height", "100dvh"); - this.class((isHorizontalLayout ? "horizontal" : "vertical") + "-layout"); this.originalViewportHeight = getViewportHeight(); } diff --git a/src/public/app/widgets/tab_row.ts b/src/public/app/widgets/tab_row.ts index 4de766357..c5fb4e582 100644 --- a/src/public/app/widgets/tab_row.ts +++ b/src/public/app/widgets/tab_row.ts @@ -11,10 +11,11 @@ import type NoteContext from "../components/note_context.js"; const isDesktop = utils.isDesktop(); -const TAB_CONTAINER_MIN_WIDTH = 24; +const TAB_CONTAINER_MIN_WIDTH = 100; const TAB_CONTAINER_MAX_WIDTH = 240; const TAB_CONTAINER_LEFT_PADDING = 5; -const NEW_TAB_WIDTH = 32; +const SCROLL_BUTTON_WIDTH = 36; +const NEW_TAB_WIDTH = 36; const MIN_FILLER_WIDTH = isDesktop ? 50 : 15; const MARGIN_WIDTH = 5; @@ -32,6 +33,8 @@ const TAB_TPL = ` `; +const CONTAINER_ANCHOR_TPL = `

        `; + const NEW_TAB_BUTTON_TPL = `
        +
        `; const FILLER_TPL = `
        `; @@ -39,11 +42,11 @@ const TAB_ROW_TPL = `
        - -
        +
        +
        +
        +
        +
        `; export default class TabRowWidget extends BasicWidget { @@ -244,11 +306,24 @@ export default class TabRowWidget extends BasicWidget { private draggabillyDragging?: Draggabilly | null; private $style!: JQuery; + private $tabScrollingContainer!: JQuery; + private $tabContainer!: JQuery; + private $scrollButtonLeft!: JQuery; + private $scrollButtonRight!: JQuery; + private $containerAnchor!: JQuery; private $filler!: JQuery; private $newTab!: JQuery; + private updateScrollTimeout: ReturnType | undefined; + + private newTabOuterWidth: number = 0; + private scrollButtonsOuterWidth: number = 0; doRender() { this.$widget = $(TAB_ROW_TPL); + this.$tabScrollingContainer = this.$widget.children(".tab-row-widget-scrolling-container"); + this.$tabContainer = this.$widget.find(".tab-row-widget-container"); + this.$scrollButtonLeft = this.$widget.children(".tab-scroll-button-left"); + this.$scrollButtonRight = this.$widget.children(".tab-scroll-button-right"); const documentStyle = window.getComputedStyle(document.documentElement); this.showNoteIcons = documentStyle.getPropertyValue("--tab-note-icons") === "true"; @@ -257,11 +332,13 @@ export default class TabRowWidget extends BasicWidget { this.setupStyle(); this.setupEvents(); + this.setupContainerAnchor(); this.setupDraggabilly(); this.setupNewButton(); this.setupFiller(); this.layoutTabs(); this.setVisibility(); + this.setupScrollEvents(); this.$widget.on("contextmenu", ".note-tab", (e) => { e.preventDefault(); @@ -300,6 +377,60 @@ export default class TabRowWidget extends BasicWidget { this.$widget.append(this.$style); } + scrollTabContainer(direction: number, behavior: ScrollBehavior = "smooth") { + const currentScrollLeft = this.$tabScrollingContainer[0]?.scrollLeft; + this.$tabScrollingContainer[0].scrollTo({ + left: currentScrollLeft + direction, + behavior + }); + }; + + setupScrollEvents() { + let isScrolling = false; + this.$tabScrollingContainer[0].addEventListener('wheel', (event) => { + if (!isScrolling) { + isScrolling = true; + requestAnimationFrame(() => { + this.scrollTabContainer(event.deltaY * 1.5, 'instant'); + isScrolling = false; + }); + } + }); + + this.$scrollButtonLeft[0].addEventListener('click', () => this.scrollTabContainer(-200)); + this.$scrollButtonRight[0].addEventListener('click', () => this.scrollTabContainer(200)); + + this.$tabScrollingContainer[0].addEventListener('scroll', () => { + clearTimeout(this.updateScrollTimeout); + this.updateScrollTimeout = setTimeout(() => { + this.updateScrollButtonState(); + }, 100); + }); + } + + updateScrollButtonState() { + const scrollLeft = this.$tabScrollingContainer[0].scrollLeft; + const scrollWidth = this.$tabScrollingContainer[0].scrollWidth; + const clientWidth = this.$tabScrollingContainer[0].clientWidth; + // Detect whether the scrollbar is at the far left or far right. + this.$scrollButtonLeft.toggleClass("disabled", Math.abs(scrollLeft) <= 1); + this.$scrollButtonRight.toggleClass("disabled", Math.abs(scrollLeft + clientWidth - scrollWidth) <= 1); + } + + setScrollButtonVisibility(show: boolean = true) { + if (show) { + this.$scrollButtonLeft.css("display", "flex"); + this.$scrollButtonRight.css("display", "flex"); + clearTimeout(this.updateScrollTimeout); + this.updateScrollTimeout = setTimeout(() => { + this.updateScrollButtonState(); + }, 200); + } else { + this.$scrollButtonLeft.css("display", "none"); + this.$scrollButtonRight.css("display", "none"); + } + } + setupEvents() { new ResizeObserver((_) => { this.cleanUpPreviouslyDraggedTabs(); @@ -317,14 +448,32 @@ export default class TabRowWidget extends BasicWidget { return Array.prototype.slice.call(this.$widget.find(".note-tab")); } - get $tabContainer() { - return this.$widget.find(".tab-row-widget-container"); + updateOuterWidth() { + if (this.newTabOuterWidth == 0) { + this.newTabOuterWidth = this.$newTab?.outerWidth(true) ?? 0; + } + if (this.scrollButtonsOuterWidth == 0) { + this.scrollButtonsOuterWidth = (this.$scrollButtonLeft?.outerWidth(true) ?? 0) + (this.$scrollButtonRight?.outerWidth(true) ?? 0); + } } + get tabWidths() { const numberOfTabs = this.tabEls.length; - const tabsContainerWidth = this.$tabContainer[0].clientWidth - NEW_TAB_WIDTH - MIN_FILLER_WIDTH; - const marginWidth = (numberOfTabs - 1) * MARGIN_WIDTH; + // this.$newTab may include margin, and using NEW_TAB_WIDTH could cause tabsContainerWidth to be slightly larger, + // resulting in misaligned scrollbars/buttons. Therefore, use outerwidth. + this.updateOuterWidth(); + let tabsContainerWidth = Math.floor(this.$widget.width() ?? 0); + tabsContainerWidth -= this.newTabOuterWidth + MIN_FILLER_WIDTH; + // Check whether the scroll buttons need to be displayed. + if ((TAB_CONTAINER_MIN_WIDTH + MARGIN_WIDTH) * numberOfTabs > tabsContainerWidth) { + tabsContainerWidth -= this.scrollButtonsOuterWidth; + this.setScrollButtonVisibility(true); + } else { + this.setScrollButtonVisibility(false); + } + + const marginWidth = (numberOfTabs - 1) * MARGIN_WIDTH + TAB_CONTAINER_LEFT_PADDING; const targetWidth = (tabsContainerWidth - marginWidth) / numberOfTabs; const clampedTargetWidth = Math.max(TAB_CONTAINER_MIN_WIDTH, Math.min(TAB_CONTAINER_MAX_WIDTH, targetWidth)); const flooredClampedTargetWidth = Math.floor(clampedTargetWidth); @@ -344,10 +493,6 @@ export default class TabRowWidget extends BasicWidget { } } - if (this.$filler) { - this.$filler.css("width", `${extraWidthRemaining + MIN_FILLER_WIDTH}px`); - } - return widths; } @@ -362,10 +507,9 @@ export default class TabRowWidget extends BasicWidget { position -= MARGIN_WIDTH; // the last margin should not be applied - const newTabPosition = position; - const fillerPosition = position + 32; + const anchorPosition = position; - return { tabPositions, newTabPosition, fillerPosition }; + return { tabPositions, anchorPosition }; } layoutTabs() { @@ -386,15 +530,14 @@ export default class TabRowWidget extends BasicWidget { let styleHTML = ""; - const { tabPositions, newTabPosition, fillerPosition } = this.getTabPositions(); + const { tabPositions, anchorPosition } = this.getTabPositions(); tabPositions.forEach((position, i) => { styleHTML += `.note-tab:nth-child(${i + 1}) { transform: translate3d(${position}px, 0, 0)} `; }); - styleHTML += `.note-new-tab { transform: translate3d(${newTabPosition}px, 0, 0) } `; - styleHTML += `.tab-row-filler { transform: translate3d(${fillerPosition}px, 0, 0) } `; - + styleHTML += `.tab-row-container-anchor { transform: translate3d(${anchorPosition}px, 0, 0) } `; + styleHTML += `.tab-row-widget-container {width: ${anchorPosition}px}`; this.$style.html(styleHTML); } @@ -406,8 +549,7 @@ export default class TabRowWidget extends BasicWidget { $tab.addClass("note-tab-was-just-added"); setTimeout(() => $tab.removeClass("note-tab-was-just-added"), 500); - - this.$newTab.before($tab); + this.$containerAnchor.before($tab); this.setVisibility(); this.setTabCloseEvent($tab); this.updateTitle($tab, t("tab_row.new_tab")); @@ -507,6 +649,7 @@ export default class TabRowWidget extends BasicWidget { setupDraggabilly() { const tabEls = this.tabEls; const { tabPositions } = this.getTabPositions(); + let initialScrollLeft = 0; if (this.isDragging && this.draggabillyDragging) { this.isDragging = false; @@ -533,7 +676,7 @@ export default class TabRowWidget extends BasicWidget { this.draggabillies.push(draggabilly); - draggabilly.on("pointerDown", () => { + draggabilly.on("staticClick", () => { appContext.tabManager.activateNoteContext(tabEl.getAttribute("data-ntx-id")); }); @@ -542,11 +685,20 @@ export default class TabRowWidget extends BasicWidget { this.draggabillyDragging = draggabilly; tabEl.classList.add("note-tab-is-dragging"); this.$widget.addClass("tab-row-widget-is-sorting"); + + initialScrollLeft = this.$tabScrollingContainer?.scrollLeft() ?? 0; + draggabilly.positionDrag = () => { }; }); draggabilly.on("dragEnd", () => { this.isDragging = false; - const finalTranslateX = parseFloat(tabEl.style.left); + const currentScrollLeft = this.$tabScrollingContainer?.scrollLeft() ?? 0; + const scrollDelta = currentScrollLeft - initialScrollLeft; + const translateX = parseFloat(tabEl.style.left) + scrollDelta; + const maxTranslateX = this.$tabContainer[0]?.offsetWidth - tabEl.offsetWidth; + const minTranslateX = 0; + const finalTranslateX = Math.min(maxTranslateX, Math.max(minTranslateX, translateX)); + tabEl.style.transform = `translate3d(0, 0, 0)`; // Animate dragged tab back into its place @@ -570,12 +722,31 @@ export default class TabRowWidget extends BasicWidget { }); }); - draggabilly.on("dragMove", (event: unknown, pointer: unknown, moveVector: MoveVector) => { + draggabilly.on("dragMove", (event: unknown, pointer: PointerEvent, moveVector: MoveVector) => { // The current index be computed within the event since it can change during the dragMove const tabEls = this.tabEls; const currentIndex = tabEls.indexOf(tabEl); - const currentTabPositionX = originalTabPositionX + moveVector.x; + const scorllContainerBounds = this.$tabScrollingContainer[0]?.getBoundingClientRect(); + const pointerX = pointer.pageX; + const scrollSpeed = 100; // The increment of each scroll. + // Check if the mouse is near the edge of the container and trigger scrolling. + if (pointerX < scorllContainerBounds.left) { + this.scrollTabContainer(- scrollSpeed); + } else if (pointerX > scorllContainerBounds.right) { + this.scrollTabContainer(scrollSpeed); + } + + const currentScrollLeft = this.$tabScrollingContainer?.scrollLeft() ?? 0; + const scrollDelta = currentScrollLeft - initialScrollLeft; + let translateX = moveVector.x + scrollDelta; + + // Limit the `translateX` so that `tabEl` cannot exceed the left and right boundaries of the container. + const maxTranslateX = this.$tabContainer[0]?.offsetWidth - tabEl.offsetWidth - originalTabPositionX; + const minTranslateX = - originalTabPositionX; + translateX = Math.min(maxTranslateX, Math.max(minTranslateX, translateX)); + tabEl.style.transform = `translate3d(${translateX}px, 0, 0)`; + const currentTabPositionX = originalTabPositionX + translateX; const destinationIndexTarget = this.closest(currentTabPositionX, tabPositions); const destinationIndex = Math.max(0, Math.min(tabEls.length, destinationIndexTarget)); @@ -594,8 +765,7 @@ export default class TabRowWidget extends BasicWidget { if (destinationIndex < originIndex) { tabEl.parentNode?.insertBefore(tabEl, this.tabEls[destinationIndex]); } else { - const beforeEl = this.tabEls[destinationIndex + 1] || this.$newTab[0]; - + const beforeEl = this.tabEls[destinationIndex + 1] || this.$containerAnchor[0]; tabEl.parentNode?.insertBefore(tabEl, beforeEl); } this.triggerEvent("tabReorder", { ntxIdsInOrder: this.getNtxIdsInOrder() }); @@ -604,14 +774,19 @@ export default class TabRowWidget extends BasicWidget { setupNewButton() { this.$newTab = $(NEW_TAB_BUTTON_TPL); - - this.$tabContainer.append(this.$newTab); + this.$widget.append(this.$newTab); } setupFiller() { this.$filler = $(FILLER_TPL); - this.$tabContainer.append(this.$filler); + this.$widget.append(this.$filler); + } + + setupContainerAnchor() { + this.$containerAnchor = $(CONTAINER_ANCHOR_TPL); + + this.$tabContainer.append(this.$containerAnchor); } closest(value: number, array: number[]) { @@ -660,7 +835,9 @@ export default class TabRowWidget extends BasicWidget { updateTabById(ntxId: string | null) { const $tab = this.getTabById(ntxId); - + $tab[0].scrollIntoView({ + behavior: 'smooth' + }); const noteContext = appContext.tabManager.getNoteContextById(ntxId); this.updateTab($tab, noteContext); diff --git a/src/public/app/widgets/type_widgets/ckeditor/config.ts b/src/public/app/widgets/type_widgets/ckeditor/config.ts index 6d4726999..817825ac3 100644 --- a/src/public/app/widgets/type_widgets/ckeditor/config.ts +++ b/src/public/app/widgets/type_widgets/ckeditor/config.ts @@ -74,7 +74,7 @@ export function buildConfig() { heading: { options: [ { model: "paragraph" as const, title: "Paragraph", class: "ck-heading_paragraph" }, - // // heading1 is not used since that should be a note's title + // heading1 is not used since that should be a note's title { model: "heading2" as const, view: "h2", title: "Heading 2", class: "ck-heading_heading2" }, { model: "heading3" as const, view: "h3", title: "Heading 3", class: "ck-heading_heading3" }, { model: "heading4" as const, view: "h4", title: "Heading 4", class: "ck-heading_heading4" }, diff --git a/src/public/stylesheets/theme-next/shell.css b/src/public/stylesheets/theme-next/shell.css index da8f3bb1d..483d6910d 100644 --- a/src/public/stylesheets/theme-next/shell.css +++ b/src/public/stylesheets/theme-next/shell.css @@ -869,46 +869,23 @@ body.mobile .fancytree-node > span { position: relative; } -/* #region Apply a border to the tab bar that avoids the current tab but also allows a transparent active tab. */ -body.layout-horizontal .tab-row-widget, -body.layout-horizontal .tab-row-widget-container, -body.layout-horizontal .tab-row-container .note-tab[active] { - overflow: visible !important; -} - -body.layout-horizontal .tab-row-container .note-tab[active]:before { - content: ""; - position: absolute; - bottom: 0; - left: -100vw; - top: var(--tab-height); - right: calc(100% - 1px); - height: 1px; +/* Apply a border to the tab bar that avoids the current tab but also allows a transparent active tab. */ +body.layout-horizontal .tab-row-container { border-bottom: 1px solid var(--launcher-pane-horiz-border-color); } -body.layout-horizontal .tab-row-container .note-tab[active]:after { - content: ""; - position: absolute; - bottom: 0; - left: 100%; - top: var(--tab-height); - right: 0; - width: 100vw; - height: 1px; - border-bottom: 1px solid var(--launcher-pane-horiz-border-color); -} -/* #endregion */ - body.layout-vertical.electron.platform-darwin .tab-row-container { border-bottom: 1px solid var(--subtle-border-color); } .tab-row-widget-container { - margin-top: calc((var(--tab-bar-height) - var(--tab-height)) / 2); height: var(--tab-height) !important; } +.tab-row-widget > * { + margin-top: calc((var(--tab-bar-height) - var(--tab-height)) / 2); +} + body.layout-horizontal .tab-row-container { padding-top: calc((var(--tab-bar-height) - var(--tab-height))); } @@ -923,11 +900,12 @@ body.layout-vertical #left-pane .quick-search { /* Limit the drag area for the previous elements to include just to the element itself and not its descendants also */ body.layout-horizontal .tab-row-container > *, -body.layout-vertical .tab-row-widget > *, +body.layout-vertical .tab-row-widget > *:not(.tab-row-filler), body.layout-vertical #left-pane .quick-search > * { -webkit-app-region: no-drag; } +body.layout-horizontal .tab-row-widget, body.layout-horizontal .tab-row-widget-container { margin-top: 0; position: relative; @@ -991,7 +969,7 @@ body.layout-horizontal .tab-row-widget .note-tab .note-tab-wrapper { text-overflow: ellipsis; } -body.layout-vertical .tab-row-widget-is-sorting .note-tab[active] .note-tab-wrapper { +body.layout-vertical .tab-row-widget-is-sorting .note-tab.note-tab-is-dragging .note-tab-wrapper { transform: scale(0.85); box-shadow: var(--active-tab-dragging-shadow) !important; } diff --git a/src/routes/login.ts b/src/routes/login.ts index 3f4d52f32..1b2d42b25 100644 --- a/src/routes/login.ts +++ b/src/routes/login.ts @@ -92,11 +92,10 @@ function login(req: Request, res: Response) { const rememberMe = req.body.rememberMe; req.session.regenerate(() => { - if (rememberMe) { - req.session.cookie.maxAge = 21 * 24 * 3600000; // 3 weeks - } else { + if (!rememberMe) { // unset default maxAge set by sessionParser - // Cookie becomes non-persistent and expires after current browser session (e.g. when browser is closed) + // Cookie becomes non-persistent and expires + // after current browser session (e.g. when browser is closed) req.session.cookie.maxAge = undefined; } @@ -134,7 +133,7 @@ function sendLoginError(req: Request, res: Response, errorType: 'password' | 'to log.info(`WARNING: Wrong password from ${req.ip}, rejecting.`); } - res.render('login', { + res.status(401).render('login', { wrongPassword: errorType === 'password', wrongTotp: errorType === 'totp', totpEnabled: totp.isTotpEnabled(), diff --git a/src/services/etapi_tokens.ts b/src/services/etapi_tokens.ts index 93d916a60..031856f2a 100644 --- a/src/services/etapi_tokens.ts +++ b/src/services/etapi_tokens.ts @@ -48,6 +48,11 @@ function parseAuthToken(auth: string | undefined) { auth = basicAuthChunks[1]; } + if (auth.startsWith("Bearer ")) { + // allow also bearer auth format + auth = auth.substring(7); + } + const chunks = auth.split("_"); if (chunks.length === 1) { diff --git a/src/services/sql.ts b/src/services/sql.ts index d135b54da..f686b0876 100644 --- a/src/services/sql.ts +++ b/src/services/sql.ts @@ -112,12 +112,21 @@ function upsert(tableName: string, primaryKey: string, rec: T) { execute(query, rec); } -function stmt(sql: string) { - if (!(sql in statementCache)) { - statementCache[sql] = dbConnection.prepare(sql); +/** + * For the given SQL query, returns a prepared statement. For the same query (string comparison), the same statement is returned. + * + * @param sql the SQL query for which to return a prepared statement. + * @param isRaw indicates whether `.raw()` is going to be called on the prepared statement in order to return the raw rows (e.g. via {@link getRawRows()}). The reason is that the raw state is preserved in the saved statement and would break non-raw calls for the same query. + * @returns the corresponding {@link Statement}. + */ +function stmt(sql: string, isRaw?: boolean) { + const key = (isRaw ? "raw/" + sql : sql); + + if (!(key in statementCache)) { + statementCache[key] = dbConnection.prepare(sql); } - return statementCache[sql]; + return statementCache[key]; } function getRow(query: string, params: Params = []): T { @@ -172,7 +181,7 @@ function getRows(query: string, params: Params = []): T[] { } function getRawRows(query: string, params: Params = []): T[] { - return (wrap(query, (s) => s.raw().all(params)) as T[]) || []; + return (wrap(query, (s) => s.raw().all(params), true) as T[]) || []; } function iterateRows(query: string, params: Params = []): IterableIterator { @@ -234,7 +243,10 @@ function executeScript(query: string): DatabaseType { return dbConnection.exec(query); } -function wrap(query: string, func: (statement: Statement) => unknown): unknown { +/** + * @param isRaw indicates whether `.raw()` is going to be called on the prepared statement in order to return the raw rows (e.g. via {@link getRawRows()}). The reason is that the raw state is preserved in the saved statement and would break non-raw calls for the same query. + */ +function wrap(query: string, func: (statement: Statement) => unknown, isRaw?: boolean): unknown { const startTimestamp = Date.now(); let result; @@ -243,7 +255,7 @@ function wrap(query: string, func: (statement: Statement) => unknown): unknown { } try { - result = func(stmt(query)); + result = func(stmt(query, isRaw)); } catch (e: any) { if (e.message.includes("The database connection is not open")) { // this often happens on killing the app which puts these alerts in front of user