Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
4a1045b41e | ||
|
|
c305b5f8dd | ||
|
|
b5962af466 | ||
|
|
13e83b6689 | ||
|
|
432e4f472a | ||
|
|
8731ca4ec1 | ||
|
|
6b165059cc | ||
|
|
213797a38e | ||
|
|
fc3e388b80 | ||
|
|
1c0359e540 | ||
|
|
da859e581b | ||
|
|
763e673738 | ||
|
|
550d3ab3e9 | ||
|
|
e62404d4c0 | ||
|
|
54e051ba02 | ||
|
|
344728dbe8 | ||
|
|
818e9ad82a | ||
|
|
4931ee3fab | ||
|
|
65e09e8fdd | ||
|
|
64e521d549 | ||
|
|
b8308d89e8 | ||
|
|
0e5b380f8c | ||
|
|
e76919ab5c | ||
|
|
84ab90fd3f | ||
|
|
87b191eb63 | ||
|
|
0def9d6d6d | ||
|
|
e708d8ee63 | ||
|
|
461001442a | ||
|
|
6b5e923e8e | ||
|
|
289606a746 | ||
|
|
9c53b3b594 | ||
|
|
d497ff8aaf | ||
|
|
51acd51cb2 | ||
|
|
b4bb3b89d6 | ||
|
|
9b51aef073 | ||
|
|
4aa0b200b9 | ||
|
|
8769b56fd1 | ||
|
|
06d2cd744f | ||
|
|
7428cc7dfb | ||
|
|
6c89d668fe | ||
|
|
2c76a4747e | ||
|
|
fd55c00015 | ||
|
|
28ef37f354 | ||
|
|
bf5a9f134d | ||
|
|
2991940ba8 | ||
|
|
54352a03d8 | ||
|
|
c4d1f12b2e | ||
|
|
aa699007f7 | ||
|
|
047a8b3c3d | ||
|
|
6098dd01b4 | ||
|
|
b16beb9af5 | ||
|
|
95565d7506 | ||
|
|
a13d17ed81 | ||
|
|
95515cac9d | ||
|
|
21dd2d1681 | ||
|
|
6d5f12c47c | ||
|
|
d01bd4ea01 | ||
|
|
bd7d66b1e8 | ||
|
|
0b9fe69819 | ||
|
|
936c38a21d | ||
|
|
e60e637437 | ||
|
|
479c957698 | ||
|
|
13fd70f944 | ||
|
|
015726f896 | ||
|
|
5e2c31598c | ||
|
|
24d1ba9f8f | ||
|
|
5345442de2 | ||
|
|
6ea2ca0ae2 | ||
|
|
6f4f526d07 | ||
|
|
13bad87500 | ||
|
|
acecc9c68d | ||
|
|
35a8501f2d | ||
|
|
09f9df968d | ||
|
|
2c58bf3d9a | ||
|
|
e063f1c73a | ||
|
|
cafc0ce1ec | ||
|
|
a192723fbd | ||
|
|
b643eeb884 | ||
|
|
eb042e5760 | ||
|
|
6251d96193 | ||
|
|
db92dabba5 | ||
|
|
2469a971ca | ||
|
|
a43bf05bf0 | ||
|
|
ffd9ebad7e | ||
|
|
d2100c8500 | ||
|
|
7e63d721bc | ||
|
|
2c774c51c6 | ||
|
|
8eba4527af | ||
|
|
181b3eb6c2 | ||
|
|
879d2f42c6 | ||
|
|
85ea26b57a | ||
|
|
800c81e598 | ||
|
|
30c3523722 | ||
|
|
761bf5b42e | ||
|
|
b10a7b1bfc | ||
|
|
313d86df23 | ||
|
|
95627cbb5c | ||
|
|
b9ea14f330 | ||
|
|
812d025a9d | ||
|
|
11cc2daa4b | ||
|
|
5c36f2aa71 | ||
|
|
987b1b9ed7 | ||
|
|
3ef0e14581 | ||
|
|
f98b3415bc | ||
|
|
16805c96aa | ||
|
|
9f2e0ba8b6 | ||
|
|
7b3609afbb | ||
|
|
84ff75f223 | ||
|
|
56dc0eefd7 | ||
|
|
eeea16531a | ||
|
|
ac5ab2a27a | ||
|
|
fef4ece2c0 | ||
|
|
613451f4d8 | ||
|
|
04dee78b27 | ||
|
|
69330bf23c | ||
|
|
4843bcacd5 | ||
|
|
3d28665e1e | ||
|
|
209585a0b4 | ||
|
|
84eab75ec8 | ||
|
|
ce17b38509 | ||
|
|
85c1e1e7d3 | ||
|
|
ebf38e2281 | ||
|
|
32ec62419d | ||
|
|
e46fda91a4 | ||
|
|
62d5388176 | ||
|
|
db2bef892a | ||
|
|
1c46935cc5 | ||
|
|
f0f19125d5 | ||
|
|
828f83d2e3 | ||
|
|
002fdb763e | ||
|
|
d3ff1fb931 | ||
|
|
7951472ae3 | ||
|
|
5cffe8846b | ||
|
|
806e975e31 | ||
|
|
e4232269f6 | ||
|
|
ab759a006d | ||
|
|
ca7f25003c | ||
|
|
08ab0b90f2 | ||
|
|
e275f5c0bb | ||
|
|
683a01627e | ||
|
|
3c11a7f63e | ||
|
|
90a96552d3 | ||
|
|
a710603b0a | ||
|
|
80a6fdca69 | ||
|
|
1dc78ca916 | ||
|
|
3118af1e41 | ||
|
|
a669d96aed | ||
|
|
a0b1fa0481 | ||
|
|
2e8c048a3e | ||
|
|
d2ddc7f0de | ||
|
|
3b88b20c61 | ||
|
|
f8371aaa57 | ||
|
|
c5a45131a5 | ||
|
|
1681fade3b | ||
|
|
4f324ad682 | ||
|
|
c1a9871703 | ||
|
|
83390f5ed2 | ||
|
|
5bf6d8063e | ||
|
|
a1913558b9 | ||
|
|
93e96829f4 | ||
|
|
11673bd8c4 | ||
|
|
4f4e2854a6 | ||
|
|
856ac94a76 | ||
|
|
28263de53d | ||
|
|
b6e855ce3e | ||
|
|
51148efe2e | ||
|
|
32e056dc24 | ||
|
|
c14ad6387d | ||
|
|
93122ade7b | ||
|
|
80bdabaf1b | ||
|
|
cea98cffde | ||
|
|
922bcee1e2 | ||
|
|
d33bb8d96e | ||
|
|
93ebb1987e | ||
|
|
cc0134dd22 | ||
|
|
1164a6bd86 | ||
|
|
6fcf7bdb5e | ||
|
|
8454f2eca8 | ||
|
|
7e480ad7e2 | ||
|
|
bc7c985489 | ||
|
|
a94c33edaf | ||
|
|
a1404062c5 | ||
|
|
9bb55afccc | ||
|
|
173e25dadc | ||
|
|
2fa9458eab | ||
|
|
0a3b7a13e3 | ||
|
|
50d27d5636 | ||
|
|
d050ac907f | ||
|
|
80ca411a08 | ||
|
|
35894cdfd1 | ||
|
|
4df96362f4 | ||
|
|
fc7d34612d | ||
|
|
1b7dc77792 | ||
|
|
5f311f2404 | ||
|
|
f19c3c2caa | ||
|
|
0f7b1debbb | ||
|
|
4aa399e96e | ||
|
|
15c6e6388a | ||
|
|
b501d2bd5c | ||
|
|
fb4c5748a8 | ||
|
|
d9cb615f1f | ||
|
|
0e09d1f9f1 | ||
|
|
db924f7352 | ||
|
|
2fea44bde6 | ||
|
|
784248f984 | ||
|
|
ee16ef07c5 | ||
|
|
3648a2dc90 | ||
|
|
8f250d1ab9 | ||
|
|
67d5ae87c3 | ||
|
|
08abbe25ef | ||
|
|
000d68a0e7 | ||
|
|
d8c391a480 | ||
|
|
459af56564 | ||
|
|
9d82f2aed7 | ||
|
|
4fb99c7e39 | ||
|
|
92e8422cb0 | ||
|
|
5defddb35c | ||
|
|
dc19976568 | ||
|
|
bd59f6dbee | ||
|
|
26fa22afaa | ||
|
|
e2702bdd51 | ||
|
|
0d0ecee06e | ||
|
|
6115ad4ab6 | ||
|
|
a400eeefed | ||
|
|
f19ddee17d | ||
|
|
393505e1ac | ||
|
|
b87e8b8a38 | ||
|
|
5b62302b3a | ||
|
|
ee0ffdf121 | ||
|
|
89eea8e71b | ||
|
|
f27512c7ee | ||
|
|
eafd2b0a24 | ||
|
|
aeafd55491 | ||
|
|
ccc5e3b7c6 | ||
|
|
a28e514ed4 | ||
|
|
7c94370e48 | ||
|
|
ab39fc2ed6 | ||
|
|
621e2b31e0 | ||
|
|
ca3900815b | ||
|
|
b36ce9ed5b | ||
|
|
59677577ec | ||
|
|
71a0b9c0a0 | ||
|
|
3876b7db96 | ||
|
|
0436136533 | ||
|
|
139ecc8176 | ||
|
|
4b82d51f6c | ||
|
|
8ceb44df78 | ||
|
|
3b3e7fbbe6 | ||
|
|
7cbd112585 | ||
|
|
d275b0e783 | ||
|
|
fedbdfd2a1 | ||
|
|
7f2aae39d1 | ||
|
|
90f6f315f9 | ||
|
|
9c092e90f1 | ||
|
|
ed2fc87e86 | ||
|
|
7624c42e02 | ||
|
|
d46012b650 | ||
|
|
5d11ae6ea0 | ||
|
|
bc372993c4 | ||
|
|
4897703111 | ||
|
|
1bc067d470 | ||
|
|
565688b284 | ||
|
|
e87a2edc00 | ||
|
|
9f595212dd | ||
|
|
79f16b454a | ||
|
|
7be1b56b7c | ||
|
|
0b17a7c458 | ||
|
|
6c2bc72fbb | ||
|
|
e67ec21319 | ||
|
|
e87c8cbe1d | ||
|
|
4fac6d05a7 | ||
|
|
fffea45c50 | ||
|
|
eb88955e20 | ||
|
|
5180bd5a38 | ||
|
|
a6b9037ae0 | ||
|
|
88251a503c | ||
|
|
283340a0f4 | ||
|
|
545d4aec3b | ||
|
|
9315151a64 | ||
|
|
4d93696028 | ||
|
|
6891a9cdc5 | ||
|
|
1e7fb248b2 | ||
|
|
19fd44b65d | ||
|
|
deeefaa849 | ||
|
|
29bd00a265 | ||
|
|
37a837a33a | ||
|
|
a26f047d2b | ||
|
|
245935bab0 | ||
|
|
61fa9cf1d8 | ||
|
|
1440bdcd90 | ||
|
|
25d4cf7a9d | ||
|
|
0b5db1340c | ||
|
|
d098066899 | ||
|
|
9e2f347a4b | ||
|
|
3453d406fc | ||
|
|
73a53dcb85 | ||
|
|
abac0985bc | ||
|
|
af394c0b43 | ||
|
|
8f4f6f5f0e | ||
|
|
74f1c7f6b0 | ||
|
|
3b5cdf22eb | ||
|
|
31f4867e91 | ||
|
|
8baa976a02 | ||
|
|
b4824973a5 | ||
|
|
a3229d451e | ||
|
|
c1faf0b0f5 | ||
|
|
564c28904f | ||
|
|
06d53cea66 | ||
|
|
d9fe358e6b | ||
|
|
253449acc2 | ||
|
|
bfea8c998a | ||
|
|
81dadd4be1 |
@@ -1,5 +1,10 @@
|
|||||||
node_modules
|
node_modules
|
||||||
|
|
||||||
|
# Foreign lockfiles - this project uses pnpm (pnpm-lock.yaml is the only lockfile)
|
||||||
|
package-lock.json
|
||||||
|
yarn.lock
|
||||||
|
bun.lockb
|
||||||
|
|
||||||
# Output
|
# Output
|
||||||
.output
|
.output
|
||||||
.vercel
|
.vercel
|
||||||
@@ -8,9 +13,23 @@ node_modules
|
|||||||
/.svelte-kit
|
/.svelte-kit
|
||||||
/build
|
/build
|
||||||
|
|
||||||
|
# Local tests
|
||||||
|
/tests/
|
||||||
|
|
||||||
|
# Nix
|
||||||
|
/result
|
||||||
|
|
||||||
# Tauri
|
# Tauri
|
||||||
/src-tauri/target
|
/src-tauri/target
|
||||||
/src-tauri/gen
|
|
||||||
|
# Track Android source (MainActivity.kt, manifest, proguard, gradle); Tauri's nested
|
||||||
|
# gen/android/.gitignore + app/.gitignore keep build/, .gradle/, .cxx/, generated/, *.so out.
|
||||||
|
/src-tauri/gen/*
|
||||||
|
!/src-tauri/gen/android/
|
||||||
|
|
||||||
|
# Never commit signing secrets (nested ignore misses keystore.properties)
|
||||||
|
/src-tauri/gen/android/keystore.properties
|
||||||
|
/src-tauri/gen/android/key.properties
|
||||||
|
|
||||||
# OS
|
# OS
|
||||||
.DS_Store
|
.DS_Store
|
||||||
|
|||||||
@@ -1,41 +1,157 @@
|
|||||||
# HelixNotes
|
# HelixNotes
|
||||||
|
|
||||||
|
[](https://gitlab.com/ArkHost/HelixNotes/-/blob/main/LICENSE)
|
||||||
|
[](https://gitlab.com/ArkHost/HelixNotes/-/releases/v1.3.4)
|
||||||
|
[](https://helixnotes.com)
|
||||||
|
[]()
|
||||||
|
|
||||||
A local markdown note-taking app built with Tauri, SvelteKit, and Rust.
|
A local markdown note-taking app built with Tauri, SvelteKit, and Rust.
|
||||||
|
|
||||||
Your notes are stored as standard Markdown files on your local filesystem. No cloud, no lock-in.
|
Your notes are stored as standard Markdown files on your local filesystem.
|
||||||
|
No cloud, no lock-in.
|
||||||
|
|
||||||
## Download
|
## Download (v1.3.4)
|
||||||
|
|
||||||
| Platform | Download | Notes |
|
### Linux
|
||||||
|----------|----------|-------|
|
|
||||||
| Linux (Arch/rolling) | [HelixNotes_1.0.9_amd64.AppImage](https://codeberg.org/ArkHost/HelixNotes/releases/download/v1.0.9/HelixNotes_1.0.9_amd64.AppImage) | Best for Arch, Fedora, openSUSE |
|
|
||||||
| Linux (Debian/Ubuntu/Mint) | [HelixNotes_1.0.9_amd64.deb](https://codeberg.org/ArkHost/HelixNotes/releases/download/v1.0.9/HelixNotes_1.0.9_amd64.deb) | Ubuntu 22.04+ |
|
|
||||||
| Windows | [HelixNotes_1.0.9_x64-setup.exe](https://codeberg.org/ArkHost/HelixNotes/releases/download/v1.0.9/HelixNotes_1.0.9_x64-setup.exe) | Windows 10/11 |
|
|
||||||
| macOS | [HelixNotes_1.0.9_aarch64.dmg](https://codeberg.org/ArkHost/HelixNotes/releases/download/v1.0.9/HelixNotes_1.0.9_aarch64.dmg) | macOS 12+ (Apple Silicon) |
|
|
||||||
|
|
||||||
See all releases: [codeberg.org/ArkHost/HelixNotes/releases](https://codeberg.org/ArkHost/HelixNotes/releases)
|
#### Arch / Manjaro (AUR)
|
||||||
|
|
||||||
|
```bash
|
||||||
|
yay -S helixnotes-appimage-bin
|
||||||
|
```
|
||||||
|
|
||||||
|
#### Debian / Ubuntu / Mint (APT)
|
||||||
|
|
||||||
|
```bash
|
||||||
|
curl -fsSL https://repo.arkhost.com/gpg.key | sudo gpg --dearmor -o /usr/share/keyrings/arkhost.gpg && echo "deb [signed-by=/usr/share/keyrings/arkhost.gpg arch=amd64] https://repo.arkhost.com stable main" | sudo tee /etc/apt/sources.list.d/helixnotes.list && sudo apt update && sudo apt install helix-notes
|
||||||
|
```
|
||||||
|
|
||||||
|
#### Solus (EOPKG)
|
||||||
|
|
||||||
|
```bash
|
||||||
|
sudo eopkg it helixnotes
|
||||||
|
```
|
||||||
|
|
||||||
|
#### NixOS
|
||||||
|
|
||||||
|
<details>
|
||||||
|
<summary>flake.nix</summary>
|
||||||
|
|
||||||
|
```nix
|
||||||
|
{
|
||||||
|
inputs = {
|
||||||
|
nixpkgs.url = "github:NixOS/nixpkgs/nixos-unstable";
|
||||||
|
helix-notes = {
|
||||||
|
url = "git+https://gitlab.com/ArkHost/HelixNotes";
|
||||||
|
# inputs.nixpkgs.follows = "nixpkgs";
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
outputs = {
|
||||||
|
nixpkgs,
|
||||||
|
helix-notes,
|
||||||
|
...
|
||||||
|
}: let
|
||||||
|
system = "x86_64-linux";
|
||||||
|
pkgs = nixpkgs.legacyPackages.${system};
|
||||||
|
in {
|
||||||
|
nixosConfigurations.default = nixpkgs.lib.nixosSystem {
|
||||||
|
system = system;
|
||||||
|
specialArgs = { inherit helix-notes; };
|
||||||
|
|
||||||
|
modules = [
|
||||||
|
/path/to/configuration.nix
|
||||||
|
];
|
||||||
|
};
|
||||||
|
};
|
||||||
|
}
|
||||||
|
```
|
||||||
|
</details>
|
||||||
|
|
||||||
|
|
||||||
|
<details>
|
||||||
|
<summary>configuration.nix</summary>
|
||||||
|
|
||||||
|
```nix
|
||||||
|
{
|
||||||
|
config,
|
||||||
|
lib,
|
||||||
|
pkgs,
|
||||||
|
helix-notes,
|
||||||
|
...
|
||||||
|
}:
|
||||||
|
{
|
||||||
|
users.users.<USERNAME> = {
|
||||||
|
packages = with pkgs; [
|
||||||
|
(helix-notes.packages.${pkgs.stdenv.hostPlatform.system}.default)
|
||||||
|
];
|
||||||
|
};
|
||||||
|
}
|
||||||
|
```
|
||||||
|
</details>
|
||||||
|
|
||||||
|
#### AppImage (Arch, Fedora 43+, openSUSE Tumbleweed)
|
||||||
|
|
||||||
|
[Download AppImage](https://download.helixnotes.com/releases/v1.3.4/HelixNotes_1.3.4_amd64.AppImage)
|
||||||
|
|
||||||
|
#### .deb (manual)
|
||||||
|
|
||||||
|
[Download .deb](https://download.helixnotes.com/releases/v1.3.4/HelixNotes_1.3.4_amd64.deb) (Ubuntu 22.04+)
|
||||||
|
|
||||||
|
#### .rpm (manual)
|
||||||
|
|
||||||
|
[Download .rpm](https://download.helixnotes.com/releases/v1.3.4/HelixNotes-1.3.4-1.x86_64.rpm)
|
||||||
|
|
||||||
|
### Windows
|
||||||
|
|
||||||
|
[Download Installer](https://download.helixnotes.com/releases/v1.3.4/HelixNotes_1.3.4_x64-setup.exe) (Windows 10/11)
|
||||||
|
|
||||||
|
### macOS
|
||||||
|
|
||||||
|
[Download .dmg (Apple Silicon)](https://download.helixnotes.com/releases/v1.3.4/HelixNotes_1.3.4_aarch64.dmg) (M-series Macs)
|
||||||
|
|
||||||
|
> **"HelixNotes is damaged and can't be opened"?** The app isn't damaged. The macOS build isn't notarized by Apple yet, so Gatekeeper blocks it on Apple Silicon. Run this once in Terminal, then open it normally (you'll need to redo it after each update):
|
||||||
|
>
|
||||||
|
> ```bash
|
||||||
|
> xattr -cr /Applications/HelixNotes.app
|
||||||
|
> ```
|
||||||
|
|
||||||
|
### Android
|
||||||
|
|
||||||
|
[Download APK](https://download.helixnotes.com/releases/v1.3.4/HelixNotes_1.3.4_android.apk)
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
All releases: [gitlab.com/ArkHost/HelixNotes/-/releases](https://gitlab.com/ArkHost/HelixNotes/-/releases)
|
||||||
|
|
||||||
## Features
|
## Features
|
||||||
|
|
||||||
- **Markdown editor** with rich formatting toolbar, slash commands, and source mode
|
- Markdown editor with toolbar, slash commands, source mode, code highlighting
|
||||||
- **Wiki-links** — link notes with `[[Note Title]]` syntax
|
- **Tasks view**: aggregate `- [ ]` checklists from across all notes, set priority and due dates, work in a list or a calendar (drag a task to reschedule)
|
||||||
- **Graph view** — visualize connections between your notes
|
- `[[Wiki-links]]` and graph view
|
||||||
- **Full-text search** powered by Tantivy
|
- Full-text search (Tantivy), CJK-aware for Chinese, Japanese, and Korean
|
||||||
- **AI writing tools** — improve, summarize, translate, and more (Anthropic / OpenAI)
|
- Outline panel, daily notes with calendar view, tags with autocomplete, drag-and-drop
|
||||||
- **Version history** — per-note snapshots with diff view
|
- Live KaTeX math editor (`/math`, `/imath`) with modal preview, double-click to edit
|
||||||
- **Backups** — automatic zip-based vault backups
|
- Mermaid diagrams (opt-in render, copy as PNG, save as PNG/SVG)
|
||||||
- **PDF preview** — inline rendering of embedded PDFs
|
- Encrypted secret blocks (`/secret`) stored as portable `helix-secret` markdown fences
|
||||||
- **Drag-and-drop** — move notes between notebooks, reorganize notebooks by dragging
|
- Insert date/time (`/date`, `/time`, `/now`), color swatches (`/color`), configurable week start
|
||||||
- **Obsidian import** — convert Obsidian wiki-links to standard markdown
|
- Manual notebook sorting (drag to reorder above, into, or below)
|
||||||
- **Themes** — light/dark mode with customizable accent colors and fonts
|
- External `.md` viewer mode with import-to-vault flow
|
||||||
- **Focus mode** — distraction-free writing
|
- PDF preview, Obsidian import, "Show in File Manager"
|
||||||
- **Local** — everything stays on your machine
|
- AI writing tools (Ollama / OpenAI-compatible / Anthropic / OpenAI)
|
||||||
|
- **Optional WebDAV sync** to your own server (Nextcloud, ownCloud, a NAS): manual or automatic, with keep-both conflict copies
|
||||||
|
- Version history with diffs, automatic backups
|
||||||
|
- Multi-window, file associations, focus mode, view mode
|
||||||
|
- Themes (light, dark, and 14 palettes), accent colors, fonts, 80-200% interface scale
|
||||||
|
- Local plain-text files, no company cloud
|
||||||
|
|
||||||
|
Full documentation: [helixnotes.com/docs](https://helixnotes.com/docs.html)
|
||||||
|
|
||||||
## Tech Stack
|
## Tech Stack
|
||||||
|
|
||||||
- **Frontend**: SvelteKit (Svelte 5) + TailwindCSS v4 + TipTap v3
|
- **Frontend**: SvelteKit (Svelte 5) + TailwindCSS v4 + TipTap v3
|
||||||
- **Backend**: Rust (Tauri 2.0) + Tantivy (search) + Notify (file watcher)
|
- **Backend**: Rust (Tauri 2.0) + Tantivy (search) + Notify (file watcher)
|
||||||
- **Platforms**: Linux (AppImage), Windows, macOS
|
- **Platforms**: Linux (AppImage), Windows, macOS, Android
|
||||||
|
|
||||||
## Building from Source
|
## Building from Source
|
||||||
|
|
||||||
@@ -59,6 +175,14 @@ pnpm tauri dev
|
|||||||
pnpm tauri build
|
pnpm tauri build
|
||||||
```
|
```
|
||||||
|
|
||||||
|
## Screenshots
|
||||||
|
|
||||||
|

|
||||||
|

|
||||||
|

|
||||||
|

|
||||||
|

|
||||||
|
|
||||||
## License
|
## License
|
||||||
|
|
||||||
[AGPL-3.0](LICENSE)
|
[AGPL-3.0](https://gitlab.com/ArkHost/HelixNotes/-/blob/main/LICENSE)
|
||||||
|
|||||||
@@ -0,0 +1,61 @@
|
|||||||
|
{
|
||||||
|
"nodes": {
|
||||||
|
"flake-utils": {
|
||||||
|
"inputs": {
|
||||||
|
"systems": "systems"
|
||||||
|
},
|
||||||
|
"locked": {
|
||||||
|
"lastModified": 1731533236,
|
||||||
|
"narHash": "sha256-l0KFg5HjrsfsO/JpG+r7fRrqm12kzFHyUHqHCVpMMbI=",
|
||||||
|
"owner": "numtide",
|
||||||
|
"repo": "flake-utils",
|
||||||
|
"rev": "11707dc2f618dd54ca8739b309ec4fc024de578b",
|
||||||
|
"type": "github"
|
||||||
|
},
|
||||||
|
"original": {
|
||||||
|
"owner": "numtide",
|
||||||
|
"repo": "flake-utils",
|
||||||
|
"type": "github"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"nixpkgs": {
|
||||||
|
"locked": {
|
||||||
|
"lastModified": 1785571196,
|
||||||
|
"narHash": "sha256-KoTsyMQqnXQZq8deCEnu4QkyldkwH/bpMMhUcfMdGIw=",
|
||||||
|
"owner": "NixOS",
|
||||||
|
"repo": "nixpkgs",
|
||||||
|
"rev": "148bab9c1c3c53136ecb44a6ea356a0ed5b39b06",
|
||||||
|
"type": "github"
|
||||||
|
},
|
||||||
|
"original": {
|
||||||
|
"owner": "NixOS",
|
||||||
|
"ref": "nixos-unstable",
|
||||||
|
"repo": "nixpkgs",
|
||||||
|
"type": "github"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"root": {
|
||||||
|
"inputs": {
|
||||||
|
"flake-utils": "flake-utils",
|
||||||
|
"nixpkgs": "nixpkgs"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"systems": {
|
||||||
|
"locked": {
|
||||||
|
"lastModified": 1681028828,
|
||||||
|
"narHash": "sha256-Vy1rq5AaRuLzOxct8nz4T6wlgyUR7zLU309k9mBC768=",
|
||||||
|
"owner": "nix-systems",
|
||||||
|
"repo": "default",
|
||||||
|
"rev": "da67096a3b9bf56a91d16901293e51ba5b49a27e",
|
||||||
|
"type": "github"
|
||||||
|
},
|
||||||
|
"original": {
|
||||||
|
"owner": "nix-systems",
|
||||||
|
"repo": "default",
|
||||||
|
"type": "github"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"root": "root",
|
||||||
|
"version": 7
|
||||||
|
}
|
||||||
@@ -0,0 +1,82 @@
|
|||||||
|
{
|
||||||
|
description = "HelixNotes (local Markdown notes app) with Nix flake";
|
||||||
|
|
||||||
|
inputs = {
|
||||||
|
nixpkgs.url = "github:NixOS/nixpkgs/nixos-unstable";
|
||||||
|
flake-utils.url = "github:numtide/flake-utils";
|
||||||
|
};
|
||||||
|
|
||||||
|
outputs =
|
||||||
|
{
|
||||||
|
self,
|
||||||
|
nixpkgs,
|
||||||
|
flake-utils,
|
||||||
|
}:
|
||||||
|
flake-utils.lib.eachDefaultSystem (
|
||||||
|
system:
|
||||||
|
let
|
||||||
|
pkgs = import nixpkgs { inherit system; };
|
||||||
|
|
||||||
|
cargoToml = builtins.fromTOML (builtins.readFile ./src-tauri/Cargo.toml);
|
||||||
|
pname = cargoToml.package.name;
|
||||||
|
version = cargoToml.package.version;
|
||||||
|
in
|
||||||
|
{
|
||||||
|
packages.default = pkgs.rustPlatform.buildRustPackage {
|
||||||
|
inherit pname version;
|
||||||
|
src = ./.;
|
||||||
|
|
||||||
|
cargoHash = "sha256-P0iXImWYKYPxVj7q2JSuDmd8JvjcKW4DzFUzXeUQZPk=C";
|
||||||
|
|
||||||
|
pnpmDeps = pkgs.fetchPnpmDeps {
|
||||||
|
inherit pname version;
|
||||||
|
src = ./.;
|
||||||
|
fetcherVersion = 4;
|
||||||
|
hash = "sha256-odUNKO2D50DG+VpuoTEo5FLMYy5pQSHQBtfHJrhJt78=";
|
||||||
|
};
|
||||||
|
|
||||||
|
nativeBuildInputs = with pkgs; [
|
||||||
|
cargo-tauri.hook
|
||||||
|
|
||||||
|
pnpmConfigHook
|
||||||
|
pnpm
|
||||||
|
nodejs
|
||||||
|
|
||||||
|
pkg-config
|
||||||
|
jq
|
||||||
|
moreutils
|
||||||
|
wrapGAppsHook3
|
||||||
|
];
|
||||||
|
|
||||||
|
buildInputs =
|
||||||
|
with pkgs;
|
||||||
|
lib.optionals stdenv.hostPlatform.isLinux [
|
||||||
|
webkitgtk_4_1
|
||||||
|
libayatana-appindicator
|
||||||
|
];
|
||||||
|
|
||||||
|
cargoRoot = "src-tauri";
|
||||||
|
buildAndTestSubdir = self.packages.${system}.default.cargoRoot;
|
||||||
|
|
||||||
|
# Deactivate the upstream update mechanism
|
||||||
|
postPatch = ''
|
||||||
|
jq '
|
||||||
|
.bundle.createUpdaterArtifacts = false |
|
||||||
|
.plugins.updater = {"active": false, "pubkey": "", "endpoints": []}
|
||||||
|
' \
|
||||||
|
src-tauri/tauri.conf.json | sponge src-tauri/tauri.conf.json
|
||||||
|
'';
|
||||||
|
|
||||||
|
meta = with pkgs.lib; {
|
||||||
|
description = "HelixNotes desktop application";
|
||||||
|
homepage = "https://helixnotes.com/";
|
||||||
|
license = licenses.agpl3Plus;
|
||||||
|
platforms = platforms.linux ++ platforms.darwin;
|
||||||
|
mainProgram = "helixnotes";
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
formatter = pkgs.nixfmt;
|
||||||
|
}
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -2,7 +2,7 @@
|
|||||||
"name": "helixnotes",
|
"name": "helixnotes",
|
||||||
"private": true,
|
"private": true,
|
||||||
"license": "AGPL-3.0-or-later",
|
"license": "AGPL-3.0-or-later",
|
||||||
"version": "1.1.0",
|
"version": "1.3.4",
|
||||||
"type": "module",
|
"type": "module",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"dev": "vite dev",
|
"dev": "vite dev",
|
||||||
@@ -59,12 +59,14 @@
|
|||||||
"@tiptap/starter-kit": "^3.19.0",
|
"@tiptap/starter-kit": "^3.19.0",
|
||||||
"@types/katex": "^0.16.8",
|
"@types/katex": "^0.16.8",
|
||||||
"@types/markdown-it": "^14.1.2",
|
"@types/markdown-it": "^14.1.2",
|
||||||
|
"highlight.js": "^11.11.1",
|
||||||
"katex": "^0.16.28",
|
"katex": "^0.16.28",
|
||||||
"lowlight": "^3.3.0",
|
"lowlight": "^3.3.0",
|
||||||
"markdown-it": "^14.1.0",
|
"markdown-it": "^14.1.0",
|
||||||
"markdown-it-mark": "^4.0.0",
|
"markdown-it-mark": "^4.0.0",
|
||||||
"markdown-it-sub": "^2.0.0",
|
"markdown-it-sub": "^2.0.0",
|
||||||
"markdown-it-sup": "^2.0.0",
|
"markdown-it-sup": "^2.0.0",
|
||||||
"markdown-it-task-lists": "^2.1.1"
|
"markdown-it-task-lists": "^2.1.1",
|
||||||
|
"mermaid": "^11.14.0"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,2 @@
|
|||||||
|
allowBuilds:
|
||||||
|
esbuild: true
|
||||||
@@ -1,6 +1,6 @@
|
|||||||
[package]
|
[package]
|
||||||
name = "helixnotes"
|
name = "helixnotes"
|
||||||
version = "1.1.0"
|
version = "1.3.4"
|
||||||
description = "Local markdown note-taking app"
|
description = "Local markdown note-taking app"
|
||||||
authors = ["HelixNotes"]
|
authors = ["HelixNotes"]
|
||||||
license = "AGPL-3.0-or-later"
|
license = "AGPL-3.0-or-later"
|
||||||
@@ -20,14 +20,14 @@ tauri-plugin-log = "2"
|
|||||||
tauri-plugin-dialog = "2"
|
tauri-plugin-dialog = "2"
|
||||||
tauri-plugin-fs = "2"
|
tauri-plugin-fs = "2"
|
||||||
tauri-plugin-opener = "2"
|
tauri-plugin-opener = "2"
|
||||||
tauri-plugin-updater = "2"
|
|
||||||
serde = { version = "1", features = ["derive"] }
|
serde = { version = "1", features = ["derive"] }
|
||||||
serde_json = "1"
|
serde_json = "1"
|
||||||
serde_yaml = "0.9"
|
serde_yaml = "0.9"
|
||||||
log = "0.4"
|
log = "0.4"
|
||||||
notify = "8"
|
notify = "8"
|
||||||
walkdir = "2"
|
walkdir = "2"
|
||||||
chrono = { version = "0.4", features = ["serde"] }
|
chrono = { version = "0.4", features = ["serde", "unstable-locales"] }
|
||||||
|
sys-locale = "0.3"
|
||||||
uuid = { version = "1", features = ["v4"] }
|
uuid = { version = "1", features = ["v4"] }
|
||||||
tantivy = "0.22"
|
tantivy = "0.22"
|
||||||
gray_matter = "0.2"
|
gray_matter = "0.2"
|
||||||
@@ -35,7 +35,23 @@ tokio = { version = "1", features = ["full"] }
|
|||||||
dirs = "6"
|
dirs = "6"
|
||||||
regex = "1"
|
regex = "1"
|
||||||
zip = { version = "2", default-features = false, features = ["deflate"] }
|
zip = { version = "2", default-features = false, features = ["deflate"] }
|
||||||
reqwest = { version = "0.12", features = ["json", "stream"] }
|
reqwest = { version = "0.12", default-features = false, features = ["json", "stream", "rustls-tls", "blocking"] }
|
||||||
futures = "0.3"
|
futures = "0.3"
|
||||||
rayon = "1"
|
rayon = "1"
|
||||||
|
quick-xml = "0.36"
|
||||||
|
urlencoding = "2"
|
||||||
|
sha2 = "0.10"
|
||||||
|
rustls = { version = "0.23", default-features = false, features = ["ring", "logging", "std", "tls12"] }
|
||||||
|
[target.'cfg(target_os = "macos")'.dependencies]
|
||||||
|
objc2-app-kit = "0.3.2"
|
||||||
|
|
||||||
|
[target.'cfg(target_os = "ios")'.dependencies]
|
||||||
|
tauri-plugin-ios-vault-access = { path = "plugins/ios-vault-access" }
|
||||||
|
|
||||||
|
[target.'cfg(not(any(target_os = "android", target_os = "ios")))'.dependencies]
|
||||||
|
tauri-plugin-updater = "2"
|
||||||
tauri-plugin-single-instance = "2"
|
tauri-plugin-single-instance = "2"
|
||||||
|
tauri-plugin-window-state = "2"
|
||||||
|
arboard = { version = "3", features = ["image-data", "wayland-data-control"] }
|
||||||
|
png = "0.17"
|
||||||
|
image = { version = "0.25", default-features = false, features = ["png", "jpeg", "webp", "gif"] }
|
||||||
|
|||||||
@@ -0,0 +1,10 @@
|
|||||||
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
|
||||||
|
<plist version="1.0">
|
||||||
|
<dict>
|
||||||
|
<key>UIFileSharingEnabled</key>
|
||||||
|
<true/>
|
||||||
|
<key>LSSupportsOpeningDocumentsInPlace</key>
|
||||||
|
<true/>
|
||||||
|
</dict>
|
||||||
|
</plist>
|
||||||
@@ -2,16 +2,21 @@
|
|||||||
"$schema": "../gen/schemas/desktop-schema.json",
|
"$schema": "../gen/schemas/desktop-schema.json",
|
||||||
"identifier": "default",
|
"identifier": "default",
|
||||||
"description": "HelixNotes default capabilities",
|
"description": "HelixNotes default capabilities",
|
||||||
"windows": ["main"],
|
"windows": ["main", "note-*"],
|
||||||
"permissions": [
|
"permissions": [
|
||||||
"core:default",
|
"core:default",
|
||||||
"core:window:default",
|
"core:window:default",
|
||||||
"core:window:allow-start-dragging",
|
"core:window:allow-start-dragging",
|
||||||
|
"core:window:allow-start-resize-dragging",
|
||||||
"core:window:allow-minimize",
|
"core:window:allow-minimize",
|
||||||
"core:window:allow-toggle-maximize",
|
"core:window:allow-toggle-maximize",
|
||||||
"core:window:allow-close",
|
"core:window:allow-close",
|
||||||
"core:window:allow-is-maximized",
|
"core:window:allow-is-maximized",
|
||||||
"core:window:allow-set-decorations",
|
"core:window:allow-set-decorations",
|
||||||
|
"core:window:allow-is-fullscreen",
|
||||||
|
"core:window:allow-set-fullscreen",
|
||||||
|
"core:window:allow-set-title",
|
||||||
|
"core:webview:allow-create-webview-window",
|
||||||
"dialog:default",
|
"dialog:default",
|
||||||
"dialog:allow-open",
|
"dialog:allow-open",
|
||||||
"dialog:allow-save",
|
"dialog:allow-save",
|
||||||
@@ -44,7 +49,6 @@
|
|||||||
{ "url": "mailto:*" }
|
{ "url": "mailto:*" }
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
"opener:allow-reveal-item-in-dir",
|
"opener:allow-reveal-item-in-dir"
|
||||||
"updater:default"
|
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,12 @@
|
|||||||
|
{
|
||||||
|
"$schema": "../gen/schemas/desktop-schema.json",
|
||||||
|
"identifier": "desktop",
|
||||||
|
"description": "Desktop-only capabilities",
|
||||||
|
"platforms": ["linux", "macOS", "windows"],
|
||||||
|
"windows": ["main"],
|
||||||
|
"permissions": [
|
||||||
|
"updater:default",
|
||||||
|
"window-state:default",
|
||||||
|
"core:webview:allow-set-webview-zoom"
|
||||||
|
]
|
||||||
|
}
|
||||||
@@ -0,0 +1,19 @@
|
|||||||
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
|
||||||
|
<plist version="1.0">
|
||||||
|
<dict>
|
||||||
|
<!-- Hardened runtime entitlements for Developer ID distribution (outside Mac App Store).
|
||||||
|
Required for notarization. These do NOT enable App Sandbox.
|
||||||
|
Without these, the WebKit webview and bundled Rust libraries get killed by the
|
||||||
|
hardened runtime's library validation. -->
|
||||||
|
<key>com.apple.security.cs.allow-jit</key>
|
||||||
|
<true/>
|
||||||
|
<key>com.apple.security.cs.allow-unsigned-executable-memory</key>
|
||||||
|
<true/>
|
||||||
|
<key>com.apple.security.cs.disable-library-validation</key>
|
||||||
|
<true/>
|
||||||
|
<!-- Network access for auto-updater, AI features, WebDAV sync, image proxy -->
|
||||||
|
<key>com.apple.security.network.client</key>
|
||||||
|
<true/>
|
||||||
|
</dict>
|
||||||
|
</plist>
|
||||||
@@ -0,0 +1,12 @@
|
|||||||
|
# EditorConfig is awesome: https://EditorConfig.org
|
||||||
|
|
||||||
|
# top-most EditorConfig file
|
||||||
|
root = true
|
||||||
|
|
||||||
|
[*]
|
||||||
|
indent_style = space
|
||||||
|
indent_size = 2
|
||||||
|
end_of_line = lf
|
||||||
|
charset = utf-8
|
||||||
|
trim_trailing_whitespace = false
|
||||||
|
insert_final_newline = false
|
||||||
@@ -0,0 +1,19 @@
|
|||||||
|
*.iml
|
||||||
|
.gradle
|
||||||
|
/local.properties
|
||||||
|
/.idea/caches
|
||||||
|
/.idea/libraries
|
||||||
|
/.idea/modules.xml
|
||||||
|
/.idea/workspace.xml
|
||||||
|
/.idea/navEditor.xml
|
||||||
|
/.idea/assetWizardSettings.xml
|
||||||
|
.DS_Store
|
||||||
|
build
|
||||||
|
/captures
|
||||||
|
.externalNativeBuild
|
||||||
|
.cxx
|
||||||
|
local.properties
|
||||||
|
key.properties
|
||||||
|
|
||||||
|
/.tauri
|
||||||
|
/tauri.settings.gradle
|
||||||
@@ -0,0 +1,6 @@
|
|||||||
|
/src/main/java/com/helixnotes/app/generated
|
||||||
|
/src/main/jniLibs/**/*.so
|
||||||
|
/src/main/assets/tauri.conf.json
|
||||||
|
/tauri.build.gradle.kts
|
||||||
|
/proguard-tauri.pro
|
||||||
|
/tauri.properties
|
||||||
@@ -0,0 +1,92 @@
|
|||||||
|
import java.util.Properties
|
||||||
|
|
||||||
|
plugins {
|
||||||
|
id("com.android.application")
|
||||||
|
id("org.jetbrains.kotlin.android")
|
||||||
|
id("rust")
|
||||||
|
}
|
||||||
|
|
||||||
|
val tauriProperties = Properties().apply {
|
||||||
|
val propFile = file("tauri.properties")
|
||||||
|
if (propFile.exists()) {
|
||||||
|
propFile.inputStream().use { load(it) }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
val keystoreProperties = Properties().apply {
|
||||||
|
val propFile = rootProject.file("keystore.properties")
|
||||||
|
if (propFile.exists()) {
|
||||||
|
propFile.inputStream().use { load(it) }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
android {
|
||||||
|
compileSdk = 36
|
||||||
|
namespace = "com.helixnotes.app"
|
||||||
|
|
||||||
|
if (keystoreProperties.containsKey("storeFile")) {
|
||||||
|
signingConfigs {
|
||||||
|
create("release") {
|
||||||
|
storeFile = file(keystoreProperties["storeFile"] as String)
|
||||||
|
storePassword = keystoreProperties["storePassword"] as String
|
||||||
|
keyAlias = keystoreProperties["keyAlias"] as String
|
||||||
|
keyPassword = keystoreProperties["keyPassword"] as String
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
defaultConfig {
|
||||||
|
manifestPlaceholders["usesCleartextTraffic"] = "false"
|
||||||
|
applicationId = "com.helixnotes.app"
|
||||||
|
minSdk = 24
|
||||||
|
targetSdk = 36
|
||||||
|
versionCode = tauriProperties.getProperty("tauri.android.versionCode", "1").toInt()
|
||||||
|
versionName = tauriProperties.getProperty("tauri.android.versionName", "1.0")
|
||||||
|
}
|
||||||
|
buildTypes {
|
||||||
|
getByName("debug") {
|
||||||
|
manifestPlaceholders["usesCleartextTraffic"] = "true"
|
||||||
|
isDebuggable = true
|
||||||
|
isJniDebuggable = true
|
||||||
|
isMinifyEnabled = false
|
||||||
|
packaging { jniLibs.keepDebugSymbols.add("*/arm64-v8a/*.so")
|
||||||
|
jniLibs.keepDebugSymbols.add("*/armeabi-v7a/*.so")
|
||||||
|
jniLibs.keepDebugSymbols.add("*/x86/*.so")
|
||||||
|
jniLibs.keepDebugSymbols.add("*/x86_64/*.so")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
getByName("release") {
|
||||||
|
isMinifyEnabled = true
|
||||||
|
proguardFiles(
|
||||||
|
*fileTree(".") { include("**/*.pro") }
|
||||||
|
.plus(getDefaultProguardFile("proguard-android-optimize.txt"))
|
||||||
|
.toList().toTypedArray()
|
||||||
|
)
|
||||||
|
if (signingConfigs.names.contains("release")) {
|
||||||
|
signingConfig = signingConfigs.getByName("release")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
kotlinOptions {
|
||||||
|
jvmTarget = "1.8"
|
||||||
|
}
|
||||||
|
buildFeatures {
|
||||||
|
buildConfig = true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
rust {
|
||||||
|
rootDirRel = "../../../"
|
||||||
|
}
|
||||||
|
|
||||||
|
dependencies {
|
||||||
|
implementation("androidx.webkit:webkit:1.14.0")
|
||||||
|
implementation("androidx.appcompat:appcompat:1.7.1")
|
||||||
|
implementation("androidx.activity:activity-ktx:1.10.1")
|
||||||
|
implementation("com.google.android.material:material:1.12.0")
|
||||||
|
testImplementation("junit:junit:4.13.2")
|
||||||
|
androidTestImplementation("androidx.test.ext:junit:1.1.4")
|
||||||
|
androidTestImplementation("androidx.test.espresso:espresso-core:3.5.0")
|
||||||
|
}
|
||||||
|
|
||||||
|
apply(from = "tauri.build.gradle.kts")
|
||||||
@@ -0,0 +1,22 @@
|
|||||||
|
# Add project specific ProGuard rules here.
|
||||||
|
# You can control the set of applied configuration files using the
|
||||||
|
# proguardFiles setting in build.gradle.
|
||||||
|
#
|
||||||
|
# For more details, see
|
||||||
|
# http://developer.android.com/guide/developing/tools/proguard.html
|
||||||
|
|
||||||
|
# Preserve JS interface annotations
|
||||||
|
-keepattributes JavascriptInterface
|
||||||
|
|
||||||
|
# Keep our custom WebView JS bridge and all its public methods
|
||||||
|
-keep class com.helixnotes.app.StorageBridge {
|
||||||
|
public *;
|
||||||
|
}
|
||||||
|
|
||||||
|
# Keep MainActivity public methods (called from StorageBridge)
|
||||||
|
-keep class com.helixnotes.app.MainActivity {
|
||||||
|
public *;
|
||||||
|
}
|
||||||
|
|
||||||
|
# Ignore missing java.beans classes (referenced by Jackson but not available on Android)
|
||||||
|
-dontwarn java.beans.**
|
||||||
@@ -0,0 +1,42 @@
|
|||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<manifest xmlns:android="http://schemas.android.com/apk/res/android">
|
||||||
|
<uses-permission android:name="android.permission.INTERNET" />
|
||||||
|
<uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE" />
|
||||||
|
<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" />
|
||||||
|
<uses-permission android:name="android.permission.MANAGE_EXTERNAL_STORAGE" />
|
||||||
|
|
||||||
|
<!-- AndroidTV support -->
|
||||||
|
<uses-feature android:name="android.software.leanback" android:required="false" />
|
||||||
|
|
||||||
|
<application
|
||||||
|
android:icon="@mipmap/ic_launcher"
|
||||||
|
android:label="@string/app_name"
|
||||||
|
android:requestLegacyExternalStorage="true"
|
||||||
|
android:theme="@style/Theme.helixnotes"
|
||||||
|
android:usesCleartextTraffic="${usesCleartextTraffic}">
|
||||||
|
<activity
|
||||||
|
android:configChanges="orientation|keyboardHidden|keyboard|screenSize|locale|smallestScreenSize|screenLayout|uiMode"
|
||||||
|
android:launchMode="singleTask"
|
||||||
|
android:label="@string/main_activity_title"
|
||||||
|
android:name=".MainActivity"
|
||||||
|
android:windowSoftInputMode="adjustResize"
|
||||||
|
android:exported="true">
|
||||||
|
<intent-filter>
|
||||||
|
<action android:name="android.intent.action.MAIN" />
|
||||||
|
<category android:name="android.intent.category.LAUNCHER" />
|
||||||
|
<!-- AndroidTV support -->
|
||||||
|
<category android:name="android.intent.category.LEANBACK_LAUNCHER" />
|
||||||
|
</intent-filter>
|
||||||
|
</activity>
|
||||||
|
|
||||||
|
<provider
|
||||||
|
android:name="androidx.core.content.FileProvider"
|
||||||
|
android:authorities="${applicationId}.fileprovider"
|
||||||
|
android:exported="false"
|
||||||
|
android:grantUriPermissions="true">
|
||||||
|
<meta-data
|
||||||
|
android:name="android.support.FILE_PROVIDER_PATHS"
|
||||||
|
android:resource="@xml/file_paths" />
|
||||||
|
</provider>
|
||||||
|
</application>
|
||||||
|
</manifest>
|
||||||
@@ -0,0 +1,157 @@
|
|||||||
|
package com.helixnotes.app
|
||||||
|
|
||||||
|
import android.Manifest
|
||||||
|
import android.content.Intent
|
||||||
|
import android.content.pm.PackageManager
|
||||||
|
import android.net.Uri
|
||||||
|
import android.os.Build
|
||||||
|
import android.os.Environment
|
||||||
|
import android.provider.Settings
|
||||||
|
import android.webkit.JavascriptInterface
|
||||||
|
import android.webkit.WebView
|
||||||
|
import android.webkit.MimeTypeMap
|
||||||
|
import androidx.core.app.ActivityCompat
|
||||||
|
import androidx.core.content.ContextCompat
|
||||||
|
import androidx.core.content.FileProvider
|
||||||
|
import java.io.File
|
||||||
|
|
||||||
|
class MainActivity : TauriActivity() {
|
||||||
|
private var webView: WebView? = null
|
||||||
|
|
||||||
|
companion object {
|
||||||
|
private const val STORAGE_PERMISSION_REQUEST_CODE = 1001
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onWebViewCreate(webView: WebView) {
|
||||||
|
super.onWebViewCreate(webView)
|
||||||
|
this.webView = webView
|
||||||
|
webView.addJavascriptInterface(StorageBridge(this), "Android")
|
||||||
|
// Render text at the size CSS specifies, ignoring the device's system font-size
|
||||||
|
// accessibility setting (textZoom is otherwise tied to the system font scale). (#100)
|
||||||
|
webView.settings.textZoom = 100
|
||||||
|
// The actual fix: disable WebView "text autosizing" / font-boosting, which recomputes
|
||||||
|
// paragraph font-size and line-height from viewport heuristics and ignores our CSS (only
|
||||||
|
// size + line-height were affected; font-family was fine). NORMAL turns it off. (#100)
|
||||||
|
webView.settings.layoutAlgorithm = android.webkit.WebSettings.LayoutAlgorithm.NORMAL
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onResume() {
|
||||||
|
super.onResume()
|
||||||
|
if (hasStoragePermission()) {
|
||||||
|
webView?.evaluateJavascript(
|
||||||
|
"window.__storagePermissionGranted && window.__storagePermissionGranted()",
|
||||||
|
null
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fun hasStoragePermission(): Boolean {
|
||||||
|
return if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) {
|
||||||
|
// Android 11+ (API 30+): "All files access" special permission.
|
||||||
|
Environment.isExternalStorageManager()
|
||||||
|
} else {
|
||||||
|
// Android 7-10 (API 24-29): legacy runtime storage permission.
|
||||||
|
ContextCompat.checkSelfPermission(this, Manifest.permission.WRITE_EXTERNAL_STORAGE) ==
|
||||||
|
PackageManager.PERMISSION_GRANTED
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fun requestStoragePermission() {
|
||||||
|
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) {
|
||||||
|
if (Environment.isExternalStorageManager()) return
|
||||||
|
try {
|
||||||
|
val intent = Intent(Settings.ACTION_MANAGE_APP_ALL_FILES_ACCESS_PERMISSION)
|
||||||
|
intent.data = Uri.parse("package:$packageName")
|
||||||
|
startActivity(intent)
|
||||||
|
} catch (e: Exception) {
|
||||||
|
try {
|
||||||
|
val fallback = Intent(Settings.ACTION_MANAGE_ALL_FILES_ACCESS_PERMISSION)
|
||||||
|
startActivity(fallback)
|
||||||
|
} catch (_: Exception) {
|
||||||
|
val details = Intent(Settings.ACTION_APPLICATION_DETAILS_SETTINGS)
|
||||||
|
details.data = Uri.parse("package:$packageName")
|
||||||
|
startActivity(details)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// Android 7-10: request the legacy runtime permission directly.
|
||||||
|
ActivityCompat.requestPermissions(
|
||||||
|
this,
|
||||||
|
arrayOf(
|
||||||
|
Manifest.permission.READ_EXTERNAL_STORAGE,
|
||||||
|
Manifest.permission.WRITE_EXTERNAL_STORAGE
|
||||||
|
),
|
||||||
|
STORAGE_PERMISSION_REQUEST_CODE
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onRequestPermissionsResult(
|
||||||
|
requestCode: Int,
|
||||||
|
permissions: Array<out String>,
|
||||||
|
grantResults: IntArray
|
||||||
|
) {
|
||||||
|
super.onRequestPermissionsResult(requestCode, permissions, grantResults)
|
||||||
|
if (requestCode == STORAGE_PERMISSION_REQUEST_CODE && hasStoragePermission()) {
|
||||||
|
webView?.evaluateJavascript(
|
||||||
|
"window.__storagePermissionGranted && window.__storagePermissionGranted()",
|
||||||
|
null
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fun prepareVaultDir(path: String): Boolean {
|
||||||
|
return try {
|
||||||
|
val dir = File(path)
|
||||||
|
dir.mkdirs()
|
||||||
|
File(dir, ".helixnotes/trash").mkdirs()
|
||||||
|
File(dir, ".helixnotes/attachments").mkdirs()
|
||||||
|
dir.exists() && dir.canWrite()
|
||||||
|
} catch (e: Exception) {
|
||||||
|
false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fun openFile(path: String) {
|
||||||
|
try {
|
||||||
|
val file = File(path)
|
||||||
|
if (!file.exists()) return
|
||||||
|
val uri = FileProvider.getUriForFile(this, "${packageName}.fileprovider", file)
|
||||||
|
val ext = MimeTypeMap.getFileExtensionFromUrl(path)
|
||||||
|
val mime = MimeTypeMap.getSingleton().getMimeTypeFromExtension(ext) ?: "*/*"
|
||||||
|
val intent = Intent(Intent.ACTION_VIEW).apply {
|
||||||
|
setDataAndType(uri, mime)
|
||||||
|
addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION)
|
||||||
|
}
|
||||||
|
startActivity(intent)
|
||||||
|
} catch (e: Exception) {
|
||||||
|
android.util.Log.e("HelixNotes", "Failed to open file: $path", e)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class StorageBridge(private val activity: MainActivity) {
|
||||||
|
@JavascriptInterface
|
||||||
|
fun hasStoragePermission(): Boolean {
|
||||||
|
return activity.hasStoragePermission()
|
||||||
|
}
|
||||||
|
|
||||||
|
@JavascriptInterface
|
||||||
|
fun requestStoragePermission() {
|
||||||
|
activity.runOnUiThread {
|
||||||
|
activity.requestStoragePermission()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@JavascriptInterface
|
||||||
|
fun prepareVaultDir(path: String): Boolean {
|
||||||
|
return activity.prepareVaultDir(path)
|
||||||
|
}
|
||||||
|
|
||||||
|
@JavascriptInterface
|
||||||
|
fun openFile(path: String) {
|
||||||
|
activity.runOnUiThread {
|
||||||
|
activity.openFile(path)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,30 @@
|
|||||||
|
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||||
|
xmlns:aapt="http://schemas.android.com/aapt"
|
||||||
|
android:width="108dp"
|
||||||
|
android:height="108dp"
|
||||||
|
android:viewportWidth="108"
|
||||||
|
android:viewportHeight="108">
|
||||||
|
<path android:pathData="M31,63.928c0,0 6.4,-11 12.1,-13.1c7.2,-2.6 26,-1.4 26,-1.4l38.1,38.1L107,108.928l-32,-1L31,63.928z">
|
||||||
|
<aapt:attr name="android:fillColor">
|
||||||
|
<gradient
|
||||||
|
android:endX="85.84757"
|
||||||
|
android:endY="92.4963"
|
||||||
|
android:startX="42.9492"
|
||||||
|
android:startY="49.59793"
|
||||||
|
android:type="linear">
|
||||||
|
<item
|
||||||
|
android:color="#44000000"
|
||||||
|
android:offset="0.0" />
|
||||||
|
<item
|
||||||
|
android:color="#00000000"
|
||||||
|
android:offset="1.0" />
|
||||||
|
</gradient>
|
||||||
|
</aapt:attr>
|
||||||
|
</path>
|
||||||
|
<path
|
||||||
|
android:fillColor="#FFFFFF"
|
||||||
|
android:fillType="nonZero"
|
||||||
|
android:pathData="M65.3,45.828l3.8,-6.6c0.2,-0.4 0.1,-0.9 -0.3,-1.1c-0.4,-0.2 -0.9,-0.1 -1.1,0.3l-3.9,6.7c-6.3,-2.8 -13.4,-2.8 -19.7,0l-3.9,-6.7c-0.2,-0.4 -0.7,-0.5 -1.1,-0.3C38.8,38.328 38.7,38.828 38.9,39.228l3.8,6.6C36.2,49.428 31.7,56.028 31,63.928h46C76.3,56.028 71.8,49.428 65.3,45.828zM43.4,57.328c-0.8,0 -1.5,-0.5 -1.8,-1.2c-0.3,-0.7 -0.1,-1.5 0.4,-2.1c0.5,-0.5 1.4,-0.7 2.1,-0.4c0.7,0.3 1.2,1 1.2,1.8C45.3,56.528 44.5,57.328 43.4,57.328L43.4,57.328zM64.6,57.328c-0.8,0 -1.5,-0.5 -1.8,-1.2s-0.1,-1.5 0.4,-2.1c0.5,-0.5 1.4,-0.7 2.1,-0.4c0.7,0.3 1.2,1 1.2,1.8C66.5,56.528 65.6,57.328 64.6,57.328L64.6,57.328z"
|
||||||
|
android:strokeWidth="1"
|
||||||
|
android:strokeColor="#00000000" />
|
||||||
|
</vector>
|
||||||
@@ -0,0 +1,10 @@
|
|||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||||
|
android:width="108dp"
|
||||||
|
android:height="108dp"
|
||||||
|
android:viewportWidth="108"
|
||||||
|
android:viewportHeight="108">
|
||||||
|
<path
|
||||||
|
android:fillColor="#5B6ABF"
|
||||||
|
android:pathData="M0,0h108v108h-108z" />
|
||||||
|
</vector>
|
||||||
@@ -0,0 +1,18 @@
|
|||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
|
||||||
|
xmlns:app="http://schemas.android.com/apk/res-auto"
|
||||||
|
xmlns:tools="http://schemas.android.com/tools"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="match_parent"
|
||||||
|
tools:context=".MainActivity">
|
||||||
|
|
||||||
|
<TextView
|
||||||
|
android:layout_width="wrap_content"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:text="Hello World!"
|
||||||
|
app:layout_constraintBottom_toBottomOf="parent"
|
||||||
|
app:layout_constraintLeft_toLeftOf="parent"
|
||||||
|
app:layout_constraintRight_toRightOf="parent"
|
||||||
|
app:layout_constraintTop_toTopOf="parent" />
|
||||||
|
|
||||||
|
</androidx.constraintlayout.widget.ConstraintLayout>
|
||||||
@@ -0,0 +1,5 @@
|
|||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<adaptive-icon xmlns:android="http://schemas.android.com/apk/res/android">
|
||||||
|
<foreground android:drawable="@mipmap/ic_launcher_foreground"/>
|
||||||
|
<background android:drawable="@color/ic_launcher_background"/>
|
||||||
|
</adaptive-icon>
|
||||||
|
After Width: | Height: | Size: 1.6 KiB |
|
After Width: | Height: | Size: 1.6 KiB |
|
After Width: | Height: | Size: 1.3 KiB |
|
After Width: | Height: | Size: 1.5 KiB |
|
After Width: | Height: | Size: 1.0 KiB |
|
After Width: | Height: | Size: 1.4 KiB |
|
After Width: | Height: | Size: 3.3 KiB |
|
After Width: | Height: | Size: 2.2 KiB |
|
After Width: | Height: | Size: 2.5 KiB |
|
After Width: | Height: | Size: 5.4 KiB |
|
After Width: | Height: | Size: 3.5 KiB |
|
After Width: | Height: | Size: 4.0 KiB |
|
After Width: | Height: | Size: 7.1 KiB |
|
After Width: | Height: | Size: 5.2 KiB |
|
After Width: | Height: | Size: 5.4 KiB |
@@ -0,0 +1,8 @@
|
|||||||
|
<resources xmlns:tools="http://schemas.android.com/tools">
|
||||||
|
<!-- Base application theme (dark mode). -->
|
||||||
|
<style name="Theme.helixnotes" parent="Theme.MaterialComponents.DayNight.NoActionBar">
|
||||||
|
<item name="android:windowBackground">#1a1b26</item>
|
||||||
|
<item name="android:navigationBarColor">#1a1b26</item>
|
||||||
|
<item name="android:statusBarColor">#1a1b26</item>
|
||||||
|
</style>
|
||||||
|
</resources>
|
||||||
@@ -0,0 +1,10 @@
|
|||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<resources>
|
||||||
|
<color name="purple_200">#FFBB86FC</color>
|
||||||
|
<color name="purple_500">#FF6200EE</color>
|
||||||
|
<color name="purple_700">#FF3700B3</color>
|
||||||
|
<color name="teal_200">#FF03DAC5</color>
|
||||||
|
<color name="teal_700">#FF018786</color>
|
||||||
|
<color name="black">#FF000000</color>
|
||||||
|
<color name="white">#FFFFFFFF</color>
|
||||||
|
</resources>
|
||||||
@@ -0,0 +1,4 @@
|
|||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<resources>
|
||||||
|
<color name="ic_launcher_background">#626ED4</color>
|
||||||
|
</resources>
|
||||||
@@ -0,0 +1,4 @@
|
|||||||
|
<resources>
|
||||||
|
<string name="app_name">HelixNotes</string>
|
||||||
|
<string name="main_activity_title">HelixNotes</string>
|
||||||
|
</resources>
|
||||||
@@ -0,0 +1,9 @@
|
|||||||
|
<resources xmlns:tools="http://schemas.android.com/tools">
|
||||||
|
<!-- Base application theme (light mode). -->
|
||||||
|
<style name="Theme.helixnotes" parent="Theme.MaterialComponents.DayNight.NoActionBar">
|
||||||
|
<item name="android:windowBackground">#ffffff</item>
|
||||||
|
<item name="android:navigationBarColor">#ffffff</item>
|
||||||
|
<item name="android:statusBarColor">#ffffff</item>
|
||||||
|
<item name="android:windowLightStatusBar">true</item>
|
||||||
|
</style>
|
||||||
|
</resources>
|
||||||
@@ -0,0 +1,5 @@
|
|||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<paths xmlns:android="http://schemas.android.com/apk/res/android">
|
||||||
|
<external-path name="my_images" path="." />
|
||||||
|
<cache-path name="my_cache_images" path="." />
|
||||||
|
</paths>
|
||||||
@@ -0,0 +1,22 @@
|
|||||||
|
buildscript {
|
||||||
|
repositories {
|
||||||
|
google()
|
||||||
|
mavenCentral()
|
||||||
|
}
|
||||||
|
dependencies {
|
||||||
|
classpath("com.android.tools.build:gradle:8.11.0")
|
||||||
|
classpath("org.jetbrains.kotlin:kotlin-gradle-plugin:1.9.25")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
allprojects {
|
||||||
|
repositories {
|
||||||
|
google()
|
||||||
|
mavenCentral()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
tasks.register("clean").configure {
|
||||||
|
delete("build")
|
||||||
|
}
|
||||||
|
|
||||||
@@ -0,0 +1,23 @@
|
|||||||
|
plugins {
|
||||||
|
`kotlin-dsl`
|
||||||
|
}
|
||||||
|
|
||||||
|
gradlePlugin {
|
||||||
|
plugins {
|
||||||
|
create("pluginsForCoolKids") {
|
||||||
|
id = "rust"
|
||||||
|
implementationClass = "RustPlugin"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
repositories {
|
||||||
|
google()
|
||||||
|
mavenCentral()
|
||||||
|
}
|
||||||
|
|
||||||
|
dependencies {
|
||||||
|
compileOnly(gradleApi())
|
||||||
|
implementation("com.android.tools.build:gradle:8.11.0")
|
||||||
|
}
|
||||||
|
|
||||||
@@ -0,0 +1,68 @@
|
|||||||
|
import java.io.File
|
||||||
|
import org.apache.tools.ant.taskdefs.condition.Os
|
||||||
|
import org.gradle.api.DefaultTask
|
||||||
|
import org.gradle.api.GradleException
|
||||||
|
import org.gradle.api.logging.LogLevel
|
||||||
|
import org.gradle.api.tasks.Input
|
||||||
|
import org.gradle.api.tasks.TaskAction
|
||||||
|
|
||||||
|
open class BuildTask : DefaultTask() {
|
||||||
|
@Input
|
||||||
|
var rootDirRel: String? = null
|
||||||
|
@Input
|
||||||
|
var target: String? = null
|
||||||
|
@Input
|
||||||
|
var release: Boolean? = null
|
||||||
|
|
||||||
|
@TaskAction
|
||||||
|
fun assemble() {
|
||||||
|
val executable = """pnpm""";
|
||||||
|
try {
|
||||||
|
runTauriCli(executable)
|
||||||
|
} catch (e: Exception) {
|
||||||
|
if (Os.isFamily(Os.FAMILY_WINDOWS)) {
|
||||||
|
// Try different Windows-specific extensions
|
||||||
|
val fallbacks = listOf(
|
||||||
|
"$executable.exe",
|
||||||
|
"$executable.cmd",
|
||||||
|
"$executable.bat",
|
||||||
|
)
|
||||||
|
|
||||||
|
var lastException: Exception = e
|
||||||
|
for (fallback in fallbacks) {
|
||||||
|
try {
|
||||||
|
runTauriCli(fallback)
|
||||||
|
return
|
||||||
|
} catch (fallbackException: Exception) {
|
||||||
|
lastException = fallbackException
|
||||||
|
}
|
||||||
|
}
|
||||||
|
throw lastException
|
||||||
|
} else {
|
||||||
|
throw e;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fun runTauriCli(executable: String) {
|
||||||
|
val rootDirRel = rootDirRel ?: throw GradleException("rootDirRel cannot be null")
|
||||||
|
val target = target ?: throw GradleException("target cannot be null")
|
||||||
|
val release = release ?: throw GradleException("release cannot be null")
|
||||||
|
val args = listOf("tauri", "android", "android-studio-script");
|
||||||
|
|
||||||
|
project.exec {
|
||||||
|
workingDir(File(project.projectDir, rootDirRel))
|
||||||
|
executable(executable)
|
||||||
|
args(args)
|
||||||
|
if (project.logger.isEnabled(LogLevel.DEBUG)) {
|
||||||
|
args("-vv")
|
||||||
|
} else if (project.logger.isEnabled(LogLevel.INFO)) {
|
||||||
|
args("-v")
|
||||||
|
}
|
||||||
|
if (release) {
|
||||||
|
args("--release")
|
||||||
|
}
|
||||||
|
args(listOf("--target", target))
|
||||||
|
}.assertNormalExitValue()
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,85 @@
|
|||||||
|
import com.android.build.api.dsl.ApplicationExtension
|
||||||
|
import org.gradle.api.DefaultTask
|
||||||
|
import org.gradle.api.Plugin
|
||||||
|
import org.gradle.api.Project
|
||||||
|
import org.gradle.kotlin.dsl.configure
|
||||||
|
import org.gradle.kotlin.dsl.get
|
||||||
|
|
||||||
|
const val TASK_GROUP = "rust"
|
||||||
|
|
||||||
|
open class Config {
|
||||||
|
lateinit var rootDirRel: String
|
||||||
|
}
|
||||||
|
|
||||||
|
open class RustPlugin : Plugin<Project> {
|
||||||
|
private lateinit var config: Config
|
||||||
|
|
||||||
|
override fun apply(project: Project) = with(project) {
|
||||||
|
config = extensions.create("rust", Config::class.java)
|
||||||
|
|
||||||
|
val defaultAbiList = listOf("arm64-v8a", "armeabi-v7a", "x86", "x86_64");
|
||||||
|
val abiList = (findProperty("abiList") as? String)?.split(',') ?: defaultAbiList
|
||||||
|
|
||||||
|
val defaultArchList = listOf("arm64", "arm", "x86", "x86_64");
|
||||||
|
val archList = (findProperty("archList") as? String)?.split(',') ?: defaultArchList
|
||||||
|
|
||||||
|
val targetsList = (findProperty("targetList") as? String)?.split(',') ?: listOf("aarch64", "armv7", "i686", "x86_64")
|
||||||
|
|
||||||
|
extensions.configure<ApplicationExtension> {
|
||||||
|
@Suppress("UnstableApiUsage")
|
||||||
|
flavorDimensions.add("abi")
|
||||||
|
productFlavors {
|
||||||
|
create("universal") {
|
||||||
|
dimension = "abi"
|
||||||
|
ndk {
|
||||||
|
abiFilters += abiList
|
||||||
|
}
|
||||||
|
}
|
||||||
|
defaultArchList.forEachIndexed { index, arch ->
|
||||||
|
create(arch) {
|
||||||
|
dimension = "abi"
|
||||||
|
ndk {
|
||||||
|
abiFilters.add(defaultAbiList[index])
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
afterEvaluate {
|
||||||
|
for (profile in listOf("debug", "release")) {
|
||||||
|
val profileCapitalized = profile.replaceFirstChar { it.uppercase() }
|
||||||
|
val buildTask = tasks.maybeCreate(
|
||||||
|
"rustBuildUniversal$profileCapitalized",
|
||||||
|
DefaultTask::class.java
|
||||||
|
).apply {
|
||||||
|
group = TASK_GROUP
|
||||||
|
description = "Build dynamic library in $profile mode for all targets"
|
||||||
|
}
|
||||||
|
|
||||||
|
tasks["mergeUniversal${profileCapitalized}JniLibFolders"].dependsOn(buildTask)
|
||||||
|
|
||||||
|
for (targetPair in targetsList.withIndex()) {
|
||||||
|
val targetName = targetPair.value
|
||||||
|
val targetArch = archList[targetPair.index]
|
||||||
|
val targetArchCapitalized = targetArch.replaceFirstChar { it.uppercase() }
|
||||||
|
val targetBuildTask = project.tasks.maybeCreate(
|
||||||
|
"rustBuild$targetArchCapitalized$profileCapitalized",
|
||||||
|
BuildTask::class.java
|
||||||
|
).apply {
|
||||||
|
group = TASK_GROUP
|
||||||
|
description = "Build dynamic library in $profile mode for $targetArch"
|
||||||
|
rootDirRel = config.rootDirRel
|
||||||
|
target = targetName
|
||||||
|
release = profile == "release"
|
||||||
|
}
|
||||||
|
|
||||||
|
buildTask.dependsOn(targetBuildTask)
|
||||||
|
tasks["merge$targetArchCapitalized${profileCapitalized}JniLibFolders"].dependsOn(
|
||||||
|
targetBuildTask
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,24 @@
|
|||||||
|
# Project-wide Gradle settings.
|
||||||
|
# IDE (e.g. Android Studio) users:
|
||||||
|
# Gradle settings configured through the IDE *will override*
|
||||||
|
# any settings specified in this file.
|
||||||
|
# For more details on how to configure your build environment visit
|
||||||
|
# http://www.gradle.org/docs/current/userguide/build_environment.html
|
||||||
|
# Specifies the JVM arguments used for the daemon process.
|
||||||
|
# The setting is particularly useful for tweaking memory settings.
|
||||||
|
org.gradle.jvmargs=-Xmx2048m -Dfile.encoding=UTF-8
|
||||||
|
# When configured, Gradle will run in incubating parallel mode.
|
||||||
|
# This option should only be used with decoupled projects. More details, visit
|
||||||
|
# http://www.gradle.org/docs/current/userguide/multi_project_builds.html#sec:decoupled_projects
|
||||||
|
# org.gradle.parallel=true
|
||||||
|
# AndroidX package structure to make it clearer which packages are bundled with the
|
||||||
|
# Android operating system, and which are packaged with your app"s APK
|
||||||
|
# https://developer.android.com/topic/libraries/support-library/androidx-rn
|
||||||
|
android.useAndroidX=true
|
||||||
|
# Kotlin code style for this project: "official" or "obsolete":
|
||||||
|
kotlin.code.style=official
|
||||||
|
# Enables namespacing of each library's R class so that its R class includes only the
|
||||||
|
# resources declared in the library itself and none from the library's dependencies,
|
||||||
|
# thereby reducing the size of the R class for that library
|
||||||
|
android.nonTransitiveRClass=true
|
||||||
|
android.nonFinalResIds=false
|
||||||
@@ -0,0 +1,6 @@
|
|||||||
|
#Tue May 10 19:22:52 CST 2022
|
||||||
|
distributionBase=GRADLE_USER_HOME
|
||||||
|
distributionUrl=https\://services.gradle.org/distributions/gradle-8.14.3-bin.zip
|
||||||
|
distributionPath=wrapper/dists
|
||||||
|
zipStorePath=wrapper/dists
|
||||||
|
zipStoreBase=GRADLE_USER_HOME
|
||||||
@@ -0,0 +1,185 @@
|
|||||||
|
#!/usr/bin/env sh
|
||||||
|
|
||||||
|
#
|
||||||
|
# Copyright 2015 the original author or authors.
|
||||||
|
#
|
||||||
|
# Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
# you may not use this file except in compliance with the License.
|
||||||
|
# You may obtain a copy of the License at
|
||||||
|
#
|
||||||
|
# https://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
#
|
||||||
|
# Unless required by applicable law or agreed to in writing, software
|
||||||
|
# distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
# See the License for the specific language governing permissions and
|
||||||
|
# limitations under the License.
|
||||||
|
#
|
||||||
|
|
||||||
|
##############################################################################
|
||||||
|
##
|
||||||
|
## Gradle start up script for UN*X
|
||||||
|
##
|
||||||
|
##############################################################################
|
||||||
|
|
||||||
|
# Attempt to set APP_HOME
|
||||||
|
# Resolve links: $0 may be a link
|
||||||
|
PRG="$0"
|
||||||
|
# Need this for relative symlinks.
|
||||||
|
while [ -h "$PRG" ] ; do
|
||||||
|
ls=`ls -ld "$PRG"`
|
||||||
|
link=`expr "$ls" : '.*-> \(.*\)$'`
|
||||||
|
if expr "$link" : '/.*' > /dev/null; then
|
||||||
|
PRG="$link"
|
||||||
|
else
|
||||||
|
PRG=`dirname "$PRG"`"/$link"
|
||||||
|
fi
|
||||||
|
done
|
||||||
|
SAVED="`pwd`"
|
||||||
|
cd "`dirname \"$PRG\"`/" >/dev/null
|
||||||
|
APP_HOME="`pwd -P`"
|
||||||
|
cd "$SAVED" >/dev/null
|
||||||
|
|
||||||
|
APP_NAME="Gradle"
|
||||||
|
APP_BASE_NAME=`basename "$0"`
|
||||||
|
|
||||||
|
# Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
|
||||||
|
DEFAULT_JVM_OPTS='"-Xmx64m" "-Xms64m"'
|
||||||
|
|
||||||
|
# Use the maximum available, or set MAX_FD != -1 to use that value.
|
||||||
|
MAX_FD="maximum"
|
||||||
|
|
||||||
|
warn () {
|
||||||
|
echo "$*"
|
||||||
|
}
|
||||||
|
|
||||||
|
die () {
|
||||||
|
echo
|
||||||
|
echo "$*"
|
||||||
|
echo
|
||||||
|
exit 1
|
||||||
|
}
|
||||||
|
|
||||||
|
# OS specific support (must be 'true' or 'false').
|
||||||
|
cygwin=false
|
||||||
|
msys=false
|
||||||
|
darwin=false
|
||||||
|
nonstop=false
|
||||||
|
case "`uname`" in
|
||||||
|
CYGWIN* )
|
||||||
|
cygwin=true
|
||||||
|
;;
|
||||||
|
Darwin* )
|
||||||
|
darwin=true
|
||||||
|
;;
|
||||||
|
MINGW* )
|
||||||
|
msys=true
|
||||||
|
;;
|
||||||
|
NONSTOP* )
|
||||||
|
nonstop=true
|
||||||
|
;;
|
||||||
|
esac
|
||||||
|
|
||||||
|
CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar
|
||||||
|
|
||||||
|
|
||||||
|
# Determine the Java command to use to start the JVM.
|
||||||
|
if [ -n "$JAVA_HOME" ] ; then
|
||||||
|
if [ -x "$JAVA_HOME/jre/sh/java" ] ; then
|
||||||
|
# IBM's JDK on AIX uses strange locations for the executables
|
||||||
|
JAVACMD="$JAVA_HOME/jre/sh/java"
|
||||||
|
else
|
||||||
|
JAVACMD="$JAVA_HOME/bin/java"
|
||||||
|
fi
|
||||||
|
if [ ! -x "$JAVACMD" ] ; then
|
||||||
|
die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME
|
||||||
|
|
||||||
|
Please set the JAVA_HOME variable in your environment to match the
|
||||||
|
location of your Java installation."
|
||||||
|
fi
|
||||||
|
else
|
||||||
|
JAVACMD="java"
|
||||||
|
which java >/dev/null 2>&1 || die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.
|
||||||
|
|
||||||
|
Please set the JAVA_HOME variable in your environment to match the
|
||||||
|
location of your Java installation."
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Increase the maximum file descriptors if we can.
|
||||||
|
if [ "$cygwin" = "false" -a "$darwin" = "false" -a "$nonstop" = "false" ] ; then
|
||||||
|
MAX_FD_LIMIT=`ulimit -H -n`
|
||||||
|
if [ $? -eq 0 ] ; then
|
||||||
|
if [ "$MAX_FD" = "maximum" -o "$MAX_FD" = "max" ] ; then
|
||||||
|
MAX_FD="$MAX_FD_LIMIT"
|
||||||
|
fi
|
||||||
|
ulimit -n $MAX_FD
|
||||||
|
if [ $? -ne 0 ] ; then
|
||||||
|
warn "Could not set maximum file descriptor limit: $MAX_FD"
|
||||||
|
fi
|
||||||
|
else
|
||||||
|
warn "Could not query maximum file descriptor limit: $MAX_FD_LIMIT"
|
||||||
|
fi
|
||||||
|
fi
|
||||||
|
|
||||||
|
# For Darwin, add options to specify how the application appears in the dock
|
||||||
|
if $darwin; then
|
||||||
|
GRADLE_OPTS="$GRADLE_OPTS \"-Xdock:name=$APP_NAME\" \"-Xdock:icon=$APP_HOME/media/gradle.icns\""
|
||||||
|
fi
|
||||||
|
|
||||||
|
# For Cygwin or MSYS, switch paths to Windows format before running java
|
||||||
|
if [ "$cygwin" = "true" -o "$msys" = "true" ] ; then
|
||||||
|
APP_HOME=`cygpath --path --mixed "$APP_HOME"`
|
||||||
|
CLASSPATH=`cygpath --path --mixed "$CLASSPATH"`
|
||||||
|
|
||||||
|
JAVACMD=`cygpath --unix "$JAVACMD"`
|
||||||
|
|
||||||
|
# We build the pattern for arguments to be converted via cygpath
|
||||||
|
ROOTDIRSRAW=`find -L / -maxdepth 1 -mindepth 1 -type d 2>/dev/null`
|
||||||
|
SEP=""
|
||||||
|
for dir in $ROOTDIRSRAW ; do
|
||||||
|
ROOTDIRS="$ROOTDIRS$SEP$dir"
|
||||||
|
SEP="|"
|
||||||
|
done
|
||||||
|
OURCYGPATTERN="(^($ROOTDIRS))"
|
||||||
|
# Add a user-defined pattern to the cygpath arguments
|
||||||
|
if [ "$GRADLE_CYGPATTERN" != "" ] ; then
|
||||||
|
OURCYGPATTERN="$OURCYGPATTERN|($GRADLE_CYGPATTERN)"
|
||||||
|
fi
|
||||||
|
# Now convert the arguments - kludge to limit ourselves to /bin/sh
|
||||||
|
i=0
|
||||||
|
for arg in "$@" ; do
|
||||||
|
CHECK=`echo "$arg"|egrep -c "$OURCYGPATTERN" -`
|
||||||
|
CHECK2=`echo "$arg"|egrep -c "^-"` ### Determine if an option
|
||||||
|
|
||||||
|
if [ $CHECK -ne 0 ] && [ $CHECK2 -eq 0 ] ; then ### Added a condition
|
||||||
|
eval `echo args$i`=`cygpath --path --ignore --mixed "$arg"`
|
||||||
|
else
|
||||||
|
eval `echo args$i`="\"$arg\""
|
||||||
|
fi
|
||||||
|
i=`expr $i + 1`
|
||||||
|
done
|
||||||
|
case $i in
|
||||||
|
0) set -- ;;
|
||||||
|
1) set -- "$args0" ;;
|
||||||
|
2) set -- "$args0" "$args1" ;;
|
||||||
|
3) set -- "$args0" "$args1" "$args2" ;;
|
||||||
|
4) set -- "$args0" "$args1" "$args2" "$args3" ;;
|
||||||
|
5) set -- "$args0" "$args1" "$args2" "$args3" "$args4" ;;
|
||||||
|
6) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" ;;
|
||||||
|
7) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" ;;
|
||||||
|
8) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" ;;
|
||||||
|
9) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" "$args8" ;;
|
||||||
|
esac
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Escape application args
|
||||||
|
save () {
|
||||||
|
for i do printf %s\\n "$i" | sed "s/'/'\\\\''/g;1s/^/'/;\$s/\$/' \\\\/" ; done
|
||||||
|
echo " "
|
||||||
|
}
|
||||||
|
APP_ARGS=`save "$@"`
|
||||||
|
|
||||||
|
# Collect all arguments for the java command, following the shell quoting and substitution rules
|
||||||
|
eval set -- $DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS "\"-Dorg.gradle.appname=$APP_BASE_NAME\"" -classpath "\"$CLASSPATH\"" org.gradle.wrapper.GradleWrapperMain "$APP_ARGS"
|
||||||
|
|
||||||
|
exec "$JAVACMD" "$@"
|
||||||
@@ -0,0 +1,89 @@
|
|||||||
|
@rem
|
||||||
|
@rem Copyright 2015 the original author or authors.
|
||||||
|
@rem
|
||||||
|
@rem Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
@rem you may not use this file except in compliance with the License.
|
||||||
|
@rem You may obtain a copy of the License at
|
||||||
|
@rem
|
||||||
|
@rem https://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
@rem
|
||||||
|
@rem Unless required by applicable law or agreed to in writing, software
|
||||||
|
@rem distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
@rem WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
@rem See the License for the specific language governing permissions and
|
||||||
|
@rem limitations under the License.
|
||||||
|
@rem
|
||||||
|
|
||||||
|
@if "%DEBUG%" == "" @echo off
|
||||||
|
@rem ##########################################################################
|
||||||
|
@rem
|
||||||
|
@rem Gradle startup script for Windows
|
||||||
|
@rem
|
||||||
|
@rem ##########################################################################
|
||||||
|
|
||||||
|
@rem Set local scope for the variables with windows NT shell
|
||||||
|
if "%OS%"=="Windows_NT" setlocal
|
||||||
|
|
||||||
|
set DIRNAME=%~dp0
|
||||||
|
if "%DIRNAME%" == "" set DIRNAME=.
|
||||||
|
set APP_BASE_NAME=%~n0
|
||||||
|
set APP_HOME=%DIRNAME%
|
||||||
|
|
||||||
|
@rem Resolve any "." and ".." in APP_HOME to make it shorter.
|
||||||
|
for %%i in ("%APP_HOME%") do set APP_HOME=%%~fi
|
||||||
|
|
||||||
|
@rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
|
||||||
|
set DEFAULT_JVM_OPTS="-Xmx64m" "-Xms64m"
|
||||||
|
|
||||||
|
@rem Find java.exe
|
||||||
|
if defined JAVA_HOME goto findJavaFromJavaHome
|
||||||
|
|
||||||
|
set JAVA_EXE=java.exe
|
||||||
|
%JAVA_EXE% -version >NUL 2>&1
|
||||||
|
if "%ERRORLEVEL%" == "0" goto execute
|
||||||
|
|
||||||
|
echo.
|
||||||
|
echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.
|
||||||
|
echo.
|
||||||
|
echo Please set the JAVA_HOME variable in your environment to match the
|
||||||
|
echo location of your Java installation.
|
||||||
|
|
||||||
|
goto fail
|
||||||
|
|
||||||
|
:findJavaFromJavaHome
|
||||||
|
set JAVA_HOME=%JAVA_HOME:"=%
|
||||||
|
set JAVA_EXE=%JAVA_HOME%/bin/java.exe
|
||||||
|
|
||||||
|
if exist "%JAVA_EXE%" goto execute
|
||||||
|
|
||||||
|
echo.
|
||||||
|
echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME%
|
||||||
|
echo.
|
||||||
|
echo Please set the JAVA_HOME variable in your environment to match the
|
||||||
|
echo location of your Java installation.
|
||||||
|
|
||||||
|
goto fail
|
||||||
|
|
||||||
|
:execute
|
||||||
|
@rem Setup the command line
|
||||||
|
|
||||||
|
set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar
|
||||||
|
|
||||||
|
|
||||||
|
@rem Execute Gradle
|
||||||
|
"%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %*
|
||||||
|
|
||||||
|
:end
|
||||||
|
@rem End local scope for the variables with windows NT shell
|
||||||
|
if "%ERRORLEVEL%"=="0" goto mainEnd
|
||||||
|
|
||||||
|
:fail
|
||||||
|
rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of
|
||||||
|
rem the _cmd.exe /c_ return code!
|
||||||
|
if not "" == "%GRADLE_EXIT_CONSOLE%" exit 1
|
||||||
|
exit /b 1
|
||||||
|
|
||||||
|
:mainEnd
|
||||||
|
if "%OS%"=="Windows_NT" endlocal
|
||||||
|
|
||||||
|
:omega
|
||||||
@@ -0,0 +1,3 @@
|
|||||||
|
include ':app'
|
||||||
|
|
||||||
|
apply from: 'tauri.settings.gradle'
|
||||||
|
Before Width: | Height: | Size: 4.0 KiB After Width: | Height: | Size: 2.4 KiB |
|
Before Width: | Height: | Size: 8.3 KiB After Width: | Height: | Size: 4.4 KiB |
|
Before Width: | Height: | Size: 953 B After Width: | Height: | Size: 709 B |
|
After Width: | Height: | Size: 1.3 KiB |
|
Before Width: | Height: | Size: 3.5 KiB After Width: | Height: | Size: 1.9 KiB |
|
Before Width: | Height: | Size: 4.5 KiB After Width: | Height: | Size: 2.7 KiB |
|
Before Width: | Height: | Size: 4.0 KiB After Width: | Height: | Size: 2.9 KiB |
|
Before Width: | Height: | Size: 9.4 KiB After Width: | Height: | Size: 5.2 KiB |
|
Before Width: | Height: | Size: 793 B After Width: | Height: | Size: 556 B |
|
Before Width: | Height: | Size: 10 KiB After Width: | Height: | Size: 5.8 KiB |
|
Before Width: | Height: | Size: 1.3 KiB After Width: | Height: | Size: 961 B |
|
Before Width: | Height: | Size: 2.2 KiB After Width: | Height: | Size: 1.5 KiB |
|
Before Width: | Height: | Size: 2.8 KiB After Width: | Height: | Size: 1.7 KiB |
|
Before Width: | Height: | Size: 1.6 KiB After Width: | Height: | Size: 984 B |
@@ -0,0 +1,11 @@
|
|||||||
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
<svg width="512" height="512" viewBox="0 0 512 512" xmlns="http://www.w3.org/2000/svg">
|
||||||
|
<g transform="translate(85.333 85.333) scale(0.666667)">
|
||||||
|
<line x1="178.45584" y1="178.45584" x2="333.54416" y2="333.54416" stroke="#ffffff" stroke-width="25" stroke-opacity="0.5" stroke-linecap="round"/>
|
||||||
|
<line x1="337.53708" y1="174.46292" x2="174.46294" y2="337.53708" stroke="#ffffff" stroke-width="25" stroke-opacity="0.5" stroke-linecap="round"/>
|
||||||
|
<circle cx="153" cy="153" r="36" fill="#ffffff"/>
|
||||||
|
<circle cx="359" cy="153" r="36" fill="#ffffff"/>
|
||||||
|
<circle cx="153" cy="359" r="36" fill="#ffffff"/>
|
||||||
|
<circle cx="359" cy="359" r="36" fill="#ffffff"/>
|
||||||
|
</g>
|
||||||
|
</svg>
|
||||||
|
After Width: | Height: | Size: 706 B |
|
Before Width: | Height: | Size: 18 KiB After Width: | Height: | Size: 54 KiB |
|
Before Width: | Height: | Size: 4.2 KiB After Width: | Height: | Size: 10 KiB |
|
After Width: | Height: | Size: 16 KiB |
|
Before Width: | Height: | Size: 17 KiB After Width: | Height: | Size: 8.9 KiB |
@@ -0,0 +1,52 @@
|
|||||||
|
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
|
||||||
|
<svg
|
||||||
|
width="512"
|
||||||
|
height="512"
|
||||||
|
viewBox="0 0 512 512"
|
||||||
|
version="1.1"
|
||||||
|
id="svg5"
|
||||||
|
xmlns="http://www.w3.org/2000/svg"><rect
|
||||||
|
width="512"
|
||||||
|
height="512"
|
||||||
|
rx="112"
|
||||||
|
ry="112"
|
||||||
|
fill="#626ED4"
|
||||||
|
id="rect1" /><line
|
||||||
|
x1="178.45584"
|
||||||
|
y1="178.45584"
|
||||||
|
x2="333.54416"
|
||||||
|
y2="333.54416"
|
||||||
|
stroke="#ffffff"
|
||||||
|
stroke-width="25"
|
||||||
|
stroke-opacity="0.5"
|
||||||
|
stroke-linecap="round"
|
||||||
|
id="line1" /><line
|
||||||
|
x1="337.53708"
|
||||||
|
y1="174.46292"
|
||||||
|
x2="174.46294"
|
||||||
|
y2="337.53708"
|
||||||
|
stroke="#ffffff"
|
||||||
|
stroke-width="25"
|
||||||
|
stroke-opacity="0.5"
|
||||||
|
stroke-linecap="round"
|
||||||
|
id="line2" /><circle
|
||||||
|
cx="153"
|
||||||
|
cy="153"
|
||||||
|
r="36"
|
||||||
|
fill="#ffffff"
|
||||||
|
id="circle2" /><circle
|
||||||
|
cx="359"
|
||||||
|
cy="153"
|
||||||
|
r="36"
|
||||||
|
fill="#ffffff"
|
||||||
|
id="circle3" /><circle
|
||||||
|
cx="153"
|
||||||
|
cy="359"
|
||||||
|
r="36"
|
||||||
|
fill="#ffffff"
|
||||||
|
id="circle4" /><circle
|
||||||
|
cx="359"
|
||||||
|
cy="359"
|
||||||
|
r="36"
|
||||||
|
fill="#ffffff"
|
||||||
|
id="circle5" /></svg>
|
||||||
|
After Width: | Height: | Size: 1.0 KiB |
|
After Width: | Height: | Size: 475 B |
|
After Width: | Height: | Size: 788 B |
|
After Width: | Height: | Size: 788 B |
|
After Width: | Height: | Size: 1.1 KiB |
|
After Width: | Height: | Size: 721 B |
|
After Width: | Height: | Size: 1.1 KiB |
|
After Width: | Height: | Size: 1.1 KiB |
|
After Width: | Height: | Size: 1.2 KiB |
|
After Width: | Height: | Size: 788 B |
|
After Width: | Height: | Size: 1.2 KiB |
|
After Width: | Height: | Size: 1.2 KiB |
|
After Width: | Height: | Size: 1.9 KiB |
|
After Width: | Height: | Size: 15 KiB |
|
After Width: | Height: | Size: 1.9 KiB |
|
After Width: | Height: | Size: 2.9 KiB |
|
After Width: | Height: | Size: 1.3 KiB |
|
After Width: | Height: | Size: 2.3 KiB |
|
After Width: | Height: | Size: 2.6 KiB |
@@ -0,0 +1,21 @@
|
|||||||
|
{
|
||||||
|
"themes": [
|
||||||
|
{
|
||||||
|
"colors": {
|
||||||
|
"bg_active": "#373a52",
|
||||||
|
"bg_editor": "#1a1b26",
|
||||||
|
"bg_hover": "#2f3145",
|
||||||
|
"bg_primary": "#1a1b26",
|
||||||
|
"bg_secondary": "#1f2029",
|
||||||
|
"bg_tertiary": "#2a2b3d",
|
||||||
|
"border_color": "#2f3145",
|
||||||
|
"text_primary": "#c8cde4",
|
||||||
|
"text_secondary": "#6b7094"
|
||||||
|
},
|
||||||
|
"id": "custom-1782146306429",
|
||||||
|
"is_dark": true,
|
||||||
|
"name": "My theme"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"version": 1
|
||||||
|
}
|
||||||
@@ -0,0 +1,15 @@
|
|||||||
|
[package]
|
||||||
|
name = "tauri-plugin-ios-vault-access"
|
||||||
|
version = "0.1.0"
|
||||||
|
authors = ["HelixNotes"]
|
||||||
|
description = "iOS Files folder access for HelixNotes"
|
||||||
|
edition = "2021"
|
||||||
|
rust-version = "1.77.2"
|
||||||
|
links = "tauri-plugin-ios-vault-access"
|
||||||
|
|
||||||
|
[dependencies]
|
||||||
|
tauri = "2"
|
||||||
|
serde = { version = "1", features = ["derive"] }
|
||||||
|
|
||||||
|
[build-dependencies]
|
||||||
|
tauri-plugin = { version = "2", features = ["build"] }
|
||||||
@@ -0,0 +1,3 @@
|
|||||||
|
fn main() {
|
||||||
|
tauri_plugin::Builder::new(&[]).ios_path("ios").build();
|
||||||
|
}
|
||||||
@@ -0,0 +1,24 @@
|
|||||||
|
// swift-tools-version:5.3
|
||||||
|
import PackageDescription
|
||||||
|
|
||||||
|
let package = Package(
|
||||||
|
name: "tauri-plugin-ios-vault-access",
|
||||||
|
platforms: [.iOS(.v14)],
|
||||||
|
products: [
|
||||||
|
.library(
|
||||||
|
name: "tauri-plugin-ios-vault-access",
|
||||||
|
type: .static,
|
||||||
|
targets: ["tauri-plugin-ios-vault-access"]
|
||||||
|
)
|
||||||
|
],
|
||||||
|
dependencies: [
|
||||||
|
.package(name: "Tauri", path: "../.tauri/tauri-api")
|
||||||
|
],
|
||||||
|
targets: [
|
||||||
|
.target(
|
||||||
|
name: "tauri-plugin-ios-vault-access",
|
||||||
|
dependencies: [.byName(name: "Tauri")],
|
||||||
|
path: "Sources"
|
||||||
|
)
|
||||||
|
]
|
||||||
|
)
|
||||||
@@ -0,0 +1,312 @@
|
|||||||
|
import Foundation
|
||||||
|
import SwiftRs
|
||||||
|
import Tauri
|
||||||
|
import UIKit
|
||||||
|
import UniformTypeIdentifiers
|
||||||
|
|
||||||
|
private struct BookmarkRequest: Decodable {
|
||||||
|
let bookmarkId: String
|
||||||
|
}
|
||||||
|
|
||||||
|
final class IosVaultAccessPlugin: Plugin, UIDocumentPickerDelegate {
|
||||||
|
private let fileManager = FileManager.default
|
||||||
|
private let lock = NSLock()
|
||||||
|
private var pickerInvoke: Invoke?
|
||||||
|
private var activeBookmarkId: String?
|
||||||
|
private var activeURL: URL?
|
||||||
|
private var stagedBookmarkId: String?
|
||||||
|
private var stagedURL: URL?
|
||||||
|
|
||||||
|
deinit {
|
||||||
|
rollbackStagedScope()
|
||||||
|
releaseCurrentScope()
|
||||||
|
}
|
||||||
|
|
||||||
|
@objc func chooseFolder(_ invoke: Invoke) {
|
||||||
|
DispatchQueue.main.async {
|
||||||
|
guard self.pickerInvoke == nil else {
|
||||||
|
invoke.reject("A folder picker is already open.")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
guard let viewController = self.manager.viewController else {
|
||||||
|
self.pickerInvoke = nil
|
||||||
|
invoke.reject("Files is unavailable right now. Try again.")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
self.pickerInvoke = invoke
|
||||||
|
let picker = UIDocumentPickerViewController(
|
||||||
|
forOpeningContentTypes: [.folder],
|
||||||
|
asCopy: false
|
||||||
|
)
|
||||||
|
picker.delegate = self
|
||||||
|
picker.allowsMultipleSelection = false
|
||||||
|
viewController.present(picker, animated: true)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@objc func resolveFolder(_ invoke: Invoke) throws {
|
||||||
|
let request = try invoke.parseArgs(BookmarkRequest.self)
|
||||||
|
do {
|
||||||
|
invoke.resolve(try resolveBookmark(request.bookmarkId))
|
||||||
|
} catch {
|
||||||
|
invoke.reject(message(for: error))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@objc func releaseActive(_ invoke: Invoke) {
|
||||||
|
rollbackStagedScope()
|
||||||
|
releaseCurrentScope()
|
||||||
|
invoke.resolve()
|
||||||
|
}
|
||||||
|
|
||||||
|
@objc func commitStaged(_ invoke: Invoke) {
|
||||||
|
commitStagedScope()
|
||||||
|
invoke.resolve()
|
||||||
|
}
|
||||||
|
|
||||||
|
@objc func rollbackStaged(_ invoke: Invoke) {
|
||||||
|
rollbackStagedScope()
|
||||||
|
invoke.resolve()
|
||||||
|
}
|
||||||
|
|
||||||
|
@objc func forgetBookmark(_ invoke: Invoke) throws {
|
||||||
|
let request = try invoke.parseArgs(BookmarkRequest.self)
|
||||||
|
do {
|
||||||
|
let file = try bookmarkFile(for: request.bookmarkId, createDirectory: false)
|
||||||
|
lock.lock()
|
||||||
|
let isActive = activeBookmarkId == request.bookmarkId
|
||||||
|
let isStaged = stagedBookmarkId == request.bookmarkId
|
||||||
|
lock.unlock()
|
||||||
|
if fileManager.fileExists(atPath: file.path) {
|
||||||
|
try fileManager.removeItem(at: file)
|
||||||
|
}
|
||||||
|
if isActive {
|
||||||
|
releaseCurrentScope()
|
||||||
|
}
|
||||||
|
if isStaged {
|
||||||
|
rollbackStagedScope()
|
||||||
|
}
|
||||||
|
invoke.resolve()
|
||||||
|
} catch {
|
||||||
|
invoke.reject(message(for: error))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func documentPicker(
|
||||||
|
_ controller: UIDocumentPickerViewController,
|
||||||
|
didPickDocumentsAt urls: [URL]
|
||||||
|
) {
|
||||||
|
guard let invoke = takePickerInvoke() else { return }
|
||||||
|
guard let url = urls.first else {
|
||||||
|
invoke.resolve(cancelledResult())
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
do {
|
||||||
|
guard url.startAccessingSecurityScopedResource() else {
|
||||||
|
throw AccessError.denied
|
||||||
|
}
|
||||||
|
var adopted = false
|
||||||
|
defer {
|
||||||
|
if !adopted { url.stopAccessingSecurityScopedResource() }
|
||||||
|
}
|
||||||
|
|
||||||
|
let name = try validateFolder(url)
|
||||||
|
let bookmarkId = existingBookmarkId(for: url) ?? UUID().uuidString.lowercased()
|
||||||
|
let bookmark = try url.bookmarkData(
|
||||||
|
options: .minimalBookmark,
|
||||||
|
includingResourceValuesForKeys: nil,
|
||||||
|
relativeTo: nil
|
||||||
|
)
|
||||||
|
try bookmark.write(to: bookmarkFile(for: bookmarkId, createDirectory: true), options: .atomic)
|
||||||
|
stageScope(url: url, bookmarkId: bookmarkId)
|
||||||
|
adopted = true
|
||||||
|
invoke.resolve(result(bookmarkId: bookmarkId, url: url, name: name))
|
||||||
|
} catch {
|
||||||
|
invoke.reject(message(for: error))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func documentPickerWasCancelled(_ controller: UIDocumentPickerViewController) {
|
||||||
|
takePickerInvoke()?.resolve(cancelledResult())
|
||||||
|
}
|
||||||
|
|
||||||
|
private func resolveBookmark(_ bookmarkId: String) throws -> [String: Any] {
|
||||||
|
guard UUID(uuidString: bookmarkId) != nil else { throw AccessError.missing }
|
||||||
|
|
||||||
|
lock.lock()
|
||||||
|
let currentURL = activeBookmarkId == bookmarkId ? activeURL : nil
|
||||||
|
lock.unlock()
|
||||||
|
if let currentURL {
|
||||||
|
return result(bookmarkId: bookmarkId, url: currentURL, name: try validateFolder(currentURL))
|
||||||
|
}
|
||||||
|
|
||||||
|
let data = try Data(contentsOf: bookmarkFile(for: bookmarkId, createDirectory: false))
|
||||||
|
var stale = false
|
||||||
|
let url = try URL(
|
||||||
|
resolvingBookmarkData: data,
|
||||||
|
options: [],
|
||||||
|
relativeTo: nil,
|
||||||
|
bookmarkDataIsStale: &stale
|
||||||
|
)
|
||||||
|
guard url.startAccessingSecurityScopedResource() else { throw AccessError.denied }
|
||||||
|
var adopted = false
|
||||||
|
defer {
|
||||||
|
if !adopted { url.stopAccessingSecurityScopedResource() }
|
||||||
|
}
|
||||||
|
|
||||||
|
let name = try validateFolder(url)
|
||||||
|
if stale {
|
||||||
|
let refreshed = try url.bookmarkData(
|
||||||
|
options: .minimalBookmark,
|
||||||
|
includingResourceValuesForKeys: nil,
|
||||||
|
relativeTo: nil
|
||||||
|
)
|
||||||
|
try refreshed.write(to: bookmarkFile(for: bookmarkId, createDirectory: true), options: .atomic)
|
||||||
|
}
|
||||||
|
stageScope(url: url, bookmarkId: bookmarkId)
|
||||||
|
adopted = true
|
||||||
|
return result(bookmarkId: bookmarkId, url: url, name: name)
|
||||||
|
}
|
||||||
|
|
||||||
|
private func validateFolder(_ url: URL) throws -> String {
|
||||||
|
let values = try url.resourceValues(forKeys: [.isDirectoryKey, .nameKey])
|
||||||
|
guard values.isDirectory == true else { throw AccessError.notFolder }
|
||||||
|
guard try url.checkResourceIsReachable() else { throw AccessError.unavailable }
|
||||||
|
return values.name ?? url.lastPathComponent
|
||||||
|
}
|
||||||
|
|
||||||
|
private func stageScope(url: URL, bookmarkId: String) {
|
||||||
|
lock.lock()
|
||||||
|
let previous = stagedURL
|
||||||
|
stagedURL = url
|
||||||
|
stagedBookmarkId = bookmarkId
|
||||||
|
lock.unlock()
|
||||||
|
previous?.stopAccessingSecurityScopedResource()
|
||||||
|
}
|
||||||
|
|
||||||
|
private func commitStagedScope() {
|
||||||
|
lock.lock()
|
||||||
|
guard let next = stagedURL else {
|
||||||
|
lock.unlock()
|
||||||
|
return
|
||||||
|
}
|
||||||
|
let previous = activeURL
|
||||||
|
activeURL = next
|
||||||
|
activeBookmarkId = stagedBookmarkId
|
||||||
|
stagedURL = nil
|
||||||
|
stagedBookmarkId = nil
|
||||||
|
lock.unlock()
|
||||||
|
previous?.stopAccessingSecurityScopedResource()
|
||||||
|
}
|
||||||
|
|
||||||
|
private func rollbackStagedScope() {
|
||||||
|
lock.lock()
|
||||||
|
let staged = stagedURL
|
||||||
|
stagedURL = nil
|
||||||
|
stagedBookmarkId = nil
|
||||||
|
lock.unlock()
|
||||||
|
staged?.stopAccessingSecurityScopedResource()
|
||||||
|
}
|
||||||
|
|
||||||
|
private func releaseCurrentScope() {
|
||||||
|
lock.lock()
|
||||||
|
let previous = activeURL
|
||||||
|
activeURL = nil
|
||||||
|
activeBookmarkId = nil
|
||||||
|
lock.unlock()
|
||||||
|
previous?.stopAccessingSecurityScopedResource()
|
||||||
|
}
|
||||||
|
|
||||||
|
private func takePickerInvoke() -> Invoke? {
|
||||||
|
let invoke = pickerInvoke
|
||||||
|
pickerInvoke = nil
|
||||||
|
return invoke
|
||||||
|
}
|
||||||
|
|
||||||
|
private func bookmarkDirectory(create: Bool) throws -> URL {
|
||||||
|
let base = try fileManager.url(
|
||||||
|
for: .applicationSupportDirectory,
|
||||||
|
in: .userDomainMask,
|
||||||
|
appropriateFor: nil,
|
||||||
|
create: true
|
||||||
|
)
|
||||||
|
let directory = base.appendingPathComponent("VaultBookmarks", isDirectory: true)
|
||||||
|
if create {
|
||||||
|
try fileManager.createDirectory(at: directory, withIntermediateDirectories: true)
|
||||||
|
}
|
||||||
|
return directory
|
||||||
|
}
|
||||||
|
|
||||||
|
private func bookmarkFile(for id: String, createDirectory: Bool) throws -> URL {
|
||||||
|
guard UUID(uuidString: id) != nil else { throw AccessError.missing }
|
||||||
|
let directory = try bookmarkDirectory(create: createDirectory)
|
||||||
|
return directory.appendingPathComponent("\(id).bookmark")
|
||||||
|
}
|
||||||
|
|
||||||
|
private func existingBookmarkId(for selectedURL: URL) -> String? {
|
||||||
|
guard
|
||||||
|
let directory = try? bookmarkDirectory(create: true),
|
||||||
|
let files = try? fileManager.contentsOfDirectory(
|
||||||
|
at: directory,
|
||||||
|
includingPropertiesForKeys: nil
|
||||||
|
)
|
||||||
|
else { return nil }
|
||||||
|
|
||||||
|
let selectedPath = selectedURL.standardizedFileURL.path
|
||||||
|
for file in files where file.pathExtension == "bookmark" {
|
||||||
|
guard let data = try? Data(contentsOf: file) else { continue }
|
||||||
|
var stale = false
|
||||||
|
guard
|
||||||
|
let resolved = try? URL(
|
||||||
|
resolvingBookmarkData: data,
|
||||||
|
options: [],
|
||||||
|
relativeTo: nil,
|
||||||
|
bookmarkDataIsStale: &stale
|
||||||
|
),
|
||||||
|
resolved.standardizedFileURL.path == selectedPath
|
||||||
|
else { continue }
|
||||||
|
let id = file.deletingPathExtension().lastPathComponent
|
||||||
|
if UUID(uuidString: id) != nil { return id }
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
private func result(bookmarkId: String, url: URL, name: String) -> [String: Any] {
|
||||||
|
["bookmarkId": bookmarkId, "path": url.path, "name": name, "cancelled": false]
|
||||||
|
}
|
||||||
|
|
||||||
|
private func cancelledResult() -> [String: Any] {
|
||||||
|
["bookmarkId": "", "path": "", "name": "", "cancelled": true]
|
||||||
|
}
|
||||||
|
|
||||||
|
private func message(for error: Error) -> String {
|
||||||
|
if let accessError = error as? AccessError { return accessError.localizedDescription }
|
||||||
|
return "The selected folder is unavailable. Open Files and try again."
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private enum AccessError: LocalizedError {
|
||||||
|
case denied
|
||||||
|
case missing
|
||||||
|
case notFolder
|
||||||
|
case unavailable
|
||||||
|
|
||||||
|
var errorDescription: String? {
|
||||||
|
switch self {
|
||||||
|
case .denied:
|
||||||
|
return "HelixNotes no longer has access to this folder. Choose it again in Files."
|
||||||
|
case .missing:
|
||||||
|
return "Saved access to this folder is missing. Choose it again in Files."
|
||||||
|
case .notFolder:
|
||||||
|
return "The selected item is not a folder."
|
||||||
|
case .unavailable:
|
||||||
|
return "The selected folder is unavailable. Open Files and try again."
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@_cdecl("init_plugin_ios_vault_access")
|
||||||
|
func initPlugin() -> Plugin {
|
||||||
|
IosVaultAccessPlugin()
|
||||||
|
}
|
||||||
@@ -0,0 +1,84 @@
|
|||||||
|
use serde::{Deserialize, Serialize};
|
||||||
|
use tauri::{
|
||||||
|
plugin::{Builder, PluginHandle, TauriPlugin},
|
||||||
|
Manager, Runtime,
|
||||||
|
};
|
||||||
|
|
||||||
|
#[derive(Debug, Clone, Deserialize)]
|
||||||
|
#[serde(rename_all = "camelCase")]
|
||||||
|
pub struct FolderSelection {
|
||||||
|
pub bookmark_id: String,
|
||||||
|
pub path: String,
|
||||||
|
pub name: String,
|
||||||
|
#[serde(default)]
|
||||||
|
pub cancelled: bool,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Serialize)]
|
||||||
|
#[serde(rename_all = "camelCase")]
|
||||||
|
struct BookmarkRequest<'a> {
|
||||||
|
bookmark_id: &'a str,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(target_os = "ios")]
|
||||||
|
tauri::ios_plugin_binding!(init_plugin_ios_vault_access);
|
||||||
|
|
||||||
|
pub struct IosVaultAccess<R: Runtime>(PluginHandle<R>);
|
||||||
|
|
||||||
|
impl<R: Runtime> IosVaultAccess<R> {
|
||||||
|
pub fn choose_folder(&self) -> Result<FolderSelection, String> {
|
||||||
|
self.0
|
||||||
|
.run_mobile_plugin("chooseFolder", ())
|
||||||
|
.map_err(|error| error.to_string())
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn resolve_folder(&self, bookmark_id: &str) -> Result<FolderSelection, String> {
|
||||||
|
self.0
|
||||||
|
.run_mobile_plugin("resolveFolder", BookmarkRequest { bookmark_id })
|
||||||
|
.map_err(|error| error.to_string())
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn release_active(&self) -> Result<(), String> {
|
||||||
|
self.0
|
||||||
|
.run_mobile_plugin("releaseActive", ())
|
||||||
|
.map_err(|error| error.to_string())
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn commit_staged(&self) -> Result<(), String> {
|
||||||
|
self.0
|
||||||
|
.run_mobile_plugin("commitStaged", ())
|
||||||
|
.map_err(|error| error.to_string())
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn rollback_staged(&self) -> Result<(), String> {
|
||||||
|
self.0
|
||||||
|
.run_mobile_plugin("rollbackStaged", ())
|
||||||
|
.map_err(|error| error.to_string())
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn forget_bookmark(&self, bookmark_id: &str) -> Result<(), String> {
|
||||||
|
self.0
|
||||||
|
.run_mobile_plugin("forgetBookmark", BookmarkRequest { bookmark_id })
|
||||||
|
.map_err(|error| error.to_string())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub trait IosVaultAccessExt<R: Runtime> {
|
||||||
|
fn ios_vault_access(&self) -> &IosVaultAccess<R>;
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<R: Runtime, T: Manager<R>> IosVaultAccessExt<R> for T {
|
||||||
|
fn ios_vault_access(&self) -> &IosVaultAccess<R> {
|
||||||
|
self.state::<IosVaultAccess<R>>().inner()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn init<R: Runtime>() -> TauriPlugin<R> {
|
||||||
|
Builder::new("ios-vault-access")
|
||||||
|
.setup(|app, api| {
|
||||||
|
let handle = api.register_ios_plugin(init_plugin_ios_vault_access)?;
|
||||||
|
app.manage(IosVaultAccess(handle));
|
||||||
|
Ok(())
|
||||||
|
})
|
||||||
|
.build()
|
||||||
|
}
|
||||||
@@ -6,6 +6,7 @@ use crate::types::AiStreamEvent;
|
|||||||
|
|
||||||
const ANTHROPIC_API_URL: &str = "https://api.anthropic.com/v1/messages";
|
const ANTHROPIC_API_URL: &str = "https://api.anthropic.com/v1/messages";
|
||||||
const OPENAI_API_URL: &str = "https://api.openai.com/v1/chat/completions";
|
const OPENAI_API_URL: &str = "https://api.openai.com/v1/chat/completions";
|
||||||
|
const OLLAMA_DEFAULT_URL: &str = "http://localhost:11434";
|
||||||
|
|
||||||
pub fn ai_request(
|
pub fn ai_request(
|
||||||
app: AppHandle,
|
app: AppHandle,
|
||||||
@@ -15,15 +16,19 @@ pub fn ai_request(
|
|||||||
system_prompt: String,
|
system_prompt: String,
|
||||||
user_message: String,
|
user_message: String,
|
||||||
request_id: String,
|
request_id: String,
|
||||||
|
base_url: Option<String>,
|
||||||
) {
|
) {
|
||||||
std::thread::spawn(move || {
|
std::thread::spawn(move || {
|
||||||
let rt = tokio::runtime::Runtime::new().unwrap();
|
let rt = tokio::runtime::Runtime::new().unwrap();
|
||||||
rt.block_on(async {
|
rt.block_on(async {
|
||||||
|
// Handle all API keys as optional; ollama and v1 completions doesnt always require it.
|
||||||
|
let key_opt = if api_key.is_empty() { None } else { Some(api_key.as_str()) };
|
||||||
let result = match provider.as_str() {
|
let result = match provider.as_str() {
|
||||||
"openai" => {
|
"openai" => {
|
||||||
stream_openai(
|
stream_openai(
|
||||||
&app,
|
&app,
|
||||||
&api_key,
|
OPENAI_API_URL,
|
||||||
|
Some(&api_key),
|
||||||
&model,
|
&model,
|
||||||
&system_prompt,
|
&system_prompt,
|
||||||
&user_message,
|
&user_message,
|
||||||
@@ -31,6 +36,38 @@ pub fn ai_request(
|
|||||||
)
|
)
|
||||||
.await
|
.await
|
||||||
}
|
}
|
||||||
|
"ollama" => {
|
||||||
|
let url = base_url.as_deref().unwrap_or(OLLAMA_DEFAULT_URL);
|
||||||
|
let url = format!("{}/v1/chat/completions", url.trim_end_matches('/'));
|
||||||
|
stream_openai(
|
||||||
|
&app,
|
||||||
|
&url,
|
||||||
|
key_opt,
|
||||||
|
&model,
|
||||||
|
&system_prompt,
|
||||||
|
&user_message,
|
||||||
|
&request_id,
|
||||||
|
)
|
||||||
|
.await
|
||||||
|
}
|
||||||
|
"openai_compatible" => {
|
||||||
|
let url = base_url.as_deref().unwrap_or("");
|
||||||
|
if url.is_empty() {
|
||||||
|
Err("No base URL configured for OpenAI Compatible provider".to_string())
|
||||||
|
} else {
|
||||||
|
let url = format!("{}/v1/chat/completions", normalize_openai_base(url));
|
||||||
|
stream_openai(
|
||||||
|
&app,
|
||||||
|
&url,
|
||||||
|
key_opt,
|
||||||
|
&model,
|
||||||
|
&system_prompt,
|
||||||
|
&user_message,
|
||||||
|
&request_id,
|
||||||
|
)
|
||||||
|
.await
|
||||||
|
}
|
||||||
|
}
|
||||||
_ => {
|
_ => {
|
||||||
stream_anthropic(
|
stream_anthropic(
|
||||||
&app,
|
&app,
|
||||||
@@ -57,6 +94,13 @@ pub fn ai_request(
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Normalize an OpenAI-compatible base URL: drop a trailing slash and a trailing `/v1`,
|
||||||
|
/// so both `https://host` and `https://host/v1` work (we append `/v1/chat/completions`).
|
||||||
|
fn normalize_openai_base(base: &str) -> String {
|
||||||
|
let b = base.trim().trim_end_matches('/');
|
||||||
|
b.strip_suffix("/v1").unwrap_or(b).trim_end_matches('/').to_string()
|
||||||
|
}
|
||||||
|
|
||||||
async fn stream_anthropic(
|
async fn stream_anthropic(
|
||||||
app: &AppHandle,
|
app: &AppHandle,
|
||||||
api_key: &str,
|
api_key: &str,
|
||||||
@@ -173,7 +217,6 @@ async fn stream_anthropic(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Stream ended — send done
|
|
||||||
let _ = app.emit(
|
let _ = app.emit(
|
||||||
"ai-stream",
|
"ai-stream",
|
||||||
AiStreamEvent {
|
AiStreamEvent {
|
||||||
@@ -188,7 +231,8 @@ async fn stream_anthropic(
|
|||||||
|
|
||||||
async fn stream_openai(
|
async fn stream_openai(
|
||||||
app: &AppHandle,
|
app: &AppHandle,
|
||||||
api_key: &str,
|
url: &str,
|
||||||
|
api_key: Option<&str>,
|
||||||
model: &str,
|
model: &str,
|
||||||
system_prompt: &str,
|
system_prompt: &str,
|
||||||
user_message: &str,
|
user_message: &str,
|
||||||
@@ -224,10 +268,15 @@ async fn stream_openai(
|
|||||||
body["temperature"] = json!(0.7);
|
body["temperature"] = json!(0.7);
|
||||||
}
|
}
|
||||||
|
|
||||||
let response = client
|
let mut req = client
|
||||||
.post(OPENAI_API_URL)
|
.post(url)
|
||||||
.header("Authorization", format!("Bearer {}", api_key))
|
.header("content-type", "application/json");
|
||||||
.header("content-type", "application/json")
|
|
||||||
|
if let Some(key) = api_key {
|
||||||
|
req = req.header("Authorization", format!("Bearer {}", key));
|
||||||
|
}
|
||||||
|
|
||||||
|
let response = req
|
||||||
.json(&body)
|
.json(&body)
|
||||||
.send()
|
.send()
|
||||||
.await
|
.await
|
||||||
@@ -325,9 +374,28 @@ async fn stream_openai(
|
|||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
pub async fn test_connection(provider: &str, api_key: &str, model: &str) -> Result<String, String> {
|
pub async fn test_connection(
|
||||||
|
provider: &str,
|
||||||
|
api_key: &str,
|
||||||
|
model: &str,
|
||||||
|
base_url: Option<&str>,
|
||||||
|
) -> Result<String, String> {
|
||||||
|
let key_opt = if api_key.is_empty() { None } else { Some(api_key) };
|
||||||
match provider {
|
match provider {
|
||||||
"openai" => test_openai(api_key, model).await,
|
"openai" => test_openai(OPENAI_API_URL, Some(api_key), model).await,
|
||||||
|
"ollama" => {
|
||||||
|
let url = base_url.unwrap_or(OLLAMA_DEFAULT_URL);
|
||||||
|
let url = format!("{}/v1/chat/completions", url.trim_end_matches('/'));
|
||||||
|
test_openai(&url, key_opt, model).await
|
||||||
|
}
|
||||||
|
"openai_compatible" => {
|
||||||
|
let url = base_url.unwrap_or("");
|
||||||
|
if url.is_empty() {
|
||||||
|
return Err("No base URL configured for OpenAI Compatible provider".to_string());
|
||||||
|
}
|
||||||
|
let url = format!("{}/v1/chat/completions", normalize_openai_base(url));
|
||||||
|
test_openai(&url, key_opt, model).await
|
||||||
|
}
|
||||||
_ => test_anthropic(api_key, model).await,
|
_ => test_anthropic(api_key, model).await,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -337,7 +405,7 @@ async fn test_anthropic(api_key: &str, model: &str) -> Result<String, String> {
|
|||||||
|
|
||||||
let body = json!({
|
let body = json!({
|
||||||
"model": model,
|
"model": model,
|
||||||
"max_tokens": 10,
|
"max_tokens": 20,
|
||||||
"messages": [
|
"messages": [
|
||||||
{
|
{
|
||||||
"role": "user",
|
"role": "user",
|
||||||
@@ -365,20 +433,15 @@ async fn test_anthropic(api_key: &str, model: &str) -> Result<String, String> {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async fn test_openai(api_key: &str, model: &str) -> Result<String, String> {
|
async fn test_openai(url: &str, api_key: Option<&str>, model: &str) -> Result<String, String> {
|
||||||
let client = Client::new();
|
let client = Client::new();
|
||||||
|
let is_gpt5 = model.starts_with("gpt-5");
|
||||||
|
let token_key = if is_gpt5 { "max_completion_tokens" } else { "max_tokens" };
|
||||||
|
|
||||||
let is_gpt5 = model.starts_with("gpt-5");
|
let body = json!({
|
||||||
let token_key = if is_gpt5 {
|
"model": model,
|
||||||
"max_completion_tokens"
|
token_key: 20,
|
||||||
} else {
|
"messages": [
|
||||||
"max_tokens"
|
|
||||||
};
|
|
||||||
|
|
||||||
let body = json!({
|
|
||||||
"model": model,
|
|
||||||
token_key: 10,
|
|
||||||
"messages": [
|
|
||||||
{
|
{
|
||||||
"role": "user",
|
"role": "user",
|
||||||
"content": "Hi"
|
"content": "Hi"
|
||||||
@@ -386,10 +449,15 @@ async fn test_openai(api_key: &str, model: &str) -> Result<String, String> {
|
|||||||
]
|
]
|
||||||
});
|
});
|
||||||
|
|
||||||
let response = client
|
let mut req = client
|
||||||
.post(OPENAI_API_URL)
|
.post(url)
|
||||||
.header("Authorization", format!("Bearer {}", api_key))
|
.header("content-type", "application/json");
|
||||||
.header("content-type", "application/json")
|
|
||||||
|
if let Some(key) = api_key {
|
||||||
|
req = req.header("Authorization", format!("Bearer {}", key));
|
||||||
|
}
|
||||||
|
|
||||||
|
let response = req
|
||||||
.json(&body)
|
.json(&body)
|
||||||
.send()
|
.send()
|
||||||
.await
|
.await
|
||||||
|
|||||||