diff --git a/.github/workflows/rust.yml b/.github/workflows/rust.yml index 1326c5278..7bda625cd 100644 --- a/.github/workflows/rust.yml +++ b/.github/workflows/rust.yml @@ -38,18 +38,8 @@ jobs: - name: Install tauri system deps run: | sudo apt-get update -y - sudo apt-get install -y \ - libwebkit2gtk-4.0-dev \ - build-essential \ - curl \ - wget \ - libssl-dev \ - libgtk-3-dev \ - libayatana-appindicator3-dev \ - librsvg2-dev - cargo install tauri-cli --version "^1.0.5" - cargo install --locked trunk - + make setup-dev-linux + make setup-dev - name: Build sidecar uses: actions-rs/cargo@v1 with: diff --git a/Cargo.lock b/Cargo.lock index 3781bb39e..a25db7c40 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1851,9 +1851,9 @@ dependencies = [ [[package]] name = "gloo" -version = "0.7.0" +version = "0.8.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "03e691526c3972d1fda35453f6df29925edea014dc75a2dede7661527e9439f0" +checksum = "3a4bef6b277b3ab073253d4bca60761240cf8d6998f4bd142211957b69a61b20" dependencies = [ "gloo-console", "gloo-dialogs", @@ -1999,9 +1999,9 @@ dependencies = [ [[package]] name = "gloo-worker" -version = "0.1.2" +version = "0.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "09110b5555bcafe508cee0fb94308af9aac7a85f980d3c88b270d117c6c6911d" +checksum = "9caac1b89bbe1e1454bb23e4d046a3fc92438ae2e95fb429c41685789e1fcbaa" dependencies = [ "anymap2", "bincode", @@ -2009,8 +2009,8 @@ dependencies = [ "gloo-utils", "js-sys", "serde", - "slab", "wasm-bindgen", + "wasm-bindgen-futures", "web-sys", ] @@ -2751,6 +2751,13 @@ dependencies = [ "safemem", ] +[[package]] +name = "local-file-indexer" +version = "0.1.0" +dependencies = [ + "spyglass-plugin", +] + [[package]] name = "lock_api" version = "0.4.7" @@ -5029,13 +5036,15 @@ dependencies = [ name = "spyglass-client" version = "0.1.0" dependencies = [ - "gloo 0.7.0", + "gloo 0.8.0", "js-sys", "log", "num-format", "serde", "serde_json", "shared", + "strum 0.24.1", + "strum_macros 0.24.2", "wasm-bindgen", "wasm-bindgen-futures", "wasm-logger", diff --git a/Cargo.toml b/Cargo.toml index 63bc28a11..2183219ac 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -11,6 +11,7 @@ members = [ # Default plugins "plugins/chrome-importer", "plugins/firefox-importer", + "plugins/local-file-indexer", ] [profile.release] diff --git a/Makefile b/Makefile index 5956bafe8..1a7d518c1 100644 --- a/Makefile +++ b/Makefile @@ -30,7 +30,7 @@ build-plugins-release: cargo build -p firefox-importer --target wasm32-wasi --release cp target/wasm32-wasi/release/firefox-importer.wasm assets/plugins/firefox-importer/main.wasm -build-release: build-backend build-styles +build-release: build-backend build-styles build-plugins-release cargo tauri build check: @@ -46,10 +46,11 @@ test: cargo test --all setup-dev: + rustup target add wasm32-unknown-unknown # Required for plugin development rustup target add wasm32-wasi # Install tauri-cli & trunk for client development - cargo install tauri-cli --locked --version ^1.0.0 + cargo install tauri-cli --locked --version ^1.0.5 cargo install --locked trunk # Install tailwind cd ./crates/client && npm install @@ -66,3 +67,6 @@ setup-dev-linux: run-client-dev: cargo tauri dev + +run-client-headless: + cd ./crates/client && HEADLESS_CLIENT=true trunk serve \ No newline at end of file diff --git a/crates/client/Cargo.toml b/crates/client/Cargo.toml index 5a96a3a70..6c5d920fe 100644 --- a/crates/client/Cargo.toml +++ b/crates/client/Cargo.toml @@ -8,12 +8,14 @@ edition = "2021" [dependencies] js-sys = "0.3" log = "0.4" -gloo = "0.7.0" +gloo = "0.8.0" num-format = { version = "0.4", default-features = false } serde = { version = "1.0", features = ["derive"] } serde_json = "1.0" "shared" = { path = "../shared" } -wasm-bindgen = "0.2" +strum = "0.24" +strum_macros = "0.24" +wasm-bindgen = { version = "0.2", features = ["serde-serialize"] } wasm-bindgen-futures = "0.4" wasm-logger = "0.2.0" web-sys = { version = "0.3", features = ["VisibilityState"] } diff --git a/crates/client/Trunk.toml b/crates/client/Trunk.toml new file mode 100644 index 000000000..0292379e0 --- /dev/null +++ b/crates/client/Trunk.toml @@ -0,0 +1,4 @@ + +[build] +target = "index.html" +dist = "dist" \ No newline at end of file diff --git a/crates/client/build.rs b/crates/client/build.rs new file mode 100644 index 000000000..28b146824 --- /dev/null +++ b/crates/client/build.rs @@ -0,0 +1,8 @@ +// Example custom build script. +fn main() { + // Tell Cargo that if the given file changes, to rerun this build script. + let is_headless = option_env!("HEADLESS_CLIENT"); + if is_headless.is_some() { + println!("cargo:rustc-cfg=headless"); + } +} diff --git a/crates/client/package-lock.json b/crates/client/package-lock.json index 1519a06d7..5346c46a6 100644 --- a/crates/client/package-lock.json +++ b/crates/client/package-lock.json @@ -5,6 +5,7 @@ "packages": { "": { "devDependencies": { + "@tailwindcss/forms": "^0.5.2", "tailwindcss": "^3.0.24" } }, @@ -43,6 +44,18 @@ "node": ">= 8" } }, + "node_modules/@tailwindcss/forms": { + "version": "0.5.2", + "resolved": "https://registry.npmjs.org/@tailwindcss/forms/-/forms-0.5.2.tgz", + "integrity": "sha512-pSrFeJB6Bg1Mrg9CdQW3+hqZXAKsBrSG9MAfFLKy1pVA4Mb4W7C0k7mEhlmS2Dfo/otxrQOET7NJiJ9RrS563w==", + "dev": true, + "dependencies": { + "mini-svg-data-uri": "^1.2.3" + }, + "peerDependencies": { + "tailwindcss": ">=3.0.0 || >= 3.0.0-alpha.1" + } + }, "node_modules/acorn": { "version": "7.4.1", "resolved": "https://registry.npmjs.org/acorn/-/acorn-7.4.1.tgz", @@ -394,6 +407,15 @@ "node": ">=8.6" } }, + "node_modules/mini-svg-data-uri": { + "version": "1.4.4", + "resolved": "https://registry.npmjs.org/mini-svg-data-uri/-/mini-svg-data-uri-1.4.4.tgz", + "integrity": "sha512-r9deDe9p5FJUPZAk3A59wGH7Ii9YrjjWw0jmw/liSbHl2CHiyXj6FcDXDu2K3TjVAXqiJdaw3xxwlZZr9E6nHg==", + "dev": true, + "bin": { + "mini-svg-data-uri": "cli.js" + } + }, "node_modules/minimist": { "version": "1.2.6", "resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.6.tgz", @@ -782,6 +804,15 @@ "fastq": "^1.6.0" } }, + "@tailwindcss/forms": { + "version": "0.5.2", + "resolved": "https://registry.npmjs.org/@tailwindcss/forms/-/forms-0.5.2.tgz", + "integrity": "sha512-pSrFeJB6Bg1Mrg9CdQW3+hqZXAKsBrSG9MAfFLKy1pVA4Mb4W7C0k7mEhlmS2Dfo/otxrQOET7NJiJ9RrS563w==", + "dev": true, + "requires": { + "mini-svg-data-uri": "^1.2.3" + } + }, "acorn": { "version": "7.4.1", "resolved": "https://registry.npmjs.org/acorn/-/acorn-7.4.1.tgz", @@ -1044,6 +1075,12 @@ "picomatch": "^2.3.1" } }, + "mini-svg-data-uri": { + "version": "1.4.4", + "resolved": "https://registry.npmjs.org/mini-svg-data-uri/-/mini-svg-data-uri-1.4.4.tgz", + "integrity": "sha512-r9deDe9p5FJUPZAk3A59wGH7Ii9YrjjWw0jmw/liSbHl2CHiyXj6FcDXDu2K3TjVAXqiJdaw3xxwlZZr9E6nHg==", + "dev": true + }, "minimist": { "version": "1.2.6", "resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.6.tgz", diff --git a/crates/client/package.json b/crates/client/package.json index 48785a003..fd051b357 100644 --- a/crates/client/package.json +++ b/crates/client/package.json @@ -1,5 +1,6 @@ { "devDependencies": { + "@tailwindcss/forms": "^0.5.2", "tailwindcss": "^3.0.24" } } diff --git a/crates/client/public/fixtures.js b/crates/client/public/fixtures.js new file mode 100644 index 000000000..e3c0b4e3b --- /dev/null +++ b/crates/client/public/fixtures.js @@ -0,0 +1,159 @@ +export let invoke = async (func_name, params) => { + console.log(`calling: ${func_name} w/`, params); + + if (func_name == "search_docs") { + return [{ + doc_id: "123", + domain: "google.com", + title: "This is an example title", + description: "Lorem ipsum dolor sit amet, consectetur adipiscing elit. Etiam et vulputate urna, sit amet semper metus.", + url: "https://google.com/this/is/a/path", + score: 1.0 + }, { + doc_id: "123", + domain: "example.com", + title: "This is an example title", + description: "Lorem ipsum dolor sit amet, consectetur adipiscing elit. Etiam et vulputate urna, sit amet semper metus.", + url: "https://example.com/this/is/a/path", + score: 1.0 + }]; + } else if (func_name == "search_lenses") { + return [{ + author: "a5huynh", + description: "Lorem ipsum dolor sit amet, consectetur adipiscing elit. Etiam et vulputate urna, sit amet semper metus.", + title: "fake_lense", + html_url: null, + download_url: null, + }, { + author: "a5huynh", + description: "Lorem ipsum dolor sit amet, consectetur adipiscing elit. Etiam et vulputate urna, sit amet semper metus.", + title: "fake_lense_2_boogaloo", + html_url: null, + download_url: null, + }]; + } else if (func_name == "list_installed_lenses") { + return [{ + author: "a5huynh", + description: "Lorem ipsum dolor sit amet, consectetur adipiscing elit. Etiam et vulputate urna, sit amet semper metus.", + title: "fake_lense", + html_url: null, + download_url: null, + }, { + author: "a5huynh", + description: "Lorem ipsum dolor sit amet, consectetur adipiscing elit. Etiam et vulputate urna, sit amet semper metus.", + title: "fake_lense_2_boogaloo", + html_url: null, + download_url: null, + }]; + } else if (func_name == "list_installable_lenses") { + return [{ + author: "testing", + description: "Lorem ipsum dolor sit amet, consectetur adipiscing elit. Etiam et vulputate urna, sit amet semper metus.", + name: "2007scape", + sha: "12345678990", + download_url: "", + html_url: "", + }, { + author: "testing", + description: "Lorem ipsum dolor sit amet, consectetur adipiscing elit. Etiam et vulputate urna, sit amet semper metus.", + name: "2007scape", + sha: "12345678990", + download_url: "", + html_url: "", + }, { + author: "testing", + description: "Lorem ipsum dolor sit amet, consectetur adipiscing elit. Etiam et vulputate urna, sit amet semper metus.", + name: "2007scape", + sha: "12345678990", + download_url: "", + html_url: "", + }, { + author: "testing", + description: "Lorem ipsum dolor sit amet, consectetur adipiscing elit. Etiam et vulputate urna, sit amet semper metus.", + name: "2007scape", + sha: "12345678990", + download_url: "", + html_url: "", + }, { + author: "testing", + description: "Lorem ipsum dolor sit amet, consectetur adipiscing elit. Etiam et vulputate urna, sit amet semper metus.", + name: "2007scape", + sha: "12345678990", + download_url: "", + html_url: "", + }]; + } else if (func_name == "list_plugins") { + return [{ + author: "a5huynh", + title: "chrome-exporter", + description: "Lorem ipsum dolor sit amet, consectetur adipiscing elit. Etiam et vulputate urna, sit amet semper metus.", + is_enabled: true, + }]; + } else if (func_name == "crawl_stats") { + return { + by_domain: [ + ['oldschool.runescape.wiki', { num_queued: 0, num_processing: 0, num_completed: 31413, num_indexed: 35453 }], + ['en.wikipedia.org', { num_queued: 0, num_processing: 0, num_completed: 31413, num_indexed: 35453 }] + ] + }; + } else if (func_name == "load_user_settings") { + return { + "user.data_directory": { + label: "Data Directory", + value: "/Users/a5huynh/Library/Application Support/com.athlabs.spyglass-dev", + form_type: "Text", + help_text: "The data directory is where your index, lenses, plugins, and logs are stored. This will require a restart.", + } + }; + } + + return []; +}; + +export let listen = async () => { + return {}; +}; + +export async function deleteDoc(id) { + return await invoke('delete_doc', { id }); +} + +export async function delete_domain(domain) { + return await invoke('delete_domain', { domain }); +} + +export async function install_lens(downloadUrl) { + return await invoke('install_lens', { downloadUrl }) +} + +export async function network_change(isOffline) { + return await invoke('network_change', { isOffline }); +} + +export async function recrawl_domain(domain) { + return await invoke('recrawl_domain', { domain }); +} + +export async function save_user_settings(settings) { + return await invoke('save_user_settings', { settings }); +} + +export async function searchDocs(lenses, query) { + return await invoke('search_docs', { lenses, query }); +} + +export async function searchLenses(query) { + return await invoke('search_lenses', { query }); +} + +export async function openResult(url) { + return await invoke('open_result', { url }); +} + +export async function resizeWindow(height) { + return await invoke('resize_window', { height }); +} + +export async function toggle_plugin(name) { + return await invoke('toggle_plugin', { name }) +} \ No newline at end of file diff --git a/crates/client/public/glue.js b/crates/client/public/glue.js index 9836c818f..57d92c3a4 100644 --- a/crates/client/public/glue.js +++ b/crates/client/public/glue.js @@ -25,6 +25,10 @@ export async function recrawl_domain(domain) { return await invoke('recrawl_domain', { domain }); } +export async function save_user_settings(settings) { + return await invoke('save_user_settings', { settings }); +} + export async function searchDocs(lenses, query) { return await invoke('search_docs', { lenses, query }); } diff --git a/crates/client/public/main.css b/crates/client/public/main.css index 17313e7c7..a21610e8e 100644 --- a/crates/client/public/main.css +++ b/crates/client/public/main.css @@ -468,6 +468,64 @@ Ensure the default browser behavior of the `hidden` attribute. --tw-backdrop-sepia: ; } +.form-input,.form-textarea,.form-select,.form-multiselect { + -webkit-appearance: none; + -moz-appearance: none; + appearance: none; + background-color: #fff; + border-color: #6b7280; + border-width: 1px; + border-radius: 0px; + padding-top: 0.5rem; + padding-right: 0.75rem; + padding-bottom: 0.5rem; + padding-left: 0.75rem; + font-size: 1rem; + line-height: 1.5rem; + --tw-shadow: 0 0 #0000; +} + +.form-input:focus, .form-textarea:focus, .form-select:focus, .form-multiselect:focus { + outline: 2px solid transparent; + outline-offset: 2px; + --tw-ring-inset: var(--tw-empty,/*!*/ /*!*/); + --tw-ring-offset-width: 0px; + --tw-ring-offset-color: #fff; + --tw-ring-color: #2563eb; + --tw-ring-offset-shadow: var(--tw-ring-inset) 0 0 0 var(--tw-ring-offset-width) var(--tw-ring-offset-color); + --tw-ring-shadow: var(--tw-ring-inset) 0 0 0 calc(1px + var(--tw-ring-offset-width)) var(--tw-ring-color); + box-shadow: var(--tw-ring-offset-shadow), var(--tw-ring-shadow), var(--tw-shadow); + border-color: #2563eb; +} + +.form-input::-moz-placeholder, .form-textarea::-moz-placeholder { + color: #6b7280; + opacity: 1; +} + +.form-input:-ms-input-placeholder, .form-textarea:-ms-input-placeholder { + color: #6b7280; + opacity: 1; +} + +.form-input::placeholder,.form-textarea::placeholder { + color: #6b7280; + opacity: 1; +} + +.form-input::-webkit-datetime-edit-fields-wrapper { + padding: 0; +} + +.form-input::-webkit-date-and-time-value { + min-height: 1.5em; +} + +.form-input::-webkit-datetime-edit,.form-input::-webkit-datetime-edit-year-field,.form-input::-webkit-datetime-edit-month-field,.form-input::-webkit-datetime-edit-day-field,.form-input::-webkit-datetime-edit-hour-field,.form-input::-webkit-datetime-edit-minute-field,.form-input::-webkit-datetime-edit-second-field,.form-input::-webkit-datetime-edit-millisecond-field,.form-input::-webkit-datetime-edit-meridiem-field { + padding-top: 0; + padding-bottom: 0; +} + .absolute { position: absolute; } @@ -485,6 +543,10 @@ Ensure the default browser behavior of the `hidden` attribute. top: 0px; } +.left-0 { + left: 0px; +} + .z-40 { z-index: 40; } @@ -498,28 +560,32 @@ Ensure the default browser behavior of the `hidden` attribute. margin-bottom: 0.75rem; } -.mr-8 { - margin-right: 2rem; -} - -.mr-2 { - margin-right: 0.5rem; +.-ml-16 { + margin-left: -4rem; } -.mt-4 { - margin-top: 1rem; +.-mt-2 { + margin-top: -0.5rem; } .ml-3 { margin-left: 0.75rem; } -.-ml-16 { - margin-left: -4rem; +.mr-2 { + margin-right: 0.5rem; } -.-mt-2 { - margin-top: -0.5rem; +.mb-6 { + margin-bottom: 1.5rem; +} + +.mb-2 { + margin-bottom: 0.5rem; +} + +.mr-8 { + margin-right: 2rem; } .ml-2 { @@ -546,62 +612,64 @@ Ensure the default browser behavior of the `hidden` attribute. display: none; } -.h-6 { - height: 1.5rem; -} - .h-4 { height: 1rem; } -.h-8 { - height: 2rem; -} - -.h-16 { - height: 4rem; +.h-5 { + height: 1.25rem; } .h-28 { height: 7rem; } -.h-5 { - height: 1.25rem; +.h-16 { + height: 4rem; } -.h-20 { - height: 5rem; +.h-screen { + height: 100vh; } -.h-24 { - height: 6rem; +.h-8 { + height: 2rem; } -.w-6 { - width: 1.5rem; +.h-6 { + height: 1.5rem; } .w-4 { width: 1rem; } -.w-full { - width: 100%; +.w-5 { + width: 1.25rem; } .w-3 { width: 0.75rem; } -.w-5 { - width: 1.25rem; +.w-full { + width: 100%; +} + +.w-48 { + width: 12rem; } .w-16 { width: 4rem; } +.min-w-max { + min-width: -webkit-max-content; + min-width: -moz-max-content; + min-width: max-content; +} + .flex-1 { flex: 1 1 0%; } @@ -639,6 +707,10 @@ Ensure the default browser behavior of the `hidden` attribute. flex-direction: row; } +.flex-col { + flex-direction: column; +} + .flex-nowrap { flex-wrap: nowrap; } @@ -651,10 +723,6 @@ Ensure the default browser behavior of the `hidden` attribute. justify-content: center; } -.gap-8 { - gap: 2rem; -} - .gap-4 { gap: 1rem; } @@ -663,6 +731,10 @@ Ensure the default browser behavior of the `hidden` attribute. gap: 0.5rem; } +.gap-8 { + gap: 2rem; +} + .divide-y > :not([hidden]) ~ :not([hidden]) { --tw-divide-y-reverse: 0; border-top-width: calc(1px * calc(1 - var(--tw-divide-y-reverse))); @@ -688,6 +760,10 @@ Ensure the default browser behavior of the `hidden` attribute. text-overflow: ellipsis; } +.rounded { + border-radius: 0.25rem; +} + .rounded-lg { border-radius: 0.5rem; } @@ -696,10 +772,6 @@ Ensure the default browser behavior of the `hidden` attribute. border-radius: 9999px; } -.rounded { - border-radius: 0.25rem; -} - .rounded-l-lg { border-top-left-radius: 0.5rem; border-bottom-left-radius: 0.5rem; @@ -714,57 +786,57 @@ Ensure the default browser behavior of the `hidden` attribute. border-width: 1px; } -.border-b-2 { - border-bottom-width: 2px; -} - .border-t { border-top-width: 1px; } +.border-b-2 { + border-bottom-width: 2px; +} + .border-neutral-600 { --tw-border-opacity: 1; border-color: rgb(82 82 82 / var(--tw-border-opacity)); } -.bg-neutral-800 { - --tw-bg-opacity: 1; - background-color: rgb(38 38 38 / var(--tw-bg-opacity)); +.border-stone-900 { + --tw-border-opacity: 1; + border-color: rgb(28 25 23 / var(--tw-border-opacity)); } -.bg-indigo-400 { - --tw-bg-opacity: 1; - background-color: rgb(129 140 248 / var(--tw-bg-opacity)); +.border-stone-800 { + --tw-border-opacity: 1; + border-color: rgb(41 37 36 / var(--tw-border-opacity)); } -.bg-indigo-500 { +.bg-neutral-800 { --tw-bg-opacity: 1; - background-color: rgb(99 102 241 / var(--tw-bg-opacity)); + background-color: rgb(38 38 38 / var(--tw-bg-opacity)); } -.bg-indigo-600 { +.bg-neutral-900 { --tw-bg-opacity: 1; - background-color: rgb(79 70 229 / var(--tw-bg-opacity)); + background-color: rgb(23 23 23 / var(--tw-bg-opacity)); } -.bg-neutral-600 { +.bg-cyan-700 { --tw-bg-opacity: 1; - background-color: rgb(82 82 82 / var(--tw-bg-opacity)); + background-color: rgb(14 116 144 / var(--tw-bg-opacity)); } -.bg-cyan-700 { +.bg-cyan-900 { --tw-bg-opacity: 1; - background-color: rgb(14 116 144 / var(--tw-bg-opacity)); + background-color: rgb(22 78 99 / var(--tw-bg-opacity)); } -.bg-neutral-900 { +.bg-stone-800 { --tw-bg-opacity: 1; - background-color: rgb(23 23 23 / var(--tw-bg-opacity)); + background-color: rgb(41 37 36 / var(--tw-bg-opacity)); } -.bg-cyan-900 { +.bg-stone-700 { --tw-bg-opacity: 1; - background-color: rgb(22 78 99 / var(--tw-bg-opacity)); + background-color: rgb(68 64 60 / var(--tw-bg-opacity)); } .bg-stone-900 { @@ -772,6 +844,11 @@ Ensure the default browser behavior of the `hidden` attribute. background-color: rgb(28 25 23 / var(--tw-bg-opacity)); } +.bg-neutral-600 { + --tw-bg-opacity: 1; + background-color: rgb(82 82 82 / var(--tw-bg-opacity)); +} + .bg-sky-600 { --tw-bg-opacity: 1; background-color: rgb(2 132 199 / var(--tw-bg-opacity)); @@ -787,10 +864,6 @@ Ensure the default browser behavior of the `hidden` attribute. background-color: rgb(63 98 18 / var(--tw-bg-opacity)); } -.p-4 { - padding: 1rem; -} - .p-2 { padding: 0.5rem; } @@ -799,22 +872,20 @@ Ensure the default browser behavior of the `hidden` attribute. padding: 0.75rem; } -.p-0 { - padding: 0px; +.p-4 { + padding: 1rem; } .p-16 { padding: 4rem; } -.py-4 { - padding-top: 1rem; - padding-bottom: 1rem; +.p-0 { + padding: 0px; } -.px-8 { - padding-left: 2rem; - padding-right: 2rem; +.p-8 { + padding: 2rem; } .py-1 { @@ -827,33 +898,35 @@ Ensure the default browser behavior of the `hidden` attribute. padding-right: 0.5rem; } -.py-2 { - padding-top: 0.5rem; - padding-bottom: 0.5rem; -} - -.pt-4 { +.py-4 { padding-top: 1rem; + padding-bottom: 1rem; } -.pb-2 { - padding-bottom: 0.5rem; +.px-8 { + padding-left: 2rem; + padding-right: 2rem; } -.pb-1 { - padding-bottom: 0.25rem; +.py-2 { + padding-top: 0.5rem; + padding-bottom: 0.5rem; } -.pl-4 { - padding-left: 1rem; +.pl-1 { + padding-left: 0.25rem; } .pr-0 { padding-right: 0px; } -.pl-1 { - padding-left: 0.25rem; +.pl-4 { + padding-left: 1rem; +} + +.pb-2 { + padding-bottom: 0.5rem; } .pt-2 { @@ -872,16 +945,6 @@ Ensure the default browser behavior of the `hidden` attribute. vertical-align: middle; } -.text-2xl { - font-size: 1.5rem; - line-height: 2rem; -} - -.text-xs { - font-size: 0.75rem; - line-height: 1rem; -} - .text-sm { font-size: 0.875rem; line-height: 1.25rem; @@ -892,9 +955,9 @@ Ensure the default browser behavior of the `hidden` attribute. line-height: 2.5rem; } -.text-5xl { - font-size: 3rem; - line-height: 1; +.text-xs { + font-size: 0.75rem; + line-height: 1rem; } .text-lg { @@ -902,15 +965,43 @@ Ensure the default browser behavior of the `hidden` attribute. line-height: 1.75rem; } +.text-2xl { + font-size: 1.5rem; + line-height: 2rem; +} + .text-xl { font-size: 1.25rem; line-height: 1.75rem; } +.text-5xl { + font-size: 3rem; + line-height: 1; +} + +.font-bold { + font-weight: 700; +} + +.uppercase { + text-transform: uppercase; +} + .leading-relaxed { line-height: 1.625; } +.text-neutral-600 { + --tw-text-opacity: 1; + color: rgb(82 82 82 / var(--tw-text-opacity)); +} + +.text-stone-700 { + --tw-text-opacity: 1; + color: rgb(68 64 60 / var(--tw-text-opacity)); +} + .text-white { --tw-text-opacity: 1; color: rgb(255 255 255 / var(--tw-text-opacity)); @@ -926,14 +1017,9 @@ Ensure the default browser behavior of the `hidden` attribute. color: rgb(163 163 163 / var(--tw-text-opacity)); } -.text-neutral-600 { - --tw-text-opacity: 1; - color: rgb(82 82 82 / var(--tw-text-opacity)); -} - -.text-red-600 { +.text-gray-500 { --tw-text-opacity: 1; - color: rgb(220 38 38 / var(--tw-text-opacity)); + color: rgb(107 114 128 / var(--tw-text-opacity)); } .text-green-400 { @@ -946,6 +1032,16 @@ Ensure the default browser behavior of the `hidden` attribute. color: rgb(248 113 113 / var(--tw-text-opacity)); } +.text-yellow-500 { + --tw-text-opacity: 1; + color: rgb(234 179 8 / var(--tw-text-opacity)); +} + +.outline-none { + outline: 2px solid transparent; + outline-offset: 2px; +} + .filter { filter: var(--tw-blur) var(--tw-brightness) var(--tw-contrast) var(--tw-grayscale) var(--tw-hue-rotate) var(--tw-invert) var(--tw-saturate) var(--tw-sepia) var(--tw-drop-shadow); } @@ -955,6 +1051,11 @@ Ensure the default browser behavior of the `hidden` attribute. background-color: rgb(82 82 82 / var(--tw-bg-opacity)); } +.hover\:bg-stone-700:hover { + --tw-bg-opacity: 1; + background-color: rgb(68 64 60 / var(--tw-bg-opacity)); +} + .hover\:text-red-600:hover { --tw-text-opacity: 1; color: rgb(220 38 38 / var(--tw-text-opacity)); @@ -975,6 +1076,11 @@ Ensure the default browser behavior of the `hidden` attribute. background-color: rgb(64 64 64 / var(--tw-bg-opacity)); } +.active\:outline-none:active { + outline: 2px solid transparent; + outline-offset: 2px; +} + .group:hover .group-hover\:block { display: block; } diff --git a/crates/client/src/components/btn.rs b/crates/client/src/components/btn.rs index ce747275a..937997ef6 100644 --- a/crates/client/src/components/btn.rs +++ b/crates/client/src/components/btn.rs @@ -132,3 +132,36 @@ pub fn delete_button(props: &DeleteDomainButtonProps) -> Html { } } + +#[derive(Properties, PartialEq)] +pub struct DefaultBtnProps { + pub onclick: Callback, + #[prop_or_default] + pub disabled: bool, + #[prop_or_default] + pub children: Children, +} + +#[function_component(Btn)] +pub fn default_button(props: &DefaultBtnProps) -> Html { + let styles = classes!( + "border-neutral-600", + "border", + "flex-row", + "flex", + "p-2", + "rounded-lg", + "text-sm", + if props.disabled { + classes!("text-stone-700") + } else { + classes!("hover:bg-neutral-600", "active:bg-neutral-700") + }, + ); + + html! { + + } +} diff --git a/crates/client/src/components/icons.rs b/crates/client/src/components/icons.rs index 7a770d95c..c0cf3d3df 100644 --- a/crates/client/src/components/icons.rs +++ b/crates/client/src/components/icons.rs @@ -9,17 +9,44 @@ pub struct IconProps { pub height: String, #[prop_or("w-5".into())] pub width: String, + #[prop_or_default] + pub classes: Classes, } impl IconProps { - pub fn class(&self) -> Vec> { - let animated = if self.animate_spin { - Some("animate-spin".to_string()) - } else { - None - }; + pub fn class(&self) -> Classes { + classes!( + self.classes.clone(), + self.animate_spin.then(|| Some("animate-spin")), + Some(format!("{} {}", self.height, self.width)) + ) + } +} + +#[function_component(AdjustmentsIcon)] +pub fn adjustments_icon(props: &IconProps) -> Html { + html! { + + + + } +} + +#[function_component(ChartBarIcon)] +pub fn chart_bar_icon(props: &IconProps) -> Html { + html! { + + + + } +} - vec![animated, Some(format!("{} {}", self.height, self.width))] +#[function_component(ChipIcon)] +pub fn chip_icon(props: &IconProps) -> Html { + html! { + + + } } @@ -60,6 +87,15 @@ pub fn eye_icon(props: &IconProps) -> Html { } } +#[function_component(FilterIcon)] +pub fn filter_icon(props: &IconProps) -> Html { + html! { + + + + } +} + #[function_component(FolderOpenIcon)] pub fn folder_open_icon(props: &IconProps) -> Html { html! { diff --git a/crates/client/src/components/mod.rs b/crates/client/src/components/mod.rs index 7fabf0fc3..5f97044d1 100644 --- a/crates/client/src/components/mod.rs +++ b/crates/client/src/components/mod.rs @@ -164,3 +164,22 @@ pub fn search_result_component(props: &SearchResultProps) -> Html { } } } + +#[derive(PartialEq, Properties)] +pub struct HeaderProps { + pub label: String, + #[prop_or_default] + pub children: Children, +} + +#[function_component(Header)] +pub fn header(props: &HeaderProps) -> Html { + html! { +
+
+

{props.label.clone()}

+ {props.children.clone()} +
+
+ } +} diff --git a/crates/client/src/main.rs b/crates/client/src/main.rs index f9683ebde..21a945bf4 100644 --- a/crates/client/src/main.rs +++ b/crates/client/src/main.rs @@ -10,18 +10,69 @@ mod events; mod pages; mod utils; -use crate::pages::{LensManagerPage, PluginManagerPage, SearchPage, StatsPage}; +use crate::pages::{SearchPage, SettingsPage, StatsPage}; +#[cfg(headless)] +#[wasm_bindgen(module = "/public/fixtures.js")] +extern "C" { + #[wasm_bindgen(catch)] + pub async fn invoke(fn_name: &str, val: JsValue) -> Result; + + #[wasm_bindgen(catch)] + pub async fn listen( + event_name: &str, + cb: &Closure, + ) -> Result; + + #[wasm_bindgen(js_name = "deleteDoc", catch)] + pub async fn delete_doc(id: String) -> Result<(), JsValue>; + + #[wasm_bindgen(catch)] + pub async fn delete_domain(domain: String) -> Result<(), JsValue>; + + #[wasm_bindgen(catch)] + pub async fn install_lens(download_url: String) -> Result<(), JsValue>; + + #[wasm_bindgen(catch)] + pub async fn save_user_settings(settings: JsValue) -> Result; + + #[wasm_bindgen(js_name = "searchDocs", catch)] + pub async fn search_docs(lenses: JsValue, query: String) -> Result; + + #[wasm_bindgen(js_name = "searchLenses", catch)] + pub async fn search_lenses(query: String) -> Result; + + #[wasm_bindgen(js_name = "openResult", catch)] + pub async fn open(url: String) -> Result<(), JsValue>; + + #[wasm_bindgen(js_name = "resizeWindow", catch)] + pub async fn resize_window(height: f64) -> Result<(), JsValue>; + + #[wasm_bindgen] + pub async fn network_change(is_offline: bool); + + #[wasm_bindgen(catch)] + pub async fn recrawl_domain(domain: String) -> Result<(), JsValue>; + + #[wasm_bindgen(catch)] + pub async fn toggle_plugin(name: &str) -> Result<(), JsValue>; +} + +#[cfg(not(headless))] #[wasm_bindgen] extern "C" { #[wasm_bindgen(js_namespace = ["window", "__TAURI__"], catch)] pub async fn invoke(fn_name: &str, val: JsValue) -> Result; #[wasm_bindgen(js_namespace = ["window", "__TAURI__", "event"], catch)] - pub async fn listen(event_name: &str, cb: &Closure) -> Result; + pub async fn listen( + event_name: &str, + cb: &Closure, + ) -> Result; } +#[cfg(not(headless))] #[wasm_bindgen(module = "/public/glue.js")] extern "C" { #[wasm_bindgen(js_name = "deleteDoc", catch)] @@ -33,6 +84,9 @@ extern "C" { #[wasm_bindgen(catch)] pub async fn install_lens(download_url: String) -> Result<(), JsValue>; + #[wasm_bindgen(catch)] + pub async fn save_user_settings(settings: JsValue) -> Result; + #[wasm_bindgen(js_name = "searchDocs", catch)] pub async fn search_docs(lenses: JsValue, query: String) -> Result; @@ -56,15 +110,13 @@ extern "C" { } #[derive(Clone, Routable, PartialEq)] -enum Route { +pub enum Route { #[at("/")] Search, - #[at("/settings/lens")] - LensManager, + #[at("/settings/:tab")] + SettingsPage { tab: pages::Tab }, #[at("/stats")] Status, - #[at("/settings/plugins")] - PluginManager, } fn main() { @@ -107,9 +159,8 @@ pub fn app() -> Html { fn switch(routes: &Route) -> Html { match routes { - Route::LensManager => html! { }, - Route::PluginManager => html! { }, Route::Search => html! { }, + Route::SettingsPage { tab } => html! { }, Route::Status => html! { }, } } diff --git a/crates/client/src/pages/admin.rs b/crates/client/src/pages/admin.rs new file mode 100644 index 000000000..0efc07a60 --- /dev/null +++ b/crates/client/src/pages/admin.rs @@ -0,0 +1,155 @@ +use serde::Deserialize; +use strum_macros::{Display, EnumString}; +use wasm_bindgen::prelude::*; +use wasm_bindgen_futures::spawn_local; +use yew::{classes, prelude::*, Children}; +use yew_router::components::Link; +use yew_router::history::History; +use yew_router::hooks::use_history; + +use crate::components::icons; +use crate::{listen, pages, Route}; +use shared::event::ClientEvent; + +#[derive(Debug, Deserialize)] +struct ListenPayload { + payload: String, +} + +#[derive(Clone, EnumString, Display, PartialEq)] +pub enum Tab { + #[strum(serialize = "lenses")] + LensManager, + #[strum(serialize = "plugins")] + PluginsManager, + #[strum(serialize = "stats")] + Stats, + #[strum(serialize = "user")] + UserSettings, +} + +#[derive(PartialEq, Properties)] +pub struct NavLinkProps { + tab: Tab, + children: Children, + current: Tab, +} + +#[function_component(NavLink)] +pub fn nav_link(props: &NavLinkProps) -> Html { + let link_styles = classes!( + "flex-row", + "flex", + "hover:bg-stone-700", + "items-center", + "p-2", + "rounded", + "w-full", + ); + + html! { + + classes={ + classes!( + (props.current == props.tab).then(|| Some("bg-stone-700")), + link_styles + ) + } + to={Route::SettingsPage { tab: props.tab.clone() }} + > + {props.children.clone()} + > + } +} + +#[derive(PartialEq, Properties)] +pub struct SettingsPageProps { + pub tab: Tab, +} + +#[function_component(SettingsPage)] +pub fn settings_page(props: &SettingsPageProps) -> Html { + let history = use_history().unwrap(); + + spawn_local(async move { + let cb = Closure::wrap(Box::new(move |payload: JsValue| { + if let Ok(payload) = payload.into_serde::() { + match payload.payload.as_str() { + "/settings/lenses" => history.push(Route::SettingsPage { + tab: pages::Tab::LensManager, + }), + "/settings/plugins" => history.push(Route::SettingsPage { + tab: pages::Tab::PluginsManager, + }), + "/settings/stats" => history.push(Route::SettingsPage { + tab: pages::Tab::Stats, + }), + "/settings/user" => history.push(Route::SettingsPage { + tab: pages::Tab::UserSettings, + }), + _ => history.push(Route::SettingsPage { + tab: pages::Tab::Stats, + }), + } + } + }) as Box); + let _ = listen(ClientEvent::Navigate.as_ref(), &cb).await; + cb.forget(); + }); + + html! { +
+
+
+
+ {"Spyglass"} +
+
    +
  • + + + {"Crawl Status"} + +
  • +
+
+ +
+
+ {"Configuration"} +
+
    +
  • + + + {"Lenses"} + +
  • +
  • + + + {"Plugins"} + +
  • +
  • + + + {"User Settings"} + +
  • +
+
+
+
+ { + match props.tab { + Tab::LensManager => html! { }, + Tab::PluginsManager => html! { }, + Tab::Stats => html!{ }, + Tab::UserSettings => html! { }, + } + } +
+
+ } +} diff --git a/crates/client/src/pages/stats.rs b/crates/client/src/pages/crawl_stats.rs similarity index 88% rename from crates/client/src/pages/stats.rs rename to crates/client/src/pages/crawl_stats.rs index 718773e7e..9772c23f5 100644 --- a/crates/client/src/pages/stats.rs +++ b/crates/client/src/pages/crawl_stats.rs @@ -69,26 +69,20 @@ struct StatsBarProps { #[function_component(StatsBar)] fn stats_bar(props: &StatsBarProps) -> Html { - let percent = props.count as f64 / props.total * 100.0; + let percent = (props.count as f64 / props.total * 100.0).max(5.0); let mut buf = Buffer::default(); buf.write_formatted(&props.count, &Locale::en); - let mut bar_style: Vec = vec![ - "relative".into(), - "flex".into(), - "justify-center".into(), - "h-8".into(), - "p-2".into(), - ]; - bar_style.push(props.color.clone()); - - if props.is_start { - bar_style.push("rounded-l-lg".into()); - } - - if props.is_end { - bar_style.push("rounded-r-lg".into()); - } + let bar_style = classes!( + "relative", + "flex", + "justify-center", + "h-8", + "p-2", + props.color.clone(), + props.is_start.then(|| Some("rounded-l-lg")), + props.is_end.then(|| Some("rounded-r-lg")), + ); html! {
@@ -158,16 +152,14 @@ pub fn stats_page() -> Html { html! {
-
+

{"Crawl Status"}

- +
diff --git a/crates/client/src/pages/lens_manager.rs b/crates/client/src/pages/lens_manager.rs index 972a6f8a2..dd45e6471 100644 --- a/crates/client/src/pages/lens_manager.rs +++ b/crates/client/src/pages/lens_manager.rs @@ -6,7 +6,7 @@ use wasm_bindgen_futures::spawn_local; use yew::function_component; use yew::prelude::*; -use crate::components::icons; +use crate::components::{btn::Btn, icons, Header}; use crate::listen; use crate::utils::RequestState; use crate::{install_lens, invoke}; @@ -116,7 +116,8 @@ pub fn lens_component(props: &LensProps) -> Html { let component_styles: Vec = vec![ "border-t".into(), "border-neutral-600".into(), - "p-4".into(), + "px-8".into(), + "py-4".into(), "pr-0".into(), "text-white".into(), "bg-netural-800".into(), @@ -184,21 +185,19 @@ pub fn lens_manager_page() -> Html { fetch_installable_lenses(installable.clone(), i_req_state.clone()); } - let on_open_folder = { - move |_| { - spawn_local(async { - let _ = invoke(ClientInvoke::OpenLensFolder.as_ref(), JsValue::NULL).await; - }); - } - }; + let on_open_folder = Callback::from(move |_| { + spawn_local(async { + let _ = invoke(ClientInvoke::OpenLensFolder.as_ref(), JsValue::NULL).await; + }); + }); let on_refresh = { let ui_req_state = ui_req_state.clone(); let i_req_state = i_req_state.clone(); - move |_| { + Callback::from(move |_| { ui_req_state.set(RequestState::NotStarted); i_req_state.set(RequestState::NotStarted); - } + }) }; let already_installed: HashSet = @@ -216,10 +215,10 @@ pub fn lens_manager_page() -> Html { let ui_req_state = ui_req_state.clone(); let i_req_state = i_req_state.clone(); spawn_local(async move { - let cb = Closure::wrap(Box::new(move || { + let cb = Closure::wrap(Box::new(move |_| { ui_req_state.set(RequestState::NotStarted); i_req_state.set(RequestState::NotStarted); - }) as Box); + }) as Box); let _ = listen(ClientEvent::RefreshLensManager.as_ref(), &cb).await; cb.forget(); @@ -253,25 +252,16 @@ pub fn lens_manager_page() -> Html { html! {
-
-
-

{"Lens Manager"}

- - -
-
-
- {contents} -
+
+ + +
{"Lens folder"}
+
+ + + +
+
{contents}
} } diff --git a/crates/client/src/pages/mod.rs b/crates/client/src/pages/mod.rs index a16c5152f..12b2cea09 100644 --- a/crates/client/src/pages/mod.rs +++ b/crates/client/src/pages/mod.rs @@ -1,3 +1,6 @@ +mod admin; +pub use admin::*; + mod lens_manager; pub use lens_manager::*; @@ -7,5 +10,8 @@ pub use plugin_manager::*; mod search; pub use search::*; -mod stats; -pub use stats::*; +mod settings; +pub use settings::*; + +mod crawl_stats; +pub use crawl_stats::*; diff --git a/crates/client/src/pages/plugin_manager.rs b/crates/client/src/pages/plugin_manager.rs index da9b2e812..fa4225689 100644 --- a/crates/client/src/pages/plugin_manager.rs +++ b/crates/client/src/pages/plugin_manager.rs @@ -7,7 +7,7 @@ use yew::prelude::*; use shared::event::ClientInvoke; use shared::response::PluginResult; -use crate::components::icons; +use crate::components::{icons, Header}; use crate::utils::RequestState; use crate::{invoke, listen, toggle_plugin}; @@ -37,14 +37,14 @@ pub struct PluginProps { #[function_component(Plugin)] pub fn plugin_comp(props: &PluginProps) -> Html { let plugin = &props.plugin; - let component_styles: Vec = vec![ - "border-t".into(), - "border-neutral-600".into(), - "p-4".into(), - "pr-0".into(), - "text-white".into(), - "bg-netural-800".into(), - ]; + let component_styles: Classes = classes!( + "border-t", + "border-neutral-600", + "py-4", + "px-8", + "text-white", + "bg-netural-800", + ); let btn_label = if plugin.is_enabled { "Disable" @@ -145,10 +145,10 @@ pub fn plugin_manager_page() -> Html { // Listen for updates from plugins { spawn_local(async move { - let cb = Closure::wrap(Box::new(move || { + let cb = Closure::wrap(Box::new(move |_| { log::info!("refresh!"); req_state.set(RequestState::NotStarted); - }) as Box); + }) as Box); let _ = listen(ClientEvent::RefreshPluginManager.as_ref(), &cb).await; cb.forget(); @@ -157,7 +157,8 @@ pub fn plugin_manager_page() -> Html { html! {
- {contents} +
+
{contents}
} } diff --git a/crates/client/src/pages/search.rs b/crates/client/src/pages/search.rs index c370f7dba..5bb78421a 100644 --- a/crates/client/src/pages/search.rs +++ b/crates/client/src/pages/search.rs @@ -107,7 +107,7 @@ pub fn search_page() -> Html { // Reset query string, results list, etc when we receive a "clear_search" // event from tauri spawn_local(async move { - let cb = Closure::wrap(Box::new(move || { + let cb = Closure::wrap(Box::new(move |_| { query.set("".to_string()); results.set(Vec::new()); selected_idx.set(0); @@ -120,7 +120,7 @@ pub fn search_page() -> Html { spawn_local(async move { resize_window(node.client_height() as f64).await.unwrap(); }); - }) as Box); + }) as Box); let _ = listen(ClientEvent::ClearSearch.as_ref(), &cb).await; cb.forget(); @@ -130,7 +130,7 @@ pub fn search_page() -> Html { // Focus on the search box when we receive an "focus_window" event from // tauri spawn_local(async move { - let cb = Closure::wrap(Box::new(move || { + let cb = Closure::wrap(Box::new(move |_| { let document = gloo::utils::document(); if let Some(el) = document.get_element_by_id("searchbox") { let el: HtmlElement = el.unchecked_into(); @@ -142,7 +142,7 @@ pub fn search_page() -> Html { resize_window(node.client_height() as f64).await.unwrap(); }); } - }) as Box); + }) as Box); let _ = listen(ClientEvent::FocusWindow.as_ref(), &cb).await; cb.forget(); }); @@ -152,14 +152,14 @@ pub fn search_page() -> Html { // Refresh search results let query = query.clone(); spawn_local(async move { - let cb = Closure::wrap(Box::new(move || { + let cb = Closure::wrap(Box::new(move |_| { let document = gloo::utils::document(); if let Some(el) = document.get_element_by_id("searchbox") { let el: HtmlInputElement = el.unchecked_into(); query.set("".into()); query.set(el.value()); } - }) as Box); + }) as Box); let _ = listen(ClientEvent::RefreshSearchResults.as_ref(), &cb).await; cb.forget(); }); @@ -204,7 +204,7 @@ pub fn search_page() -> Html { ref={(*query_ref).clone()} id="searchbox" type="text" - class="bg-neutral-800 text-white text-5xl p-4 overflow-hidden flex-1 focus:outline-none" + class="bg-neutral-800 text-white text-5xl p-4 overflow-hidden flex-1 outline-none active:outline-none focus:outline-none" placeholder="Search" {onkeyup} {onkeydown} diff --git a/crates/client/src/pages/settings.rs b/crates/client/src/pages/settings.rs new file mode 100644 index 000000000..5d813c64e --- /dev/null +++ b/crates/client/src/pages/settings.rs @@ -0,0 +1,160 @@ +use std::collections::HashMap; +use wasm_bindgen::JsValue; +use wasm_bindgen_futures::spawn_local; +use web_sys::HtmlInputElement; +use yew::prelude::*; + +use crate::{ + components::{btn, icons, Header}, + invoke, save_user_settings, + utils::RequestState, +}; +use shared::event::ClientInvoke; +use shared::SettingOpts; + +#[derive(Properties, PartialEq)] +pub struct SettingFormProps { + #[prop_or_default] + onchange: Callback, + setting_ref: String, + opts: SettingOpts, +} + +pub struct SettingChangeEvent { + setting_ref: String, + new_value: String, +} + +#[function_component(SettingForm)] +pub fn setting_form(props: &SettingFormProps) -> Html { + let value = use_state(|| props.opts.value.clone()); + + let onkeyup = { + let onchange = props.onchange.clone(); + let setting_ref = props.setting_ref.clone(); + let value = value.clone(); + Callback::from(move |e: KeyboardEvent| { + let input: HtmlInputElement = e.target_unchecked_into(); + let input_value = input.value(); + onchange.emit(SettingChangeEvent { + setting_ref: setting_ref.clone(), + new_value: input_value.clone(), + }); + value.set(input_value); + }) + }; + + html! { +
+
+ + { + if let Some(help_text) = props.opts.help_text.clone() { + html! { +
+ + {help_text.clone()} + +
+ } + } else { + html! { } + } + } +
+
+ +
+
+ } +} + +#[function_component(UserSettingsPage)] +pub fn user_settings_page() -> Html { + let current_settings: UseStateHandle> = use_state_eq(HashMap::new); + let changes: UseStateHandle> = use_state_eq(HashMap::new); + + let req_state = use_state_eq(|| RequestState::NotStarted); + if *req_state == RequestState::NotStarted { + req_state.set(RequestState::InProgress); + let current_settings = current_settings.clone(); + spawn_local(async move { + if let Ok(res) = invoke(ClientInvoke::LoadUserSettings.as_ref(), JsValue::NULL).await { + if let Ok(deser) = JsValue::into_serde::>(&res) { + current_settings.set(deser); + } else { + log::error!("unable to deserialize"); + } + } else { + log::error!("unable to invoke"); + } + }) + } + + // Detect changes in setting values & enable the save changes button + let has_changes = use_state_eq(|| false); + let onchange = { + let has_changes = has_changes.clone(); + let changes = changes.clone(); + Callback::from(move |evt: SettingChangeEvent| { + has_changes.set(true); + let mut updated = (*changes).clone(); + updated.insert(evt.setting_ref, evt.new_value); + changes.set(updated); + }) + }; + + let handle_show_folder = Callback::from(|_| { + spawn_local(async move { + let _ = invoke(ClientInvoke::OpenSettingsFolder.as_ref(), JsValue::NULL).await; + }); + }); + + let handle_save_changes = { + let has_changes = has_changes.clone(); + Callback::from(move |_| { + let changes_ref = changes.clone(); + let updated = (*changes).clone(); + spawn_local(async move { + // Send changes to backend to be validated & saved. + if let Ok(ser) = JsValue::from_serde(&updated.clone()) { + // TODO: Handle any validation errors from backend and show + // them to user. + let _ = save_user_settings(ser).await; + } + }); + + changes_ref.set(HashMap::new()); + has_changes.set(false); + }) + }; + + let contents = current_settings + .iter() + .map(|(setting_ref, setting)| { + html! { + + } + }) + .collect::(); + + html! { +
+
+ + + {"Settings folder"} + + + {"Save Changes"} + +
+
{contents}
+
+ } +} diff --git a/crates/client/tailwind.config.js b/crates/client/tailwind.config.js index 4dde66179..202ca0506 100644 --- a/crates/client/tailwind.config.js +++ b/crates/client/tailwind.config.js @@ -3,5 +3,7 @@ module.exports = { theme: { extend: {}, }, - plugins: [], + plugins: [ + require('@tailwindcss/forms')({ strategy: 'class' }) + ], } diff --git a/crates/shared/src/config.rs b/crates/shared/src/config.rs index 22d5801b6..2b48939f1 100644 --- a/crates/shared/src/config.rs +++ b/crates/shared/src/config.rs @@ -134,6 +134,22 @@ impl UserSettings { } } +impl From for HashMap { + fn from(settings: UserSettings) -> Self { + let mut map: HashMap = HashMap::new(); + map.insert( + "user.data_directory".to_string(), + settings + .data_directory + .to_str() + .expect("Unable to convert to string") + .to_string(), + ); + + map + } +} + impl Default for UserSettings { fn default() -> Self { UserSettings { @@ -168,6 +184,15 @@ impl Config { Ok(()) } + pub fn save_user_settings(&self, user_settings: &UserSettings) -> anyhow::Result<()> { + let prefs_path = Self::prefs_file(); + let serialized = ron::ser::to_string_pretty(user_settings, Default::default()) + .expect("Unable to serialize user settings"); + fs::write(prefs_path, serialized).expect("Unable to save user preferences file"); + + Ok(()) + } + fn load_plugin_setings(&self) -> anyhow::Result { let prefs_path = self.plugin_settings_file(); if prefs_path.exists() { diff --git a/crates/shared/src/event.rs b/crates/shared/src/event.rs index 072fde525..3dff9be90 100644 --- a/crates/shared/src/event.rs +++ b/crates/shared/src/event.rs @@ -4,6 +4,7 @@ use strum_macros::{AsRefStr, Display}; pub enum ClientEvent { ClearSearch, FocusWindow, + Navigate, RefreshLensManager, RefreshPluginManager, RefreshSearchResults, @@ -21,6 +22,10 @@ pub enum ClientInvoke { ListInstalledLenses, #[strum(serialize = "list_installable_lenses")] ListInstallableLenses, + #[strum(serialize = "load_user_settings")] + LoadUserSettings, #[strum(serialize = "open_lens_folder")] OpenLensFolder, + #[strum(serialize = "open_settings_folder")] + OpenSettingsFolder, } diff --git a/crates/shared/src/lib.rs b/crates/shared/src/lib.rs index 7aaee993f..ba3da7a52 100644 --- a/crates/shared/src/lib.rs +++ b/crates/shared/src/lib.rs @@ -1,5 +1,21 @@ +use serde::{Deserialize, Serialize}; +use strum_macros::{Display, EnumString}; + pub mod config; pub mod event; pub mod request; pub mod response; pub mod rpc; + +#[derive(Clone, Debug, Display, EnumString, PartialEq, Serialize, Deserialize)] +pub enum FormType { + Text, +} + +#[derive(Clone, Debug, Serialize, Deserialize, PartialEq)] +pub struct SettingOpts { + pub label: String, + pub value: String, + pub form_type: FormType, + pub help_text: Option, +} diff --git a/crates/tauri/src/cmd.rs b/crates/tauri/src/cmd.rs index 891f698f2..a35427752 100644 --- a/crates/tauri/src/cmd.rs +++ b/crates/tauri/src/cmd.rs @@ -1,4 +1,6 @@ +use std::collections::HashMap; use std::fs; +use std::path::PathBuf; use jsonrpc_core::Value; use tauri::State; @@ -10,6 +12,7 @@ use shared::{ event::ClientEvent, request, response::{self, InstallableLens}, + FormType, SettingOpts, }; #[tauri::command] @@ -33,6 +36,12 @@ pub async fn open_plugins_folder( Ok(()) } +#[tauri::command] +pub async fn open_settings_folder(_: tauri::Window) -> Result<(), String> { + open_folder(Config::prefs_dir()); + Ok(()) +} + #[tauri::command] pub async fn open_result(_: tauri::Window, url: &str) -> Result<(), String> { open::that(url).unwrap(); @@ -316,3 +325,40 @@ pub async fn toggle_plugin( Ok(()) } + +#[tauri::command] +pub async fn save_user_settings( + _: tauri::Window, + config: State<'_, Config>, + settings: HashMap, +) -> Result<(), String> { + let mut user_settings = config.user_settings.clone(); + // Update the user settings + for (key, value) in settings.iter() { + if key == "user.data_directory" { + user_settings.data_directory = PathBuf::from(value); + } + } + + dbg!(&user_settings); + let _ = config.save_user_settings(&user_settings); + + Ok(()) +} + +#[tauri::command] +pub async fn load_user_settings( + _: tauri::Window, + config: State<'_, Config>, +) -> Result, String> { + let serialized: HashMap = config.user_settings.clone().into(); + let mut map = HashMap::new(); + map.insert("user.data_directory".into(), SettingOpts { + label: "Data Directory".into(), + value: serialized.get("user.data_directory").unwrap_or(&"".to_string()).to_string(), + form_type: FormType::Text, + help_text: Some("The data directory is where your index, lenses, plugins, and logs are stored. This will require a restart.".into()) + }); + + Ok(map) +} diff --git a/crates/tauri/src/constants.rs b/crates/tauri/src/constants.rs index bf5783998..ce91bb6cd 100644 --- a/crates/tauri/src/constants.rs +++ b/crates/tauri/src/constants.rs @@ -9,6 +9,4 @@ pub const DISCORD_JOIN_URL: &str = "https://discord.gg/663wPVBSTB"; pub const LENS_DIRECTORY_INDEX_URL: &str = "https://raw.githubusercontent.com/spyglass-search/lens-box/main/index.ron"; -pub const STATS_WIN_NAME: &str = "crawl_stats"; -pub const LENS_MANAGER_WIN_NAME: &str = "lens_manager"; -pub const PLUGIN_MANAGER_WIN_NAME: &str = "plugin_manager"; +pub const SETTINGS_WIN_NAME: &str = "settings_window"; diff --git a/crates/tauri/src/main.rs b/crates/tauri/src/main.rs index 46e53327f..245885070 100644 --- a/crates/tauri/src/main.rs +++ b/crates/tauri/src/main.rs @@ -32,7 +32,9 @@ mod menu; use menu::MenuID; mod rpc; mod window; -use window::{show_crawl_stats_window, show_lens_manager_window, show_plugin_manager}; +use window::{ + show_crawl_stats_window, show_lens_manager_window, show_plugin_manager, show_user_settings, +}; fn main() -> Result<(), Box> { let ctx = tauri::generate_context!(); @@ -75,12 +77,15 @@ fn main() -> Result<(), Box> { cmd::list_installable_lenses, cmd::list_installed_lenses, cmd::list_plugins, + cmd::load_user_settings, cmd::network_change, cmd::open_lens_folder, cmd::open_plugins_folder, cmd::open_result, + cmd::open_settings_folder, cmd::recrawl_domain, cmd::resize_window, + cmd::save_user_settings, cmd::search_docs, cmd::search_lenses, cmd::toggle_plugin, @@ -172,7 +177,7 @@ fn main() -> Result<(), Box> { MenuID::OPEN_LENS_MANAGER => { show_lens_manager_window(app); }, MenuID::OPEN_PLUGIN_MANAGER => { show_plugin_manager(app); }, MenuID::OPEN_LOGS_FOLDER => open_folder(config.logs_dir()), - MenuID::OPEN_SETTINGS_FOLDER => open_folder(Config::prefs_dir()), + MenuID::OPEN_SETTINGS_MANAGER => { show_user_settings(app) }, MenuID::SHOW_CRAWL_STATUS => { show_crawl_stats_window(app); } diff --git a/crates/tauri/src/menu.rs b/crates/tauri/src/menu.rs index b1930842c..13b3629af 100644 --- a/crates/tauri/src/menu.rs +++ b/crates/tauri/src/menu.rs @@ -15,7 +15,7 @@ pub enum MenuID { OPEN_LENS_MANAGER, OPEN_LOGS_FOLDER, OPEN_PLUGIN_MANAGER, - OPEN_SETTINGS_FOLDER, + OPEN_SETTINGS_MANAGER, QUIT, SHOW_CRAWL_STATUS, SHOW_SEARCHBAR, @@ -29,11 +29,6 @@ pub fn get_tray_menu(ctx: &Context, config: &Config) -> SystemTr let pause = CustomMenuItem::new(MenuID::CRAWL_STATUS.to_string(), ""); let quit = CustomMenuItem::new(MenuID::QUIT.to_string(), "Quit"); - let open_settings_folder = CustomMenuItem::new( - MenuID::OPEN_SETTINGS_FOLDER.to_string(), - "Open settings folder", - ); - let open_logs_folder = CustomMenuItem::new(MenuID::OPEN_LOGS_FOLDER.to_string(), "Open logs folder"); @@ -50,18 +45,21 @@ pub fn get_tray_menu(ctx: &Context, config: &Config) -> SystemTr ) .add_item(CustomMenuItem::new( MenuID::SHOW_CRAWL_STATUS.to_string(), - "Show crawl status", + "Crawl status", )) .add_item(CustomMenuItem::new( MenuID::OPEN_LENS_MANAGER.to_string(), - "Manage/install lenses", + "Manage lenses", )) .add_item(CustomMenuItem::new( MenuID::OPEN_PLUGIN_MANAGER.to_string(), "Manage plugins", )) + .add_item(CustomMenuItem::new( + MenuID::OPEN_SETTINGS_MANAGER.to_string(), + "Preferences", + )) .add_native_item(SystemTrayMenuItem::Separator) - .add_item(open_settings_folder) .add_item(open_logs_folder); // Add dev utils diff --git a/crates/tauri/src/window.rs b/crates/tauri/src/window.rs index c350be89b..4441bc8c6 100644 --- a/crates/tauri/src/window.rs +++ b/crates/tauri/src/window.rs @@ -46,55 +46,40 @@ pub fn show_window(window: &Window) { center_window(window); } -pub fn show_crawl_stats_window(app: &AppHandle) -> Window { - if let Some(window) = app.get_window(constants::STATS_WIN_NAME) { - let _ = window.show(); - let _ = window.set_focus(); - return window; - } +fn _show_tab(app: &AppHandle, tab_url: &str) { + let window = if let Some(window) = app.get_window(constants::SETTINGS_WIN_NAME) { + window + } else { + WindowBuilder::new( + app, + constants::SETTINGS_WIN_NAME, + WindowUrl::App(tab_url.into()), + ) + .title("Spyglass - Personal Search Engine") + .build() + .unwrap() + }; - WindowBuilder::new( - app, - constants::STATS_WIN_NAME, - WindowUrl::App("/stats".into()), - ) - .title("Status") - .build() - .unwrap() + let _ = window.emit(ClientEvent::Navigate.as_ref(), tab_url); + // A little hack to bring window to the front if its hiding behind something. + let _ = window.set_always_on_top(true); + let _ = window.set_always_on_top(false); } -pub fn show_lens_manager_window(app: &AppHandle) -> Window { - if let Some(window) = app.get_window(constants::LENS_MANAGER_WIN_NAME) { - let _ = window.show(); - let _ = window.set_focus(); - return window; - } +pub fn show_crawl_stats_window(app: &AppHandle) { + _show_tab(app, "/settings/stats"); +} - WindowBuilder::new( - app, - constants::LENS_MANAGER_WIN_NAME, - WindowUrl::App("/settings/lens".into()), - ) - .title("Lens Manager") - .build() - .unwrap() +pub fn show_lens_manager_window(app: &AppHandle) { + _show_tab(app, "/settings/lenses"); } -pub fn show_plugin_manager(app: &AppHandle) -> Window { - if let Some(window) = app.get_window(constants::PLUGIN_MANAGER_WIN_NAME) { - let _ = window.show(); - let _ = window.set_focus(); - return window; - } +pub fn show_plugin_manager(app: &AppHandle) { + _show_tab(app, "/settings/plugins"); +} - WindowBuilder::new( - app, - constants::PLUGIN_MANAGER_WIN_NAME, - WindowUrl::App("/settings/plugins".into()), - ) - .title("Plugins Manager") - .build() - .unwrap() +pub fn show_user_settings(app: &AppHandle) { + _show_tab(app, "/settings/user"); } pub fn alert(window: &Window, title: &str, message: &str) { diff --git a/plugins/local-file-indexer/Cargo.toml b/plugins/local-file-indexer/Cargo.toml new file mode 100644 index 000000000..cbf64a6ef --- /dev/null +++ b/plugins/local-file-indexer/Cargo.toml @@ -0,0 +1,11 @@ +[package] +name = "local-file-indexer" +version = "0.1.0" +edition = "2021" + +[[bin]] +name = "local-file-indexer" +path = "src/main.rs" + +[dependencies] +spyglass-plugin = { path = "../../crates/spyglass-plugin" } \ No newline at end of file diff --git a/plugins/local-file-indexer/src/main.rs b/plugins/local-file-indexer/src/main.rs new file mode 100644 index 000000000..e7a11a969 --- /dev/null +++ b/plugins/local-file-indexer/src/main.rs @@ -0,0 +1,3 @@ +fn main() { + println!("Hello, world!"); +}