Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
0e5b380f8c | ||
|
|
e76919ab5c | ||
|
|
84ab90fd3f | ||
|
|
87b191eb63 | ||
|
|
0def9d6d6d | ||
|
|
e708d8ee63 | ||
|
|
461001442a | ||
|
|
6b5e923e8e | ||
|
|
289606a746 | ||
|
|
9c53b3b594 | ||
|
|
d497ff8aaf | ||
|
|
51acd51cb2 |
@@ -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.3.3",
|
"version": "1.3.4",
|
||||||
"type": "module",
|
"type": "module",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"dev": "vite dev",
|
"dev": "vite dev",
|
||||||
|
|||||||
@@ -1850,7 +1850,7 @@ checksum = "2304e00983f87ffb38b55b444b5e3b60a884b5d30c0fca7d82fe33449bbe55ea"
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "helixnotes"
|
name = "helixnotes"
|
||||||
version = "1.3.3"
|
version = "1.3.4"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"arboard",
|
"arboard",
|
||||||
"chrono",
|
"chrono",
|
||||||
@@ -1860,6 +1860,7 @@ dependencies = [
|
|||||||
"image",
|
"image",
|
||||||
"log",
|
"log",
|
||||||
"notify",
|
"notify",
|
||||||
|
"objc2-app-kit",
|
||||||
"png 0.17.16",
|
"png 0.17.16",
|
||||||
"quick-xml 0.36.2",
|
"quick-xml 0.36.2",
|
||||||
"rayon",
|
"rayon",
|
||||||
@@ -1876,6 +1877,7 @@ dependencies = [
|
|||||||
"tauri-build",
|
"tauri-build",
|
||||||
"tauri-plugin-dialog",
|
"tauri-plugin-dialog",
|
||||||
"tauri-plugin-fs",
|
"tauri-plugin-fs",
|
||||||
|
"tauri-plugin-ios-vault-access",
|
||||||
"tauri-plugin-log",
|
"tauri-plugin-log",
|
||||||
"tauri-plugin-opener",
|
"tauri-plugin-opener",
|
||||||
"tauri-plugin-single-instance",
|
"tauri-plugin-single-instance",
|
||||||
@@ -5201,6 +5203,15 @@ dependencies = [
|
|||||||
"url",
|
"url",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "tauri-plugin-ios-vault-access"
|
||||||
|
version = "0.1.0"
|
||||||
|
dependencies = [
|
||||||
|
"serde",
|
||||||
|
"tauri",
|
||||||
|
"tauri-plugin",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "tauri-plugin-log"
|
name = "tauri-plugin-log"
|
||||||
version = "2.8.0"
|
version = "2.8.0"
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
[package]
|
[package]
|
||||||
name = "helixnotes"
|
name = "helixnotes"
|
||||||
version = "1.3.3"
|
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"
|
||||||
@@ -42,6 +42,12 @@ quick-xml = "0.36"
|
|||||||
urlencoding = "2"
|
urlencoding = "2"
|
||||||
sha2 = "0.10"
|
sha2 = "0.10"
|
||||||
rustls = { version = "0.23", default-features = false, features = ["ring", "logging", "std", "tls12"] }
|
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]
|
[target.'cfg(not(any(target_os = "android", target_os = "ios")))'.dependencies]
|
||||||
tauri-plugin-updater = "2"
|
tauri-plugin-updater = "2"
|
||||||
tauri-plugin-single-instance = "2"
|
tauri-plugin-single-instance = "2"
|
||||||
|
|||||||
@@ -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>
|
||||||
|
Before Width: | Height: | Size: 3.0 KiB After Width: | Height: | Size: 1.6 KiB |
|
Before Width: | Height: | Size: 2.0 KiB After Width: | Height: | Size: 1.0 KiB |
|
Before Width: | Height: | Size: 4.0 KiB After Width: | Height: | Size: 2.2 KiB |
|
Before Width: | Height: | Size: 5.9 KiB After Width: | Height: | Size: 3.5 KiB |
|
Before Width: | Height: | Size: 8.0 KiB After Width: | Height: | Size: 5.2 KiB |
@@ -1,4 +1,4 @@
|
|||||||
<?xml version="1.0" encoding="utf-8"?>
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
<resources>
|
<resources>
|
||||||
<color name="ic_launcher_background">#fff</color>
|
<color name="ic_launcher_background">#626ED4</color>
|
||||||
</resources>
|
</resources>
|
||||||
@@ -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 |
@@ -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()
|
||||||
|
}
|
||||||
@@ -21,15 +21,63 @@ fn remove_note_bg(state: &State<'_, AppState>, path: &str) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn clear_vault_runtime(state: &State<'_, AppState>) -> Result<(), String> {
|
||||||
|
*state.watcher.lock().map_err(|error| error.to_string())? = None;
|
||||||
|
*state
|
||||||
|
.search_index
|
||||||
|
.lock()
|
||||||
|
.map_err(|error| error.to_string())? = None;
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
// ── Vault Management ──
|
// ── Vault Management ──
|
||||||
|
|
||||||
#[tauri::command]
|
#[cfg(target_os = "ios")]
|
||||||
pub fn open_vault(app: AppHandle, state: State<'_, AppState>, path: String) -> Result<(), String> {
|
use tauri_plugin_ios_vault_access::{FolderSelection, IosVaultAccessExt};
|
||||||
|
|
||||||
|
#[derive(Debug, Clone, serde::Serialize)]
|
||||||
|
#[serde(rename_all = "camelCase")]
|
||||||
|
pub struct ExternalVaultResult {
|
||||||
|
pub bookmark_id: String,
|
||||||
|
pub path: String,
|
||||||
|
pub name: String,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(target_os = "ios")]
|
||||||
|
impl From<FolderSelection> for ExternalVaultResult {
|
||||||
|
fn from(selection: FolderSelection) -> Self {
|
||||||
|
Self {
|
||||||
|
bookmark_id: selection.bookmark_id,
|
||||||
|
path: selection.path,
|
||||||
|
name: selection.name,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn open_vault_path(
|
||||||
|
app: AppHandle,
|
||||||
|
state: &State<'_, AppState>,
|
||||||
|
path: String,
|
||||||
|
external: Option<(String, String)>,
|
||||||
|
) -> Result<(), String> {
|
||||||
operations::ensure_vault_structure(&path)?;
|
operations::ensure_vault_structure(&path)?;
|
||||||
|
|
||||||
// Initialize search index - rebuild in background on mobile (sandboxed FS is slow)
|
// Stage the search index and watcher before replacing the active runtime.
|
||||||
let search = std::sync::Arc::new(SearchIndex::new(&path)?);
|
let search = std::sync::Arc::new(SearchIndex::new(&path)?);
|
||||||
#[cfg(mobile)]
|
#[cfg(target_os = "ios")]
|
||||||
|
{
|
||||||
|
if external.is_some() {
|
||||||
|
search.rebuild(&path)?;
|
||||||
|
} else {
|
||||||
|
let search_bg = search.clone();
|
||||||
|
let vault = path.clone();
|
||||||
|
std::thread::spawn(move || {
|
||||||
|
let _ = search_bg.rebuild(&vault);
|
||||||
|
log::info!("mobile: search index rebuild complete");
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
#[cfg(all(mobile, not(target_os = "ios")))]
|
||||||
{
|
{
|
||||||
let search_bg = search.clone();
|
let search_bg = search.clone();
|
||||||
let vault = path.clone();
|
let vault = path.clone();
|
||||||
@@ -39,43 +87,227 @@ pub fn open_vault(app: AppHandle, state: State<'_, AppState>, path: String) -> R
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
#[cfg(desktop)]
|
#[cfg(desktop)]
|
||||||
{
|
|
||||||
search.rebuild(&path)?;
|
search.rebuild(&path)?;
|
||||||
}
|
|
||||||
*state.search_index.lock().map_err(|e| e.to_string())? = Some(search);
|
|
||||||
|
|
||||||
// Start file watcher
|
let new_watcher = watcher::start_watcher(app.clone(), path.clone())?;
|
||||||
let w = watcher::start_watcher(app.clone(), path.clone())?;
|
|
||||||
*state.watcher.lock().map_err(|e| e.to_string())? = Some(w);
|
|
||||||
|
|
||||||
// Update config
|
// Update config. External vaults use the bookmark as their stable identity;
|
||||||
|
// the resolved path is refreshed whenever the vault opens.
|
||||||
|
let mut search_slot = state.search_index.lock().map_err(|e| e.to_string())?;
|
||||||
|
let mut watcher_slot = state.watcher.lock().map_err(|e| e.to_string())?;
|
||||||
let mut config = state.config.lock().map_err(|e| e.to_string())?;
|
let mut config = state.config.lock().map_err(|e| e.to_string())?;
|
||||||
if !config.vaults.iter().any(|v| v.path == path) {
|
let mut next = config.clone();
|
||||||
|
let active_bookmark_id = external
|
||||||
|
.as_ref()
|
||||||
|
.map(|(bookmark_id, _)| bookmark_id.clone());
|
||||||
|
if let Some((bookmark_id, name)) = external {
|
||||||
|
if let Some(vault) = next
|
||||||
|
.vaults
|
||||||
|
.iter_mut()
|
||||||
|
.find(|vault| vault.bookmark_id.as_deref() == Some(bookmark_id.as_str()))
|
||||||
|
{
|
||||||
|
vault.path.clone_from(&path);
|
||||||
|
vault.name = name;
|
||||||
|
} else {
|
||||||
|
next.vaults.push(VaultConfig {
|
||||||
|
path: path.clone(),
|
||||||
|
name,
|
||||||
|
bookmark_id: Some(bookmark_id),
|
||||||
|
..Default::default()
|
||||||
|
});
|
||||||
|
}
|
||||||
|
} else if !next
|
||||||
|
.vaults
|
||||||
|
.iter()
|
||||||
|
.any(|vault| vault.bookmark_id.is_none() && vault.path == path)
|
||||||
|
{
|
||||||
let name = std::path::Path::new(&path)
|
let name = std::path::Path::new(&path)
|
||||||
.file_name()
|
.file_name()
|
||||||
.unwrap_or_default()
|
.unwrap_or_default()
|
||||||
.to_string_lossy()
|
.to_string_lossy()
|
||||||
.to_string();
|
.to_string();
|
||||||
config.vaults.push(VaultConfig {
|
next.vaults.push(VaultConfig {
|
||||||
path: path.clone(),
|
path: path.clone(),
|
||||||
name,
|
name,
|
||||||
..Default::default()
|
..Default::default()
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
config.active_vault = Some(path);
|
next.active_vault = Some(path);
|
||||||
save_app_config(&config)?;
|
next.active_bookmark_id = active_bookmark_id;
|
||||||
|
save_app_config(&next)?;
|
||||||
|
*search_slot = Some(search);
|
||||||
|
*watcher_slot = Some(new_watcher);
|
||||||
|
*config = next;
|
||||||
|
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
#[tauri::command]
|
#[tauri::command]
|
||||||
pub fn remove_vault(state: State<'_, AppState>, path: String) -> Result<(), String> {
|
pub async fn open_vault(
|
||||||
let mut config = state.config.lock().map_err(|e| e.to_string())?;
|
app: AppHandle,
|
||||||
config.vaults.retain(|v| v.path != path);
|
state: State<'_, AppState>,
|
||||||
if config.active_vault.as_deref() == Some(path.as_str()) {
|
path: String,
|
||||||
config.active_vault = None;
|
) -> Result<(), String> {
|
||||||
|
let _transition = state.vault_transition.lock().await;
|
||||||
|
open_vault_path(app.clone(), &state, path, None)?;
|
||||||
|
#[cfg(target_os = "ios")]
|
||||||
|
app.ios_vault_access().release_active()?;
|
||||||
|
Ok(())
|
||||||
}
|
}
|
||||||
save_app_config(&config)?;
|
|
||||||
|
#[tauri::command]
|
||||||
|
pub async fn choose_external_vault(
|
||||||
|
app: AppHandle,
|
||||||
|
state: State<'_, AppState>,
|
||||||
|
) -> Result<Option<ExternalVaultResult>, String> {
|
||||||
|
let _transition = state.vault_transition.lock().await;
|
||||||
|
#[cfg(target_os = "ios")]
|
||||||
|
{
|
||||||
|
let picker_app = app.clone();
|
||||||
|
let selection = tauri::async_runtime::spawn_blocking(move || {
|
||||||
|
picker_app.ios_vault_access().choose_folder()
|
||||||
|
})
|
||||||
|
.await
|
||||||
|
.map_err(|error| error.to_string())??;
|
||||||
|
if selection.cancelled {
|
||||||
|
return Ok(None);
|
||||||
|
}
|
||||||
|
let result = ExternalVaultResult::from(selection);
|
||||||
|
let bookmark_was_registered = state
|
||||||
|
.config
|
||||||
|
.lock()
|
||||||
|
.map_err(|error| error.to_string())?
|
||||||
|
.vaults
|
||||||
|
.iter()
|
||||||
|
.any(|vault| vault.bookmark_id.as_deref() == Some(result.bookmark_id.as_str()));
|
||||||
|
if let Err(error) = open_vault_path(
|
||||||
|
app.clone(),
|
||||||
|
&state,
|
||||||
|
result.path.clone(),
|
||||||
|
Some((result.bookmark_id.clone(), result.name.clone())),
|
||||||
|
) {
|
||||||
|
if bookmark_was_registered {
|
||||||
|
let _ = app.ios_vault_access().rollback_staged();
|
||||||
|
} else {
|
||||||
|
let _ = app
|
||||||
|
.ios_vault_access()
|
||||||
|
.forget_bookmark(&result.bookmark_id);
|
||||||
|
}
|
||||||
|
return Err(error);
|
||||||
|
}
|
||||||
|
app.ios_vault_access().commit_staged()?;
|
||||||
|
Ok(Some(result))
|
||||||
|
}
|
||||||
|
#[cfg(not(target_os = "ios"))]
|
||||||
|
{
|
||||||
|
let _ = (&app, &state);
|
||||||
|
Err("Files folders are only available on iOS.".to_string())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[tauri::command]
|
||||||
|
pub async fn restore_external_vault(
|
||||||
|
app: AppHandle,
|
||||||
|
state: State<'_, AppState>,
|
||||||
|
bookmark_id: String,
|
||||||
|
) -> Result<ExternalVaultResult, String> {
|
||||||
|
let _transition = state.vault_transition.lock().await;
|
||||||
|
#[cfg(target_os = "ios")]
|
||||||
|
{
|
||||||
|
let resolver_app = app.clone();
|
||||||
|
let selection = tauri::async_runtime::spawn_blocking(move || {
|
||||||
|
resolver_app.ios_vault_access().resolve_folder(&bookmark_id)
|
||||||
|
})
|
||||||
|
.await
|
||||||
|
.map_err(|error| error.to_string())??;
|
||||||
|
let result = ExternalVaultResult::from(selection);
|
||||||
|
if let Err(error) = open_vault_path(
|
||||||
|
app.clone(),
|
||||||
|
&state,
|
||||||
|
result.path.clone(),
|
||||||
|
Some((result.bookmark_id.clone(), result.name.clone())),
|
||||||
|
) {
|
||||||
|
let _ = app.ios_vault_access().rollback_staged();
|
||||||
|
return Err(error);
|
||||||
|
}
|
||||||
|
app.ios_vault_access().commit_staged()?;
|
||||||
|
Ok(result)
|
||||||
|
}
|
||||||
|
#[cfg(not(target_os = "ios"))]
|
||||||
|
{
|
||||||
|
let _ = (&app, &state, &bookmark_id);
|
||||||
|
Err("Files folders are only available on iOS.".to_string())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[tauri::command]
|
||||||
|
pub async fn remove_vault(
|
||||||
|
app: AppHandle,
|
||||||
|
state: State<'_, AppState>,
|
||||||
|
path: String,
|
||||||
|
bookmark_id: Option<String>,
|
||||||
|
) -> Result<(), String> {
|
||||||
|
let _transition = state.vault_transition.lock().await;
|
||||||
|
let mut config = state.config.lock().map_err(|error| error.to_string())?;
|
||||||
|
let target = if let Some(bookmark_id) = bookmark_id.as_deref() {
|
||||||
|
config
|
||||||
|
.vaults
|
||||||
|
.iter()
|
||||||
|
.find(|vault| vault.bookmark_id.as_deref() == Some(bookmark_id))
|
||||||
|
} else {
|
||||||
|
config
|
||||||
|
.vaults
|
||||||
|
.iter()
|
||||||
|
.find(|vault| vault.bookmark_id.is_none() && vault.path == path)
|
||||||
|
}
|
||||||
|
.map(|vault| {
|
||||||
|
let is_active = if let Some(bookmark_id) = vault.bookmark_id.as_deref() {
|
||||||
|
config.active_bookmark_id.as_deref() == Some(bookmark_id)
|
||||||
|
} else {
|
||||||
|
config.active_bookmark_id.is_none()
|
||||||
|
&& config.active_vault.as_deref() == Some(vault.path.as_str())
|
||||||
|
};
|
||||||
|
(vault.path.clone(), vault.bookmark_id.clone(), is_active)
|
||||||
|
});
|
||||||
|
|
||||||
|
let Some((target_path, target_bookmark, is_active)) = target else {
|
||||||
|
return Ok(());
|
||||||
|
};
|
||||||
|
|
||||||
|
let old = config.clone();
|
||||||
|
let mut next = old.clone();
|
||||||
|
if let Some(id) = target_bookmark.as_deref() {
|
||||||
|
next.vaults
|
||||||
|
.retain(|vault| vault.bookmark_id.as_deref() != Some(id));
|
||||||
|
} else {
|
||||||
|
next.vaults
|
||||||
|
.retain(|vault| vault.bookmark_id.is_some() || vault.path != target_path);
|
||||||
|
}
|
||||||
|
if is_active {
|
||||||
|
next.active_vault = None;
|
||||||
|
next.active_bookmark_id = None;
|
||||||
|
}
|
||||||
|
save_app_config(&next)?;
|
||||||
|
|
||||||
|
#[cfg(target_os = "ios")]
|
||||||
|
if let Some(id) = target_bookmark.as_deref() {
|
||||||
|
if let Err(error) = app.ios_vault_access().forget_bookmark(id) {
|
||||||
|
return match save_app_config(&old) {
|
||||||
|
Ok(()) => Err(error),
|
||||||
|
Err(rollback_error) => Err(format!(
|
||||||
|
"{error} Configuration rollback also failed: {rollback_error}"
|
||||||
|
)),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
#[cfg(not(target_os = "ios"))]
|
||||||
|
let _ = (&app, &target_bookmark);
|
||||||
|
|
||||||
|
if is_active {
|
||||||
|
clear_vault_runtime(&state)?;
|
||||||
|
}
|
||||||
|
*config = next;
|
||||||
|
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -160,11 +392,15 @@ pub fn import_custom_themes(state: State<'_, AppState>, path: String) -> Result<
|
|||||||
}
|
}
|
||||||
|
|
||||||
#[tauri::command]
|
#[tauri::command]
|
||||||
pub fn set_font_size(state: State<'_, AppState>, size: u32) -> Result<(), String> {
|
pub fn set_font_size(app: AppHandle, state: State<'_, AppState>, size: u32) -> Result<(), String> {
|
||||||
let mut config = state.config.lock().map_err(|e| e.to_string())?;
|
let mut config = state.config.lock().map_err(|e| e.to_string())?;
|
||||||
config.font_size = Some(size);
|
config.font_size = Some(size);
|
||||||
save_app_config(&config)?;
|
save_app_config(&config)?;
|
||||||
Ok(())
|
drop(config);
|
||||||
|
|
||||||
|
use tauri::Emitter;
|
||||||
|
app.emit("editor-font-size-changed", size)
|
||||||
|
.map_err(|e| e.to_string())
|
||||||
}
|
}
|
||||||
|
|
||||||
#[tauri::command]
|
#[tauri::command]
|
||||||
@@ -1762,15 +1998,63 @@ pub fn test_ai_connection(app: AppHandle) -> Result<(), String> {
|
|||||||
|
|
||||||
// ── Sync (WebDAV) ──
|
// ── Sync (WebDAV) ──
|
||||||
|
|
||||||
fn active_vault_config(config: &AppConfig) -> Result<&VaultConfig, String> {
|
fn vault_matches_identity(
|
||||||
|
vault: &VaultConfig,
|
||||||
|
path: &str,
|
||||||
|
bookmark_id: Option<&str>,
|
||||||
|
) -> bool {
|
||||||
|
if let Some(bookmark_id) = bookmark_id {
|
||||||
|
vault.bookmark_id.as_deref() == Some(bookmark_id)
|
||||||
|
} else {
|
||||||
|
vault.bookmark_id.is_none() && vault.path == path
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn active_vault_index(config: &AppConfig) -> Result<usize, String> {
|
||||||
let active = config.active_vault.as_deref().ok_or("No active vault")?;
|
let active = config.active_vault.as_deref().ok_or("No active vault")?;
|
||||||
config
|
config
|
||||||
.vaults
|
.vaults
|
||||||
.iter()
|
.iter()
|
||||||
.find(|v| v.path == active)
|
.position(|vault| {
|
||||||
|
vault_matches_identity(vault, active, config.active_bookmark_id.as_deref())
|
||||||
|
})
|
||||||
.ok_or_else(|| "Active vault not found in config".to_string())
|
.ok_or_else(|| "Active vault not found in config".to_string())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn active_vault_config(config: &AppConfig) -> Result<&VaultConfig, String> {
|
||||||
|
Ok(&config.vaults[active_vault_index(config)?])
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
mod vault_identity_tests {
|
||||||
|
use super::*;
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn bookmark_identity_disambiguates_vaults_with_the_same_path() {
|
||||||
|
let mut config = AppConfig::default();
|
||||||
|
config.active_vault = Some("/same/path".to_string());
|
||||||
|
config.vaults = vec![
|
||||||
|
VaultConfig {
|
||||||
|
path: "/same/path".to_string(),
|
||||||
|
name: "Local".to_string(),
|
||||||
|
..Default::default()
|
||||||
|
},
|
||||||
|
VaultConfig {
|
||||||
|
path: "/same/path".to_string(),
|
||||||
|
name: "Files".to_string(),
|
||||||
|
bookmark_id: Some("bookmark".to_string()),
|
||||||
|
..Default::default()
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
config.active_bookmark_id = Some("bookmark".to_string());
|
||||||
|
assert_eq!(active_vault_config(&config).unwrap().name, "Files");
|
||||||
|
|
||||||
|
config.active_bookmark_id = None;
|
||||||
|
assert_eq!(active_vault_config(&config).unwrap().name, "Local");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
fn sync_config_from(config: &AppConfig) -> Result<crate::sync::WebdavConfig, String> {
|
fn sync_config_from(config: &AppConfig) -> Result<crate::sync::WebdavConfig, String> {
|
||||||
let v = active_vault_config(config)?;
|
let v = active_vault_config(config)?;
|
||||||
if v.sync_provider.as_deref() != Some("webdav") {
|
if v.sync_provider.as_deref() != Some("webdav") {
|
||||||
@@ -1800,12 +2084,8 @@ pub fn set_sync_settings(
|
|||||||
sync_interval_minutes: u32,
|
sync_interval_minutes: u32,
|
||||||
) -> Result<(), String> {
|
) -> Result<(), String> {
|
||||||
let mut config = state.config.lock().map_err(|e| e.to_string())?;
|
let mut config = state.config.lock().map_err(|e| e.to_string())?;
|
||||||
let active = config.active_vault.clone().ok_or("No active vault")?;
|
let active_index = active_vault_index(&config)?;
|
||||||
let v = config
|
let v = &mut config.vaults[active_index];
|
||||||
.vaults
|
|
||||||
.iter_mut()
|
|
||||||
.find(|v| v.path == active)
|
|
||||||
.ok_or_else(|| "Active vault not found in config".to_string())?;
|
|
||||||
v.sync_provider = provider.filter(|p| !p.is_empty());
|
v.sync_provider = provider.filter(|p| !p.is_empty());
|
||||||
v.webdav_url = url.filter(|u| !u.trim().is_empty());
|
v.webdav_url = url.filter(|u| !u.trim().is_empty());
|
||||||
v.webdav_username = username.filter(|u| !u.is_empty());
|
v.webdav_username = username.filter(|u| !u.is_empty());
|
||||||
@@ -1851,7 +2131,7 @@ pub fn sync_now(app: AppHandle) -> Result<(), String> {
|
|||||||
if app.state::<AppState>().syncing.swap(true, Ordering::SeqCst) {
|
if app.state::<AppState>().syncing.swap(true, Ordering::SeqCst) {
|
||||||
return Ok(()); // a sync is already running
|
return Ok(()); // a sync is already running
|
||||||
}
|
}
|
||||||
let (vault, cfg) = {
|
let (vault, bookmark_id, cfg) = {
|
||||||
let state = app.state::<AppState>();
|
let state = app.state::<AppState>();
|
||||||
let config = match state.config.lock() {
|
let config = match state.config.lock() {
|
||||||
Ok(c) => c,
|
Ok(c) => c,
|
||||||
@@ -1864,9 +2144,13 @@ pub fn sync_now(app: AppHandle) -> Result<(), String> {
|
|||||||
.active_vault
|
.active_vault
|
||||||
.clone()
|
.clone()
|
||||||
.ok_or_else(|| "No active vault".to_string())
|
.ok_or_else(|| "No active vault".to_string())
|
||||||
.and_then(|v| sync_config_from(&config).map(|c| (v, c)));
|
.and_then(|vault| {
|
||||||
|
sync_config_from(&config).map(|cfg| {
|
||||||
|
(vault, config.active_bookmark_id.clone(), cfg)
|
||||||
|
})
|
||||||
|
});
|
||||||
match gathered {
|
match gathered {
|
||||||
Ok(vc) => vc,
|
Ok(vault_config) => vault_config,
|
||||||
Err(e) => {
|
Err(e) => {
|
||||||
drop(config);
|
drop(config);
|
||||||
state.syncing.store(false, Ordering::SeqCst);
|
state.syncing.store(false, Ordering::SeqCst);
|
||||||
@@ -1882,8 +2166,10 @@ pub fn sync_now(app: AppHandle) -> Result<(), String> {
|
|||||||
Ok(summary) => {
|
Ok(summary) => {
|
||||||
let ts = chrono::Utc::now().to_rfc3339();
|
let ts = chrono::Utc::now().to_rfc3339();
|
||||||
if let Ok(mut config) = app.state::<AppState>().config.lock() {
|
if let Ok(mut config) = app.state::<AppState>().config.lock() {
|
||||||
if let Some(v) = config.vaults.iter_mut().find(|v| v.path == vault) {
|
if let Some(vault_config) = config.vaults.iter_mut().find(|candidate| {
|
||||||
v.last_sync_time = Some(ts.clone());
|
vault_matches_identity(candidate, &vault, bookmark_id.as_deref())
|
||||||
|
}) {
|
||||||
|
vault_config.last_sync_time = Some(ts.clone());
|
||||||
}
|
}
|
||||||
let _ = save_app_config(&config);
|
let _ = save_app_config(&config);
|
||||||
}
|
}
|
||||||
@@ -2022,7 +2308,7 @@ fn migrate_global_sync_to_vault(config: &mut AppConfig) -> bool {
|
|||||||
if config.sync_provider.is_none() && config.webdav_url.is_none() {
|
if config.sync_provider.is_none() && config.webdav_url.is_none() {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
let Some(active) = config.active_vault.clone() else { return false; };
|
let Ok(active_index) = active_vault_index(config) else { return false; };
|
||||||
let g_provider = config.sync_provider.clone();
|
let g_provider = config.sync_provider.clone();
|
||||||
let g_url = config.webdav_url.clone();
|
let g_url = config.webdav_url.clone();
|
||||||
let g_user = config.webdav_username.clone();
|
let g_user = config.webdav_username.clone();
|
||||||
@@ -2031,7 +2317,7 @@ fn migrate_global_sync_to_vault(config: &mut AppConfig) -> bool {
|
|||||||
let g_on_change = config.sync_on_change;
|
let g_on_change = config.sync_on_change;
|
||||||
let g_interval = config.sync_interval_minutes;
|
let g_interval = config.sync_interval_minutes;
|
||||||
let g_last = config.last_sync_time.clone();
|
let g_last = config.last_sync_time.clone();
|
||||||
let Some(v) = config.vaults.iter_mut().find(|v| v.path == active) else { return false; };
|
let v = &mut config.vaults[active_index];
|
||||||
if v.sync_provider.is_some() || v.webdav_url.is_some() {
|
if v.sync_provider.is_some() || v.webdav_url.is_some() {
|
||||||
return false; // already migrated / has its own config
|
return false; // already migrated / has its own config
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -56,7 +56,27 @@ pub fn run() {
|
|||||||
.plugin(tauri_plugin_dialog::init())
|
.plugin(tauri_plugin_dialog::init())
|
||||||
.plugin(tauri_plugin_fs::init())
|
.plugin(tauri_plugin_fs::init())
|
||||||
.plugin(tauri_plugin_opener::init())
|
.plugin(tauri_plugin_opener::init())
|
||||||
|
.on_page_load(|webview, payload| {
|
||||||
|
#[cfg(target_os = "macos")]
|
||||||
|
if webview.label() == "main"
|
||||||
|
&& matches!(
|
||||||
|
payload.event(),
|
||||||
|
tauri::webview::PageLoadEvent::Finished
|
||||||
|
)
|
||||||
|
{
|
||||||
|
let window = webview.window();
|
||||||
|
let window_on_main = window.clone();
|
||||||
|
let _ = window.run_on_main_thread(move || {
|
||||||
|
fix_macos_traffic_lights(&window_on_main);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
#[cfg(not(target_os = "macos"))]
|
||||||
|
let _ = (webview, payload);
|
||||||
|
})
|
||||||
.setup(move |app| {
|
.setup(move |app| {
|
||||||
|
#[cfg(target_os = "ios")]
|
||||||
|
app.handle().plugin(tauri_plugin_ios_vault_access::init())?;
|
||||||
|
|
||||||
if cfg!(debug_assertions) {
|
if cfg!(debug_assertions) {
|
||||||
app.handle().plugin(
|
app.handle().plugin(
|
||||||
tauri_plugin_log::Builder::default()
|
tauri_plugin_log::Builder::default()
|
||||||
@@ -85,13 +105,6 @@ pub fn run() {
|
|||||||
setup_tray(app)?;
|
setup_tray(app)?;
|
||||||
}
|
}
|
||||||
|
|
||||||
#[cfg(target_os = "macos")]
|
|
||||||
{
|
|
||||||
if let Some(window) = app.get_webview_window("main") {
|
|
||||||
fix_macos_traffic_lights(&window);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Check CLI args for a .md file path on initial launch
|
// Check CLI args for a .md file path on initial launch
|
||||||
#[cfg(desktop)]
|
#[cfg(desktop)]
|
||||||
{
|
{
|
||||||
@@ -119,6 +132,8 @@ pub fn run() {
|
|||||||
.manage(app_state)
|
.manage(app_state)
|
||||||
.invoke_handler(tauri::generate_handler![
|
.invoke_handler(tauri::generate_handler![
|
||||||
commands::open_vault,
|
commands::open_vault,
|
||||||
|
commands::choose_external_vault,
|
||||||
|
commands::restore_external_vault,
|
||||||
commands::remove_vault,
|
commands::remove_vault,
|
||||||
commands::get_app_config,
|
commands::get_app_config,
|
||||||
commands::set_theme,
|
commands::set_theme,
|
||||||
@@ -396,44 +411,49 @@ fn percent_decode(input: &str) -> String {
|
|||||||
}
|
}
|
||||||
|
|
||||||
#[cfg(target_os = "macos")]
|
#[cfg(target_os = "macos")]
|
||||||
fn fix_macos_traffic_lights(window: &tauri::WebviewWindow) {
|
fn fix_macos_traffic_lights(window: &tauri::Window) {
|
||||||
use objc2_app_kit::{NSView, NSWindow, NSWindowButton};
|
use objc2_app_kit::{NSView, NSWindow, NSWindowButton};
|
||||||
|
|
||||||
|
const TITLEBAR_HEIGHT: f64 = 34.0;
|
||||||
|
const LEFT_INSET: f64 = 12.0;
|
||||||
|
|
||||||
let ns_window_ptr = match window.ns_window() {
|
let ns_window_ptr = match window.ns_window() {
|
||||||
Ok(ptr) => ptr,
|
Ok(ptr) => ptr,
|
||||||
Err(_) => return,
|
Err(_) => return,
|
||||||
};
|
};
|
||||||
|
|
||||||
let ns_window: &NSWindow = unsafe { &*(ns_window_ptr as *const NSWindow) };
|
let ns_window: &NSWindow = unsafe { &*(ns_window_ptr as *const NSWindow) };
|
||||||
|
|
||||||
let close = match ns_window.standardWindowButton(NSWindowButton::CloseButton) {
|
let close = match ns_window.standardWindowButton(NSWindowButton::CloseButton) {
|
||||||
Some(btn) => btn,
|
Some(button) => button,
|
||||||
None => return,
|
None => return,
|
||||||
};
|
};
|
||||||
let miniaturize = match ns_window.standardWindowButton(NSWindowButton::MiniaturizeButton) {
|
let miniaturize = match ns_window.standardWindowButton(NSWindowButton::MiniaturizeButton) {
|
||||||
Some(btn) => btn,
|
Some(button) => button,
|
||||||
None => return,
|
None => return,
|
||||||
};
|
};
|
||||||
let zoom = match ns_window.standardWindowButton(NSWindowButton::ZoomButton) {
|
let zoom = match ns_window.standardWindowButton(NSWindowButton::ZoomButton) {
|
||||||
Some(btn) => btn,
|
Some(button) => button,
|
||||||
None => return,
|
None => return,
|
||||||
};
|
};
|
||||||
|
|
||||||
let superview = match close.superview() {
|
let window_height = ns_window.frame().size.height;
|
||||||
Some(sv) => sv,
|
if window_height <= 0.0 {
|
||||||
None => return,
|
|
||||||
};
|
|
||||||
|
|
||||||
let sv_height = NSView::frame(&superview).size.height;
|
|
||||||
let btn_height = NSView::frame(&close).size.height;
|
|
||||||
if sv_height <= 0.0 || btn_height <= 0.0 {
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
let centered_y = (sv_height - btn_height) / 2.0;
|
|
||||||
|
|
||||||
for btn in [&close, &miniaturize, &zoom] {
|
let close_frame = NSView::frame(&close);
|
||||||
let mut frame = NSView::frame(btn);
|
let button_spacing = NSView::frame(&miniaturize).origin.x - close_frame.origin.x;
|
||||||
frame.origin.y = centered_y;
|
for (index, button) in [&close, &miniaturize, &zoom].into_iter().enumerate() {
|
||||||
btn.setFrameOrigin(frame.origin);
|
let button_superview = match unsafe { button.superview() } {
|
||||||
|
Some(view) => view,
|
||||||
|
None => return,
|
||||||
|
};
|
||||||
|
let mut frame = NSView::frame(button);
|
||||||
|
let top_inset = ((TITLEBAR_HEIGHT - frame.size.height) / 2.0).max(0.0);
|
||||||
|
let mut target = frame.origin;
|
||||||
|
target.y = window_height - top_inset - frame.size.height;
|
||||||
|
frame.origin.y = button_superview.convertPoint_fromView(target, None).y;
|
||||||
|
frame.origin.x = LEFT_INSET + index as f64 * button_spacing;
|
||||||
|
button.setFrameOrigin(frame.origin);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -9,6 +9,7 @@ pub struct AppState {
|
|||||||
pub config: Mutex<AppConfig>,
|
pub config: Mutex<AppConfig>,
|
||||||
pub search_index: Mutex<Option<Arc<SearchIndex>>>,
|
pub search_index: Mutex<Option<Arc<SearchIndex>>>,
|
||||||
pub watcher: Mutex<Option<RecommendedWatcher>>,
|
pub watcher: Mutex<Option<RecommendedWatcher>>,
|
||||||
|
pub vault_transition: tokio::sync::Mutex<()>,
|
||||||
pub importing: AtomicBool,
|
pub importing: AtomicBool,
|
||||||
pub syncing: AtomicBool,
|
pub syncing: AtomicBool,
|
||||||
pub pending_open_file: Mutex<Option<String>>,
|
pub pending_open_file: Mutex<Option<String>>,
|
||||||
@@ -20,6 +21,7 @@ impl AppState {
|
|||||||
config: Mutex::new(config),
|
config: Mutex::new(config),
|
||||||
search_index: Mutex::new(None),
|
search_index: Mutex::new(None),
|
||||||
watcher: Mutex::new(None),
|
watcher: Mutex::new(None),
|
||||||
|
vault_transition: tokio::sync::Mutex::new(()),
|
||||||
importing: AtomicBool::new(false),
|
importing: AtomicBool::new(false),
|
||||||
syncing: AtomicBool::new(false),
|
syncing: AtomicBool::new(false),
|
||||||
pending_open_file: Mutex::new(None),
|
pending_open_file: Mutex::new(None),
|
||||||
|
|||||||
@@ -54,6 +54,8 @@ pub struct NoteContent {
|
|||||||
pub struct VaultConfig {
|
pub struct VaultConfig {
|
||||||
pub path: String,
|
pub path: String,
|
||||||
pub name: String,
|
pub name: String,
|
||||||
|
#[serde(default)]
|
||||||
|
pub bookmark_id: Option<String>,
|
||||||
// Per-vault WebDAV sync (was previously global on AppConfig; those fields are kept deprecated for migration).
|
// Per-vault WebDAV sync (was previously global on AppConfig; those fields are kept deprecated for migration).
|
||||||
#[serde(default)]
|
#[serde(default)]
|
||||||
pub sync_provider: Option<String>,
|
pub sync_provider: Option<String>,
|
||||||
@@ -98,6 +100,8 @@ pub struct CustomTheme {
|
|||||||
pub struct AppConfig {
|
pub struct AppConfig {
|
||||||
pub vaults: Vec<VaultConfig>,
|
pub vaults: Vec<VaultConfig>,
|
||||||
pub active_vault: Option<String>,
|
pub active_vault: Option<String>,
|
||||||
|
#[serde(default)]
|
||||||
|
pub active_bookmark_id: Option<String>,
|
||||||
pub theme: String,
|
pub theme: String,
|
||||||
#[serde(default)]
|
#[serde(default)]
|
||||||
pub accent_color: Option<String>,
|
pub accent_color: Option<String>,
|
||||||
@@ -255,6 +259,7 @@ impl Default for AppConfig {
|
|||||||
Self {
|
Self {
|
||||||
vaults: Vec::new(),
|
vaults: Vec::new(),
|
||||||
active_vault: None,
|
active_vault: None,
|
||||||
|
active_bookmark_id: None,
|
||||||
theme: "system".to_string(),
|
theme: "system".to_string(),
|
||||||
accent_color: None,
|
accent_color: None,
|
||||||
font_size: None,
|
font_size: None,
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
{
|
{
|
||||||
"$schema": "../node_modules/@tauri-apps/cli/config.schema.json",
|
"$schema": "../node_modules/@tauri-apps/cli/config.schema.json",
|
||||||
"productName": "HelixNotes",
|
"productName": "HelixNotes",
|
||||||
"version": "1.3.3",
|
"version": "1.3.4",
|
||||||
"identifier": "com.helixnotes.app",
|
"identifier": "com.helixnotes.app",
|
||||||
"build": {
|
"build": {
|
||||||
"frontendDist": "../build",
|
"frontendDist": "../build",
|
||||||
|
|||||||
@@ -9,7 +9,7 @@
|
|||||||
"hiddenTitle": true,
|
"hiddenTitle": true,
|
||||||
"trafficLightPosition": {
|
"trafficLightPosition": {
|
||||||
"x": 12,
|
"x": 12,
|
||||||
"y": 12
|
"y": 10
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
|
|||||||
@@ -15,14 +15,23 @@ import type {
|
|||||||
BackupEntry,
|
BackupEntry,
|
||||||
VersionEntry,
|
VersionEntry,
|
||||||
TaskItem,
|
TaskItem,
|
||||||
|
ExternalVaultResult,
|
||||||
} from "./types";
|
} from "./types";
|
||||||
|
|
||||||
export async function openVault(path: string): Promise<void> {
|
export async function openVault(path: string): Promise<void> {
|
||||||
return invoke("open_vault", { path });
|
return invoke("open_vault", { path });
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function removeVault(path: string): Promise<void> {
|
export async function chooseExternalVault(): Promise<ExternalVaultResult | null> {
|
||||||
return invoke("remove_vault", { path });
|
return invoke("choose_external_vault");
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function restoreExternalVault(bookmarkId: string): Promise<ExternalVaultResult> {
|
||||||
|
return invoke("restore_external_vault", { bookmarkId });
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function removeVault(path: string, bookmarkId?: string | null): Promise<void> {
|
||||||
|
return invoke("remove_vault", { path, bookmarkId: bookmarkId ?? null });
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function getAppConfig(): Promise<AppConfig> {
|
export async function getAppConfig(): Promise<AppConfig> {
|
||||||
|
|||||||
@@ -61,7 +61,7 @@
|
|||||||
const appWindow = getCurrentWindow();
|
const appWindow = getCurrentWindow();
|
||||||
const isMac = navigator.platform.startsWith('Mac');
|
const isMac = navigator.platform.startsWith('Mac');
|
||||||
const isMobile = $derived($platformIsMobile);
|
const isMobile = $derived($platformIsMobile);
|
||||||
import { loadVaultState, saveVaultState, readNote, createDailyNote, createBackup, getPendingOpenFile, addQuickAccess, removeQuickAccess, getQuickAccess, setTheme, syncNow, setTaskDone, setTaskPriority, setTaskDue, findOrphanedAttachments, trashOrphanedAttachments } from '$lib/api';
|
import { loadVaultState, saveVaultState, readNote, createDailyNote, createBackup, getPendingOpenFile, addQuickAccess, removeQuickAccess, getQuickAccess, setTheme, syncNow, getAppConfig, setTaskDone, setTaskPriority, setTaskDue, findOrphanedAttachments, trashOrphanedAttachments } from '$lib/api';
|
||||||
import { darkThemes, isAndroid } from '$lib/platform';
|
import { darkThemes, isAndroid } from '$lib/platform';
|
||||||
import { debounce } from '$lib/utils/debounce';
|
import { debounce } from '$lib/utils/debounce';
|
||||||
import { openNoteWindow } from '$lib/utils/window';
|
import { openNoteWindow } from '$lib/utils/window';
|
||||||
@@ -614,7 +614,9 @@
|
|||||||
// Run sidebar and note list refresh in parallel
|
// Run sidebar and note list refresh in parallel
|
||||||
await Promise.all([sidebar?.refresh(), noteList?.refresh()]);
|
await Promise.all([sidebar?.refresh(), noteList?.refresh()]);
|
||||||
|
|
||||||
// Auto-cleanup orphaned attachments in the background
|
// Full-vault attachment scans cause navigation stalls on mobile storage.
|
||||||
|
// Mobile users can run the same cleanup explicitly from the Info panel.
|
||||||
|
if (!isMobile) {
|
||||||
setTimeout(async () => {
|
setTimeout(async () => {
|
||||||
if (get(editorDirty)) return; // skip if user is actively editing
|
if (get(editorDirty)) return; // skip if user is actively editing
|
||||||
try {
|
try {
|
||||||
@@ -633,6 +635,7 @@
|
|||||||
}
|
}
|
||||||
} catch (_) {}
|
} catch (_) {}
|
||||||
}, 3000);
|
}, 3000);
|
||||||
|
}
|
||||||
|
|
||||||
// Restore the last session (view + open note) if enabled.
|
// Restore the last session (view + open note) if enabled.
|
||||||
if ($appConfig?.restore_last_session) {
|
if ($appConfig?.restore_last_session) {
|
||||||
@@ -729,11 +732,11 @@
|
|||||||
|
|
||||||
// ── WebDAV sync: global status + auto-sync triggers ──
|
// ── WebDAV sync: global status + auto-sync triggers ──
|
||||||
unlistenSync.push(await listen('sync-progress', () => syncState.set({ running: true, error: null })));
|
unlistenSync.push(await listen('sync-progress', () => syncState.set({ running: true, error: null })));
|
||||||
unlistenSync.push(await listen('sync-done', (event: any) => {
|
unlistenSync.push(await listen('sync-done', async () => {
|
||||||
syncState.set({ running: false, error: null });
|
syncState.set({ running: false, error: null });
|
||||||
const cur = get(appConfig);
|
try {
|
||||||
const ts = event.payload?.last_sync_time;
|
appConfig.set(await getAppConfig());
|
||||||
if (cur && ts) appConfig.set({ ...cur, vaults: cur.vaults.map((v) => v.path === cur.active_vault ? { ...v, last_sync_time: ts } : v) });
|
} catch {}
|
||||||
}));
|
}));
|
||||||
unlistenSync.push(await listen('sync-error', (event: any) => syncState.set({ running: false, error: event.payload?.error ?? 'Sync failed' })));
|
unlistenSync.push(await listen('sync-error', (event: any) => syncState.set({ running: false, error: event.payload?.error ?? 'Sync failed' })));
|
||||||
|
|
||||||
|
|||||||
@@ -1112,7 +1112,7 @@
|
|||||||
{/if}
|
{/if}
|
||||||
|
|
||||||
<!-- svelte-ignore a11y_no_static_element_interactions -->
|
<!-- svelte-ignore a11y_no_static_element_interactions -->
|
||||||
<div class="list-content" bind:this={listContainer} onscroll={onListScroll} ondblclick={handleListDoubleClick}>
|
<div class="list-content" class:tasks-mode={$viewMode === 'tasks'} bind:this={listContainer} onscroll={onListScroll} ondblclick={handleListDoubleClick}>
|
||||||
{#if $viewMode === 'tasks'}
|
{#if $viewMode === 'tasks'}
|
||||||
<TasksView onOpenTask={openTask} onToggleTask={onToggleTask} onSetTaskPriority={onSetTaskPriority} onSetTaskDue={onSetTaskDue} />
|
<TasksView onOpenTask={openTask} onToggleTask={onToggleTask} onSetTaskPriority={onSetTaskPriority} onSetTaskDue={onSetTaskDue} />
|
||||||
{/if}
|
{/if}
|
||||||
@@ -2213,6 +2213,11 @@
|
|||||||
padding: 4px 8px 180px;
|
padding: 4px 8px 180px;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.note-list.mobile .list-content.tasks-mode {
|
||||||
|
padding-bottom: 0;
|
||||||
|
overflow: hidden;
|
||||||
|
}
|
||||||
|
|
||||||
.note-list.mobile .note-item {
|
.note-list.mobile .note-item {
|
||||||
padding: 14px 16px;
|
padding: 14px 16px;
|
||||||
min-height: 56px;
|
min-height: 56px;
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import { showSettings, theme, appConfig, activeVaultConfig, updateAvailable as globalUpdateAvailable, updateObj as globalUpdateObj, installType, settingsTab, vaultReady, androidApkUrl, checkForUpdateMobile, notebookSortMode, isManagedInstall, customThemes } from '$lib/stores/app';
|
import { showSettings, theme, appConfig, activeVaultConfig, updateAvailable as globalUpdateAvailable, updateObj as globalUpdateObj, installType, settingsTab, vaultReady, androidApkUrl, checkForUpdateMobile, notebookSortMode, isManagedInstall, customThemes } from '$lib/stores/app';
|
||||||
import { setTheme, setAccentColor, setFontSize, setFontFamily, setLineHeight, setUiScale, setContentWidth, setGeneralSettings, importObsidian, createBackup, listBackups, restoreBackup, deleteBackup, setBackupSettings, setAiSettings, testAiConnection, setSyncSettings, testSyncConnection, syncNow, saveCustomTheme, deleteCustomTheme, exportCustomTheme, importCustomThemes } from '$lib/api';
|
import { setTheme, setAccentColor, setFontSize, setFontFamily, setLineHeight, setUiScale, setContentWidth, setGeneralSettings, importObsidian, createBackup, listBackups, restoreBackup, deleteBackup, setBackupSettings, setAiSettings, testAiConnection, setSyncSettings, testSyncConnection, syncNow, getAppConfig, saveCustomTheme, deleteCustomTheme, exportCustomTheme, importCustomThemes } from '$lib/api';
|
||||||
import { darkThemes, isMobile, isAndroid } from '$lib/platform';
|
import { darkThemes, isMobile, isAndroid } from '$lib/platform';
|
||||||
import { open as openDialog, save as saveDialog } from '@tauri-apps/plugin-dialog';
|
import { open as openDialog, save as saveDialog } from '@tauri-apps/plugin-dialog';
|
||||||
import { listen } from '@tauri-apps/api/event';
|
import { listen } from '@tauri-apps/api/event';
|
||||||
@@ -318,9 +318,13 @@
|
|||||||
// Re-seed the sync form when the active vault changes, so Settings reflects the current vault.
|
// Re-seed the sync form when the active vault changes, so Settings reflects the current vault.
|
||||||
let lastSyncVault: string | null = null;
|
let lastSyncVault: string | null = null;
|
||||||
$effect(() => {
|
$effect(() => {
|
||||||
const path = $appConfig?.active_vault ?? null;
|
const identity = $appConfig?.active_bookmark_id
|
||||||
if (path === lastSyncVault) return;
|
? `bookmark:${$appConfig.active_bookmark_id}`
|
||||||
lastSyncVault = path;
|
: $appConfig?.active_vault
|
||||||
|
? `path:${$appConfig.active_vault}`
|
||||||
|
: null;
|
||||||
|
if (identity === lastSyncVault) return;
|
||||||
|
lastSyncVault = identity;
|
||||||
const vc = activeVaultConfig($appConfig);
|
const vc = activeVaultConfig($appConfig);
|
||||||
syncProvider = vc?.sync_provider ?? null;
|
syncProvider = vc?.sync_provider ?? null;
|
||||||
syncUrl = vc?.webdav_url ?? '';
|
syncUrl = vc?.webdav_url ?? '';
|
||||||
@@ -337,10 +341,11 @@
|
|||||||
// triggers update live, without an app restart.
|
// triggers update live, without an app restart.
|
||||||
if ($appConfig) {
|
if ($appConfig) {
|
||||||
const cur = $appConfig;
|
const cur = $appConfig;
|
||||||
|
const active = activeVaultConfig(cur);
|
||||||
$appConfig = {
|
$appConfig = {
|
||||||
...cur,
|
...cur,
|
||||||
vaults: cur.vaults.map((v) => v.path === cur.active_vault ? {
|
vaults: cur.vaults.map((vault) => vault === active ? {
|
||||||
...v,
|
...vault,
|
||||||
sync_provider: syncProvider,
|
sync_provider: syncProvider,
|
||||||
webdav_url: syncUrl || null,
|
webdav_url: syncUrl || null,
|
||||||
webdav_username: syncUsername || null,
|
webdav_username: syncUsername || null,
|
||||||
@@ -348,7 +353,7 @@
|
|||||||
sync_on_open: syncOnOpen,
|
sync_on_open: syncOnOpen,
|
||||||
sync_on_change: syncOnChange,
|
sync_on_change: syncOnChange,
|
||||||
sync_interval_minutes: syncIntervalMinutes,
|
sync_interval_minutes: syncIntervalMinutes,
|
||||||
} : v),
|
} : vault),
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -379,7 +384,7 @@
|
|||||||
syncMessage = null;
|
syncMessage = null;
|
||||||
const unlisteners: Array<() => void> = [];
|
const unlisteners: Array<() => void> = [];
|
||||||
const cleanup = () => { unlisteners.forEach((u) => u()); };
|
const cleanup = () => { unlisteners.forEach((u) => u()); };
|
||||||
unlisteners.push(await listen<{ success: boolean; summary?: { uploaded?: number; downloaded?: number; deleted_local?: number; deleted_remote?: number; conflicts?: number }; last_sync_time?: string }>('sync-done', (event) => {
|
unlisteners.push(await listen<{ success: boolean; summary?: { uploaded?: number; downloaded?: number; deleted_local?: number; deleted_remote?: number; conflicts?: number }; last_sync_time?: string }>('sync-done', async (event) => {
|
||||||
const s = event.payload.summary ?? {};
|
const s = event.payload.summary ?? {};
|
||||||
const parts: string[] = [];
|
const parts: string[] = [];
|
||||||
if (s.uploaded) parts.push(`${s.uploaded} uploaded`);
|
if (s.uploaded) parts.push(`${s.uploaded} uploaded`);
|
||||||
@@ -389,10 +394,10 @@
|
|||||||
if (s.conflicts) parts.push(`${s.conflicts} conflict copies`);
|
if (s.conflicts) parts.push(`${s.conflicts} conflict copies`);
|
||||||
syncMessage = { type: 'success', text: parts.length ? `Synced: ${parts.join(', ')}.` : 'Already up to date.' };
|
syncMessage = { type: 'success', text: parts.length ? `Synced: ${parts.join(', ')}.` : 'Already up to date.' };
|
||||||
syncRunning = false;
|
syncRunning = false;
|
||||||
if ($appConfig && event.payload.last_sync_time) {
|
if (event.payload.last_sync_time) {
|
||||||
const cur = $appConfig;
|
try {
|
||||||
const ts = event.payload.last_sync_time;
|
$appConfig = await getAppConfig();
|
||||||
$appConfig = { ...cur, vaults: cur.vaults.map((v) => v.path === cur.active_vault ? { ...v, last_sync_time: ts } : v) };
|
} catch {}
|
||||||
}
|
}
|
||||||
cleanup();
|
cleanup();
|
||||||
}));
|
}));
|
||||||
|
|||||||
@@ -807,6 +807,7 @@
|
|||||||
class="notebook-item"
|
class="notebook-item"
|
||||||
class:active={$viewMode === 'notebook' && $activeNotebook?.relative_path === ''}
|
class:active={$viewMode === 'notebook' && $activeNotebook?.relative_path === ''}
|
||||||
onclick={selectUnfiled}
|
onclick={selectUnfiled}
|
||||||
|
style="padding-left: 4px"
|
||||||
>
|
>
|
||||||
<span style="width:14px;flex-shrink:0"></span>
|
<span style="width:14px;flex-shrink:0"></span>
|
||||||
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" style="flex-shrink:0;opacity:0.6">
|
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" style="flex-shrink:0;opacity:0.6">
|
||||||
|
|||||||
@@ -2,11 +2,13 @@
|
|||||||
import { open } from '@tauri-apps/plugin-dialog';
|
import { open } from '@tauri-apps/plugin-dialog';
|
||||||
import { getCurrentWindow } from '@tauri-apps/api/window';
|
import { getCurrentWindow } from '@tauri-apps/api/window';
|
||||||
import { documentDir } from '@tauri-apps/api/path';
|
import { documentDir } from '@tauri-apps/api/path';
|
||||||
import { openVault, removeVault, getAppConfig } from '$lib/api';
|
import { chooseExternalVault, getAppConfig, openVault, removeVault, restoreExternalVault } from '$lib/api';
|
||||||
import { appConfig, vaultReady } from '$lib/stores/app';
|
import { appConfig, vaultReady } from '$lib/stores/app';
|
||||||
import { isAndroid, isIOS, isMobile } from '$lib/platform';
|
import { isAndroid, isIOS, isMobile } from '$lib/platform';
|
||||||
import type { VaultConfig } from '$lib/types';
|
import type { VaultConfig } from '$lib/types';
|
||||||
|
|
||||||
|
let { initialError = '' }: { initialError?: string } = $props();
|
||||||
|
|
||||||
const appWindow = getCurrentWindow();
|
const appWindow = getCurrentWindow();
|
||||||
|
|
||||||
let recentVaults: VaultConfig[] = $derived($appConfig?.vaults ?? []);
|
let recentVaults: VaultConfig[] = $derived($appConfig?.vaults ?? []);
|
||||||
@@ -15,6 +17,10 @@
|
|||||||
let vaultName = $state('HelixNotes');
|
let vaultName = $state('HelixNotes');
|
||||||
let hasPermission = $state(!isAndroid);
|
let hasPermission = $state(!isAndroid);
|
||||||
let selectedLocation = $state('Documents');
|
let selectedLocation = $state('Documents');
|
||||||
|
|
||||||
|
$effect(() => {
|
||||||
|
if (initialError && !error) error = initialError;
|
||||||
|
});
|
||||||
let customPath = $state('');
|
let customPath = $state('');
|
||||||
let iosBasePath = $state('');
|
let iosBasePath = $state('');
|
||||||
|
|
||||||
@@ -108,14 +114,63 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async function forgetVault(path: string) {
|
async function pickExternalFolder() {
|
||||||
|
loading = true;
|
||||||
|
error = '';
|
||||||
try {
|
try {
|
||||||
await removeVault(path);
|
const selected = await chooseExternalVault();
|
||||||
|
if (!selected) return;
|
||||||
$appConfig = await getAppConfig();
|
$appConfig = await getAppConfig();
|
||||||
|
$vaultReady = true;
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
error = String(e);
|
error = String(e);
|
||||||
|
} finally {
|
||||||
|
loading = false;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async function openRecentVault(vault: VaultConfig) {
|
||||||
|
if (loading) return;
|
||||||
|
if (!isIOS || !vault.bookmark_id) {
|
||||||
|
await openSelectedVault(vault.path);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
loading = true;
|
||||||
|
error = '';
|
||||||
|
try {
|
||||||
|
await restoreExternalVault(vault.bookmark_id);
|
||||||
|
$appConfig = await getAppConfig();
|
||||||
|
$vaultReady = true;
|
||||||
|
} catch (e) {
|
||||||
|
error = String(e);
|
||||||
|
} finally {
|
||||||
|
loading = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function forgetVault(vault: VaultConfig) {
|
||||||
|
if (loading) return;
|
||||||
|
loading = true;
|
||||||
|
error = '';
|
||||||
|
try {
|
||||||
|
const wasActive = isActiveVault(vault);
|
||||||
|
await removeVault(vault.path, vault.bookmark_id);
|
||||||
|
$appConfig = await getAppConfig();
|
||||||
|
if (wasActive) $vaultReady = false;
|
||||||
|
} catch (e) {
|
||||||
|
error = String(e);
|
||||||
|
} finally {
|
||||||
|
loading = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function isActiveVault(vault: VaultConfig): boolean {
|
||||||
|
if (vault.bookmark_id) {
|
||||||
|
return vault.bookmark_id === $appConfig?.active_bookmark_id;
|
||||||
|
}
|
||||||
|
return !$appConfig?.active_bookmark_id && vault.path === $appConfig?.active_vault;
|
||||||
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<!-- svelte-ignore a11y_no_static_element_interactions -->
|
<!-- svelte-ignore a11y_no_static_element_interactions -->
|
||||||
@@ -146,7 +201,7 @@
|
|||||||
<p class="subtitle">Local markdown notes</p>
|
<p class="subtitle">Local markdown notes</p>
|
||||||
{#if isMobile}
|
{#if isMobile}
|
||||||
{#if isIOS}
|
{#if isIOS}
|
||||||
<p class="description">Your notes are saved in the app's folder and appear in the Files app under "On My iPhone → HelixNotes".</p>
|
<p class="description">Keep notes in HelixNotes, or use an existing folder from iCloud Drive or Files.</p>
|
||||||
{:else}
|
{:else}
|
||||||
<p class="description">Choose where to store your notes. Sync with Syncthing, Nextcloud, or any file sync app.</p>
|
<p class="description">Choose where to store your notes. Sync with Syncthing, Nextcloud, or any file sync app.</p>
|
||||||
{/if}
|
{/if}
|
||||||
@@ -197,6 +252,8 @@
|
|||||||
<button class="btn-primary" onclick={pickFolder} disabled={loading}>
|
<button class="btn-primary" onclick={pickFolder} disabled={loading}>
|
||||||
{#if loading}
|
{#if loading}
|
||||||
Opening...
|
Opening...
|
||||||
|
{:else if isIOS}
|
||||||
|
Use Local Folder
|
||||||
{:else if isMobile}
|
{:else if isMobile}
|
||||||
Get Started
|
Get Started
|
||||||
{:else}
|
{:else}
|
||||||
@@ -204,6 +261,12 @@
|
|||||||
{/if}
|
{/if}
|
||||||
</button>
|
</button>
|
||||||
|
|
||||||
|
{#if isIOS}
|
||||||
|
<button class="btn-back" onclick={pickExternalFolder} disabled={loading}>
|
||||||
|
Choose Folder in Files
|
||||||
|
</button>
|
||||||
|
{/if}
|
||||||
|
|
||||||
{#if $appConfig?.active_vault}
|
{#if $appConfig?.active_vault}
|
||||||
<button class="btn-back" onclick={() => ($vaultReady = true)}>
|
<button class="btn-back" onclick={() => ($vaultReady = true)}>
|
||||||
Back
|
Back
|
||||||
@@ -215,11 +278,11 @@
|
|||||||
<span class="recent-label">Recent</span>
|
<span class="recent-label">Recent</span>
|
||||||
{#each recentVaults as vault}
|
{#each recentVaults as vault}
|
||||||
<div class="vault-row">
|
<div class="vault-row">
|
||||||
<button class="vault-item" class:current={vault.path === $appConfig?.active_vault} onclick={() => openSelectedVault(vault.path)}>
|
<button class="vault-item" class:current={isActiveVault(vault)} onclick={() => openRecentVault(vault)} disabled={loading}>
|
||||||
<span class="vault-name">{vault.name}{#if vault.path === $appConfig?.active_vault}<span class="vault-current-badge">Current</span>{/if}</span>
|
<span class="vault-name">{vault.name}{#if isActiveVault(vault)}<span class="vault-current-badge">Current</span>{/if}</span>
|
||||||
<span class="vault-path">{vault.path}</span>
|
<span class="vault-path">{vault.bookmark_id ? `Files · ${vault.path}` : vault.path}</span>
|
||||||
</button>
|
</button>
|
||||||
<button class="vault-remove" title="Remove from list" aria-label="Remove from list" onclick={() => forgetVault(vault.path)}>
|
<button class="vault-remove" title="Remove from list" aria-label="Remove from list" onclick={() => forgetVault(vault)} disabled={loading}>
|
||||||
<svg width="12" height="12" viewBox="0 0 10 10">
|
<svg width="12" height="12" viewBox="0 0 10 10">
|
||||||
<line x1="1.5" y1="1.5" x2="8.5" y2="8.5" stroke="currentColor" stroke-width="1.3" stroke-linecap="round" />
|
<line x1="1.5" y1="1.5" x2="8.5" y2="8.5" stroke="currentColor" stroke-width="1.3" stroke-linecap="round" />
|
||||||
<line x1="8.5" y1="1.5" x2="1.5" y2="8.5" stroke="currentColor" stroke-width="1.3" stroke-linecap="round" />
|
<line x1="8.5" y1="1.5" x2="1.5" y2="8.5" stroke="currentColor" stroke-width="1.3" stroke-linecap="round" />
|
||||||
|
|||||||
@@ -18,7 +18,10 @@ export const appConfig = writable<AppConfig | null>(null);
|
|||||||
// The active vault's config entry (where per-vault WebDAV sync settings live).
|
// The active vault's config entry (where per-vault WebDAV sync settings live).
|
||||||
export function activeVaultConfig(c: AppConfig | null): VaultConfig | null {
|
export function activeVaultConfig(c: AppConfig | null): VaultConfig | null {
|
||||||
if (!c?.active_vault) return null;
|
if (!c?.active_vault) return null;
|
||||||
return c.vaults.find((v) => v.path === c.active_vault) ?? null;
|
if (c.active_bookmark_id) {
|
||||||
|
return c.vaults.find((v) => v.bookmark_id === c.active_bookmark_id) ?? null;
|
||||||
|
}
|
||||||
|
return c.vaults.find((v) => !v.bookmark_id && v.path === c.active_vault) ?? null;
|
||||||
}
|
}
|
||||||
export const vaultReady = writable(false);
|
export const vaultReady = writable(false);
|
||||||
|
|
||||||
|
|||||||
@@ -44,6 +44,7 @@ export interface NoteContent {
|
|||||||
export interface VaultConfig {
|
export interface VaultConfig {
|
||||||
path: string;
|
path: string;
|
||||||
name: string;
|
name: string;
|
||||||
|
bookmark_id?: string | null;
|
||||||
// Per-vault WebDAV sync (moved off the global AppConfig).
|
// Per-vault WebDAV sync (moved off the global AppConfig).
|
||||||
sync_provider?: string | null;
|
sync_provider?: string | null;
|
||||||
webdav_url?: string | null;
|
webdav_url?: string | null;
|
||||||
@@ -55,6 +56,12 @@ export interface VaultConfig {
|
|||||||
last_sync_time?: string | null;
|
last_sync_time?: string | null;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export interface ExternalVaultResult {
|
||||||
|
bookmarkId: string;
|
||||||
|
path: string;
|
||||||
|
name: string;
|
||||||
|
}
|
||||||
|
|
||||||
export interface CustomThemeColors {
|
export interface CustomThemeColors {
|
||||||
bg_primary: string;
|
bg_primary: string;
|
||||||
bg_secondary: string;
|
bg_secondary: string;
|
||||||
@@ -77,6 +84,7 @@ export interface CustomTheme {
|
|||||||
export interface AppConfig {
|
export interface AppConfig {
|
||||||
vaults: VaultConfig[];
|
vaults: VaultConfig[];
|
||||||
active_vault: string | null;
|
active_vault: string | null;
|
||||||
|
active_bookmark_id?: string | null;
|
||||||
theme: string;
|
theme: string;
|
||||||
accent_color: string | null;
|
accent_color: string | null;
|
||||||
font_size: number | null;
|
font_size: number | null;
|
||||||
|
|||||||
@@ -1,17 +1,20 @@
|
|||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import { onDestroy, onMount } from 'svelte';
|
import { onDestroy, onMount } from 'svelte';
|
||||||
import { appConfig, vaultReady, theme } from '$lib/stores/app';
|
import { appConfig, vaultReady, theme } from '$lib/stores/app';
|
||||||
import { getAppConfig, openVault, setFontSize } from '$lib/api';
|
import { getAppConfig, openVault, restoreExternalVault, setFontSize } from '$lib/api';
|
||||||
import { darkThemes } from '$lib/platform';
|
import { darkThemes, isIOS } from '$lib/platform';
|
||||||
import { getCurrentWebview } from '@tauri-apps/api/webview';
|
import { getCurrentWebview } from '@tauri-apps/api/webview';
|
||||||
|
import { listen } from '@tauri-apps/api/event';
|
||||||
import VaultPicker from '$lib/components/VaultPicker.svelte';
|
import VaultPicker from '$lib/components/VaultPicker.svelte';
|
||||||
import AppLayout from '$lib/components/AppLayout.svelte';
|
import AppLayout from '$lib/components/AppLayout.svelte';
|
||||||
import NoteWindow from '$lib/components/NoteWindow.svelte';
|
import NoteWindow from '$lib/components/NoteWindow.svelte';
|
||||||
|
|
||||||
let loading = $state(true);
|
let loading = $state(true);
|
||||||
let noteWindowPath = $state<string | null>(null);
|
let noteWindowPath = $state<string | null>(null);
|
||||||
|
let startupVaultError = $state('');
|
||||||
let fontSizeSaveTimer: ReturnType<typeof setTimeout> | null = null;
|
let fontSizeSaveTimer: ReturnType<typeof setTimeout> | null = null;
|
||||||
let removeEditorZoomShortcuts: (() => void) | null = null;
|
let removeEditorZoomShortcuts: (() => void) | null = null;
|
||||||
|
let removeFontSizeListener: (() => void) | null = null;
|
||||||
|
|
||||||
const defaultEditorFontSize = 14;
|
const defaultEditorFontSize = 14;
|
||||||
const minEditorFontSize = 10;
|
const minEditorFontSize = 10;
|
||||||
@@ -55,12 +58,17 @@
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
function setEditorFontSize(value: number) {
|
function applyEditorFontSize(value: number) {
|
||||||
const nextSize = Math.max(minEditorFontSize, Math.min(maxEditorFontSize, Math.round(value)));
|
const nextSize = Math.max(minEditorFontSize, Math.min(maxEditorFontSize, Math.round(value)));
|
||||||
const restoreEditorViewport = preserveEditorViewportAfterLayout(getEditorScrollSurface());
|
const restoreEditorViewport = preserveEditorViewportAfterLayout(getEditorScrollSurface());
|
||||||
if ($appConfig) $appConfig.font_size = nextSize;
|
if ($appConfig) $appConfig.font_size = nextSize;
|
||||||
document.documentElement.style.setProperty('--editor-font-size', `${nextSize}px`);
|
document.documentElement.style.setProperty('--editor-font-size', `${nextSize}px`);
|
||||||
restoreEditorViewport();
|
restoreEditorViewport();
|
||||||
|
return nextSize;
|
||||||
|
}
|
||||||
|
|
||||||
|
function setEditorFontSize(value: number) {
|
||||||
|
const nextSize = applyEditorFontSize(value);
|
||||||
if (fontSizeSaveTimer) clearTimeout(fontSizeSaveTimer);
|
if (fontSizeSaveTimer) clearTimeout(fontSizeSaveTimer);
|
||||||
fontSizeSaveTimer = setTimeout(() => {
|
fontSizeSaveTimer = setTimeout(() => {
|
||||||
setFontSize(nextSize).catch((e) => console.error('Failed to save font size:', e));
|
setFontSize(nextSize).catch((e) => console.error('Failed to save font size:', e));
|
||||||
@@ -116,6 +124,9 @@
|
|||||||
try {
|
try {
|
||||||
const config = await getAppConfig();
|
const config = await getAppConfig();
|
||||||
$appConfig = config;
|
$appConfig = config;
|
||||||
|
removeFontSizeListener = await listen<number>('editor-font-size-changed', (event) => {
|
||||||
|
if ($appConfig?.font_size !== event.payload) applyEditorFontSize(event.payload);
|
||||||
|
});
|
||||||
$theme = config.theme || 'system';
|
$theme = config.theme || 'system';
|
||||||
|
|
||||||
// Apply theme immediately to prevent flash
|
// Apply theme immediately to prevent flash
|
||||||
@@ -132,9 +143,7 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Apply saved font settings
|
// Apply saved font settings
|
||||||
if (config.font_size) {
|
if (config.font_size) applyEditorFontSize(config.font_size);
|
||||||
document.documentElement.style.setProperty('--editor-font-size', `${config.font_size}px`);
|
|
||||||
}
|
|
||||||
if (config.line_height) {
|
if (config.line_height) {
|
||||||
document.documentElement.style.setProperty('--editor-line-height', String(config.line_height));
|
document.documentElement.style.setProperty('--editor-line-height', String(config.line_height));
|
||||||
}
|
}
|
||||||
@@ -192,26 +201,43 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Apply saved interface scale (desktop main window only; no-ops gracefully elsewhere)
|
// Apply saved interface scale to every desktop window; no-ops gracefully elsewhere.
|
||||||
if (!noteWindowPath && config.ui_scale && config.ui_scale !== 1) {
|
if (config.ui_scale && config.ui_scale !== 1) {
|
||||||
try {
|
try {
|
||||||
await getCurrentWebview().setZoom(config.ui_scale);
|
await getCurrentWebview().setZoom(config.ui_scale);
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
console.error('Failed to apply interface scale:', e);
|
console.error('Failed to apply interface scale:', e);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if (!noteWindowPath) removeEditorZoomShortcuts = installEditorZoomShortcuts();
|
removeEditorZoomShortcuts = installEditorZoomShortcuts();
|
||||||
|
|
||||||
// Auto-open last vault if available
|
// Auto-open last vault if available
|
||||||
if (config.active_vault) {
|
if (config.active_vault) {
|
||||||
|
const activeVault = config.active_bookmark_id
|
||||||
|
? config.vaults.find((vault) => vault.bookmark_id === config.active_bookmark_id)
|
||||||
|
: config.vaults.find(
|
||||||
|
(vault) => !vault.bookmark_id && vault.path === config.active_vault
|
||||||
|
);
|
||||||
try {
|
try {
|
||||||
// Note windows don't re-open the vault (already open in main process)
|
// Note windows don't re-open the vault (already open in main process)
|
||||||
if (!noteWindowPath) {
|
if (!noteWindowPath) {
|
||||||
|
if (isIOS && activeVault?.bookmark_id) {
|
||||||
|
await restoreExternalVault(activeVault.bookmark_id);
|
||||||
|
$appConfig = await getAppConfig();
|
||||||
|
} else {
|
||||||
await openVault(config.active_vault);
|
await openVault(config.active_vault);
|
||||||
}
|
}
|
||||||
|
}
|
||||||
$vaultReady = true;
|
$vaultReady = true;
|
||||||
} catch {
|
} catch (e) {
|
||||||
// Vault path might not exist anymore
|
if (isIOS && activeVault?.bookmark_id) {
|
||||||
|
startupVaultError = String(e);
|
||||||
|
$appConfig = {
|
||||||
|
...config,
|
||||||
|
active_vault: null,
|
||||||
|
active_bookmark_id: null
|
||||||
|
};
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
} catch {
|
} catch {
|
||||||
@@ -222,6 +248,7 @@
|
|||||||
|
|
||||||
onDestroy(() => {
|
onDestroy(() => {
|
||||||
removeEditorZoomShortcuts?.();
|
removeEditorZoomShortcuts?.();
|
||||||
|
removeFontSizeListener?.();
|
||||||
if (fontSizeSaveTimer) clearTimeout(fontSizeSaveTimer);
|
if (fontSizeSaveTimer) clearTimeout(fontSizeSaveTimer);
|
||||||
});
|
});
|
||||||
</script>
|
</script>
|
||||||
@@ -235,7 +262,7 @@
|
|||||||
{:else if $vaultReady}
|
{:else if $vaultReady}
|
||||||
<AppLayout />
|
<AppLayout />
|
||||||
{:else}
|
{:else}
|
||||||
<VaultPicker />
|
<VaultPicker initialError={startupVaultError} />
|
||||||
{/if}
|
{/if}
|
||||||
|
|
||||||
<style>
|
<style>
|
||||||
|
|||||||