diff --git a/Cargo.lock b/Cargo.lock index 1b54baa9f..a84f3b923 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -3249,6 +3249,7 @@ dependencies = [ "shared", "tantivy", "tar", + "url", ] [[package]] @@ -4965,21 +4966,6 @@ version = "0.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f638d531eccd6e23b980caf34876660d38e265409d8e99b397ab71eb3612fad0" -[[package]] -name = "sentry" -version = "0.27.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "73642819e7fa63eb264abc818a2f65ac8764afbe4870b5ee25bcecc491be0d4c" -dependencies = [ - "httpdate", - "reqwest", - "sentry-backtrace 0.27.0", - "sentry-contexts 0.27.0", - "sentry-core 0.27.0", - "sentry-panic 0.27.0", - "tokio", -] - [[package]] name = "sentry" version = "0.28.0" @@ -4989,26 +4975,14 @@ dependencies = [ "httpdate", "native-tls", "reqwest", - "sentry-backtrace 0.28.0", - "sentry-contexts 0.28.0", - "sentry-core 0.28.0", - "sentry-panic 0.28.0", + "sentry-backtrace", + "sentry-contexts", + "sentry-core", + "sentry-panic", "tokio", "ureq", ] -[[package]] -name = "sentry-backtrace" -version = "0.27.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "49bafa55eefc6dbc04c7dac91e8c8ab9e89e9414f3193c105cabd991bbc75134" -dependencies = [ - "backtrace", - "once_cell", - "regex", - "sentry-core 0.27.0", -] - [[package]] name = "sentry-backtrace" version = "0.28.0" @@ -5018,20 +4992,7 @@ dependencies = [ "backtrace", "once_cell", "regex", - "sentry-core 0.28.0", -] - -[[package]] -name = "sentry-contexts" -version = "0.27.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c63317c4051889e73f0b00ce4024cae3e6a225f2e18a27d2c1522eb9ce2743da" -dependencies = [ - "hostname", - "libc", - "rustc_version 0.4.0", - "sentry-core 0.27.0", - "uname", + "sentry-core", ] [[package]] @@ -5044,23 +5005,10 @@ dependencies = [ "libc", "os_info", "rustc_version 0.4.0", - "sentry-core 0.28.0", + "sentry-core", "uname", ] -[[package]] -name = "sentry-core" -version = "0.27.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5a4591a2d128af73b1b819ab95f143bc6a2fbe48cd23a4c45e1ee32177e66ae6" -dependencies = [ - "once_cell", - "rand 0.8.5", - "sentry-types 0.27.0", - "serde", - "serde_json", -] - [[package]] name = "sentry-core" version = "0.28.0" @@ -5069,46 +5017,30 @@ checksum = "ff58433a7ad557b586a09c42c4298d5f3ddb0c777e1a79d950e510d7b93fce0e" dependencies = [ "once_cell", "rand 0.8.5", - "sentry-types 0.28.0", + "sentry-types", "serde", "serde_json", ] -[[package]] -name = "sentry-panic" -version = "0.27.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "696c74c5882d5a0d5b4a31d0ff3989b04da49be7983b7f52a52c667da5b480bf" -dependencies = [ - "sentry-backtrace 0.27.0", - "sentry-core 0.27.0", -] - [[package]] name = "sentry-panic" version = "0.28.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "4145005d9b5c117132765c34e2cb33e9d24d16e73d7f3a357122b77fe3a3b815" dependencies = [ - "sentry-backtrace 0.28.0", - "sentry-core 0.28.0", + "sentry-backtrace", + "sentry-core", ] [[package]] -name = "sentry-types" -version = "0.27.0" +name = "sentry-tracing" +version = "0.28.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "823923ae5f54a729159d720aa12181673044ee5c79cbda3be09e56f885e5468f" +checksum = "fc8d5bae1e1c06d96a966efc425bf1479a90464de99757d40601ce449f91fbed" dependencies = [ - "debugid", - "getrandom 0.2.8", - "hex", - "serde", - "serde_json", - "thiserror", - "time 0.3.17", - "url", - "uuid 1.2.1", + "sentry-core", + "tracing-core", + "tracing-subscriber", ] [[package]] @@ -5475,7 +5407,7 @@ dependencies = [ [[package]] name = "spyglass" -version = "22.11.5" +version = "22.11.6" dependencies = [ "addr", "anyhow", @@ -5506,7 +5438,8 @@ dependencies = [ "reqwest", "ron", "rusqlite", - "sentry 0.28.0", + "sentry", + "sentry-tracing", "serde", "sha2", "shared", @@ -5539,10 +5472,12 @@ dependencies = [ "log", "migration", "num-format", + "objc", "open", "reqwest", "ron", - "sentry 0.27.0", + "sentry", + "sentry-tracing", "serde", "serde_json", "shared", diff --git a/README.md b/README.md index aa7f74d86..e7a69d1c1 100644 --- a/README.md +++ b/README.md @@ -5,13 +5,13 @@

Download now: - + macOS | - + Windows | - + Linux (AppImage)
diff --git a/VERSION.json b/VERSION.json index 08b25081f..7621b9ceb 100644 --- a/VERSION.json +++ b/VERSION.json @@ -1,23 +1,23 @@ { - "version": "22.11.4", - "notes": "See full release notes here: https://github.com/a5huynh/spyglass/releases/tag/v2022.11.4", - "pub_date": "2022-11-10T00:14:07Z", + "version": "22.11.5", + "notes": "See full release notes here: https://github.com/a5huynh/spyglass/releases/tag/v2022.11.5", + "pub_date": "2022-11-11T23:51:08Z", "platforms": { "darwin-x86_64": { - "signature": "dW50cnVzdGVkIGNvbW1lbnQ6IHNpZ25hdHVyZSBmcm9tIHRhdXJpIHNlY3JldCBrZXkKUlVSRk9WYXdDUXphYm9wK1N0cWtLbFA1UHhxT2JrSGJXM2toTFlMa3B1UTNFV1BPN2hmUWtMQ2NuVHhlSTBENjBoTjM2dzRTSWtjOFFkRDRiQStoU3NBUU95aDdIOUpHcEFZPQp0cnVzdGVkIGNvbW1lbnQ6IHRpbWVzdGFtcDoxNjY4MDM5MTg2CWZpbGU6U3B5Z2xhc3MuYXBwLnRhci5negppVm5nK2hybHBKZzl6UUtMUHFQcDZoSWFKaXZndlp3Y21oeWNOOE5YMHZvbFVwWDNOZEdTSUswZFRvMWV3MHppeXB2K1B4N3dDM2p2OVdYb1RWaWhCZz09Cg==", - "url": "https://github.com/a5huynh/spyglass/releases/download/v2022.11.4/Spyglass.app.tar.gz" + "signature": "dW50cnVzdGVkIGNvbW1lbnQ6IHNpZ25hdHVyZSBmcm9tIHRhdXJpIHNlY3JldCBrZXkKUlVSRk9WYXdDUXphYnRKM1orYmdMOXVSU3NnSVcweFlGdlpZOHYyWi9PZTAwVTA0MlZya0VpanFKM2dLWDBHK1QzVGVIRmlQaVNSM203ejFOS1RqUS9YZ0lDcmJ4Y29URlF3PQp0cnVzdGVkIGNvbW1lbnQ6IHRpbWVzdGFtcDoxNjY4MjEwMjY4CWZpbGU6U3B5Z2xhc3MuYXBwLnRhci5negp5WTd2aXJrQ0hoTVBrVE85dGxoK1VNYWsrMjcrd1pWaEZyalh2cUhXY0xOSjA4UlVld2QrT2hZK240ZTM5amJWWGFyK0JMbVpKR0MveEhDa0JMQ0pCdz09Cg==", + "url": "https://github.com/a5huynh/spyglass/releases/download/v2022.11.5/Spyglass.app.tar.gz" }, "darwin-aarch64": { - "signature": "dW50cnVzdGVkIGNvbW1lbnQ6IHNpZ25hdHVyZSBmcm9tIHRhdXJpIHNlY3JldCBrZXkKUlVSRk9WYXdDUXphYm9wK1N0cWtLbFA1UHhxT2JrSGJXM2toTFlMa3B1UTNFV1BPN2hmUWtMQ2NuVHhlSTBENjBoTjM2dzRTSWtjOFFkRDRiQStoU3NBUU95aDdIOUpHcEFZPQp0cnVzdGVkIGNvbW1lbnQ6IHRpbWVzdGFtcDoxNjY4MDM5MTg2CWZpbGU6U3B5Z2xhc3MuYXBwLnRhci5negppVm5nK2hybHBKZzl6UUtMUHFQcDZoSWFKaXZndlp3Y21oeWNOOE5YMHZvbFVwWDNOZEdTSUswZFRvMWV3MHppeXB2K1B4N3dDM2p2OVdYb1RWaWhCZz09Cg==", - "url": "https://github.com/a5huynh/spyglass/releases/download/v2022.11.4/Spyglass.app.tar.gz" + "signature": "dW50cnVzdGVkIGNvbW1lbnQ6IHNpZ25hdHVyZSBmcm9tIHRhdXJpIHNlY3JldCBrZXkKUlVSRk9WYXdDUXphYnRKM1orYmdMOXVSU3NnSVcweFlGdlpZOHYyWi9PZTAwVTA0MlZya0VpanFKM2dLWDBHK1QzVGVIRmlQaVNSM203ejFOS1RqUS9YZ0lDcmJ4Y29URlF3PQp0cnVzdGVkIGNvbW1lbnQ6IHRpbWVzdGFtcDoxNjY4MjEwMjY4CWZpbGU6U3B5Z2xhc3MuYXBwLnRhci5negp5WTd2aXJrQ0hoTVBrVE85dGxoK1VNYWsrMjcrd1pWaEZyalh2cUhXY0xOSjA4UlVld2QrT2hZK240ZTM5amJWWGFyK0JMbVpKR0MveEhDa0JMQ0pCdz09Cg==", + "url": "https://github.com/a5huynh/spyglass/releases/download/v2022.11.5/Spyglass.app.tar.gz" }, "linux-x86_64": { - "signature": "dW50cnVzdGVkIGNvbW1lbnQ6IHNpZ25hdHVyZSBmcm9tIHRhdXJpIHNlY3JldCBrZXkKUlVSRk9WYXdDUXphYnJJL3F5UWJZbldBQ3JSMUJXb3NDT1ErOEtnWmU0RDFWRFBkVjlLQ09seTc1anZLU09EMkVMNWFpMDMyY29EUEZzVmtPdTZON1F4dWpocDQrbklYdXdVPQp0cnVzdGVkIGNvbW1lbnQ6IHRpbWVzdGFtcDoxNjY4MDM3NTY5CWZpbGU6c3B5Z2xhc3NfMjIuMTEuNF9hbWQ2NC5BcHBJbWFnZS50YXIuZ3oKTk1vdUlCY000TStkUWtKS2xXem1kUlcySGZGRVREYjN3M216ZnoyeDE3cXFOMUtTM1F1U3U4aG1yUThkcllKNjRqYy82Z0ozOWNHR3VkczdxNUw5REE9PQo=", - "url": "https://github.com/a5huynh/spyglass/releases/download/v2022.11.4/spyglass_22.11.4_amd64.AppImage.tar.gz" + "signature": "dW50cnVzdGVkIGNvbW1lbnQ6IHNpZ25hdHVyZSBmcm9tIHRhdXJpIHNlY3JldCBrZXkKUlVSRk9WYXdDUXphYnQ2elVvODd4ZHVNbGZ6a2VWdFZBNFp3dDRrYVRCdklNV1M4a2F3R0N2VGVrTE9ySCtldW4veG51bnBsMDZIamJwMnpHbk5tVGhNUkFDV3hMUElvdWdNPQp0cnVzdGVkIGNvbW1lbnQ6IHRpbWVzdGFtcDoxNjY4MjA4ODExCWZpbGU6c3B5Z2xhc3NfMjIuMTEuNV9hbWQ2NC5BcHBJbWFnZS50YXIuZ3oKd1JVWVYyWllwUk1qMkVreDAxbkRwbHNjRnNHQTZZS25xaDlRNit3WkNSOVN4YXpTdHZudU9zL1FXQzZGR1JVMXlLVytmbmJrczc1SzFoeUNpZzJBQlE9PQo=", + "url": "https://github.com/a5huynh/spyglass/releases/download/v2022.11.5/spyglass_22.11.5_amd64.AppImage.tar.gz" }, "windows-x86_64": { - "signature": "dW50cnVzdGVkIGNvbW1lbnQ6IHNpZ25hdHVyZSBmcm9tIHRhdXJpIHNlY3JldCBrZXkKUlVSRk9WYXdDUXphYmozYklGUUxwR2NnRnJGQnh1T3RSUEdFWXlMMEwvNzVoQzIvcmk2ekJzbjU1NStJZHpkbHhFc1Z5SkFSdjBlcDVKSWU2Tk9nRk1WZUpMNmV6TEV5UlE0PQp0cnVzdGVkIGNvbW1lbnQ6IHRpbWVzdGFtcDoxNjY4MDM5MTUzCWZpbGU6U3B5Z2xhc3NfMjIuMTEuNF94NjRfZW4tVVMubXNpLnppcApRVUVGL21LS1dZZ3RiaVQyalpqdG03eWdmZkpaWVF1MWg2dUQ2QUdobnBBQjlQNm8vY3g1YTRzeXl6UXNIQVMzdkhmc1BhOTdFRUVjNFFrcFVNbjBCUT09Cg==", - "url": "https://github.com/a5huynh/spyglass/releases/download/v2022.11.4/Spyglass_22.11.4_x64_en-US.msi.zip" + "signature": "dW50cnVzdGVkIGNvbW1lbnQ6IHNpZ25hdHVyZSBmcm9tIHRhdXJpIHNlY3JldCBrZXkKUlVSRk9WYXdDUXphYnJjcDZlNzd1N2hqNE5KMmNGQ05pd1lFRE8yY211elVHUVIxNC9NcXJIdXdHZ3Zzcjh2bHpUVC9BMDFxQzFkMGF6THJSN0RDMUx1cU5INkJSZGFwWndRPQp0cnVzdGVkIGNvbW1lbnQ6IHRpbWVzdGFtcDoxNjY4MjA5Nzg0CWZpbGU6U3B5Z2xhc3NfMjIuMTEuNV94NjRfZW4tVVMubXNpLnppcApCT0V0QW9TYUVNa3A5ckFGSUpIeTVPT1Z0MXkvVnhnVXc5MFQxSUFRY1cvR1JTVCt0NWp5VUVnVTZFck1ZRlJ4UFNYTnR5Um5lYTNHUjZSSVJ0NHFCdz09Cg==", + "url": "https://github.com/a5huynh/spyglass/releases/download/v2022.11.5/Spyglass_22.11.5_x64_en-US.msi.zip" } } } \ No newline at end of file diff --git a/crates/client/public/main.css b/crates/client/public/main.css index dbbed6a3b..a2191476a 100644 --- a/crates/client/public/main.css +++ b/crates/client/public/main.css @@ -1 +1 @@ -/*! tailwindcss v3.1.8 | MIT License | https://tailwindcss.com*/*,:after,:before{box-sizing:border-box;border:0 solid #e5e7eb}:after,:before{--tw-content:""}html{line-height:1.5;-webkit-text-size-adjust:100%;-moz-tab-size:4;-o-tab-size:4;tab-size:4;font-family:ui-sans-serif,system-ui,-apple-system,BlinkMacSystemFont,Segoe UI,Roboto,Helvetica Neue,Arial,Noto Sans,sans-serif,Apple Color Emoji,Segoe UI Emoji,Segoe UI Symbol,Noto Color Emoji}body{margin:0;line-height:inherit}hr{height:0;color:inherit;border-top-width:1px}abbr:where([title]){-webkit-text-decoration:underline dotted;text-decoration:underline dotted}h1,h2,h3,h4,h5,h6{font-size:inherit;font-weight:inherit}a{color:inherit;text-decoration:inherit}b,strong{font-weight:bolder}code,kbd,pre,samp{font-family:ui-monospace,SFMono-Regular,Menlo,Monaco,Consolas,Liberation Mono,Courier New,monospace;font-size:1em}small{font-size:80%}sub,sup{font-size:75%;line-height:0;position:relative;vertical-align:initial}sub{bottom:-.25em}sup{top:-.5em}table{text-indent:0;border-color:inherit;border-collapse:collapse}button,input,optgroup,select,textarea{font-family:inherit;font-size:100%;font-weight:inherit;line-height:inherit;color:inherit;margin:0;padding:0}button,select{text-transform:none}[type=button],[type=reset],[type=submit],button{-webkit-appearance:button;background-color:initial;background-image:none}:-moz-focusring{outline:auto}:-moz-ui-invalid{box-shadow:none}progress{vertical-align:initial}::-webkit-inner-spin-button,::-webkit-outer-spin-button{height:auto}[type=search]{-webkit-appearance:textfield;outline-offset:-2px}::-webkit-search-decoration{-webkit-appearance:none}::-webkit-file-upload-button{-webkit-appearance:button;font:inherit}summary{display:list-item}blockquote,dd,dl,figure,h1,h2,h3,h4,h5,h6,hr,p,pre{margin:0}fieldset{margin:0}fieldset,legend{padding:0}menu,ol,ul{list-style:none;margin:0;padding:0}textarea{resize:vertical}input::-moz-placeholder,textarea::-moz-placeholder{opacity:1;color:#9ca3af}input::placeholder,textarea::placeholder{opacity:1;color:#9ca3af}[role=button],button{cursor:pointer}:disabled{cursor:default}audio,canvas,embed,iframe,img,object,svg,video{display:block;vertical-align:middle}img,video{max-width:100%;height:auto}*,:after,:before{--tw-border-spacing-x:0;--tw-border-spacing-y:0;--tw-translate-x:0;--tw-translate-y:0;--tw-rotate:0;--tw-skew-x:0;--tw-skew-y:0;--tw-scale-x:1;--tw-scale-y:1;--tw-pan-x: ;--tw-pan-y: ;--tw-pinch-zoom: ;--tw-scroll-snap-strictness:proximity;--tw-ordinal: ;--tw-slashed-zero: ;--tw-numeric-figure: ;--tw-numeric-spacing: ;--tw-numeric-fraction: ;--tw-ring-inset: ;--tw-ring-offset-width:0px;--tw-ring-offset-color:#fff;--tw-ring-color:rgba(59,130,246,.5);--tw-ring-offset-shadow:0 0 #0000;--tw-ring-shadow:0 0 #0000;--tw-shadow:0 0 #0000;--tw-shadow-colored:0 0 #0000;--tw-blur: ;--tw-brightness: ;--tw-contrast: ;--tw-grayscale: ;--tw-hue-rotate: ;--tw-invert: ;--tw-saturate: ;--tw-sepia: ;--tw-drop-shadow: ;--tw-backdrop-blur: ;--tw-backdrop-brightness: ;--tw-backdrop-contrast: ;--tw-backdrop-grayscale: ;--tw-backdrop-hue-rotate: ;--tw-backdrop-invert: ;--tw-backdrop-opacity: ;--tw-backdrop-saturate: ;--tw-backdrop-sepia: }::-webkit-backdrop{--tw-border-spacing-x:0;--tw-border-spacing-y:0;--tw-translate-x:0;--tw-translate-y:0;--tw-rotate:0;--tw-skew-x:0;--tw-skew-y:0;--tw-scale-x:1;--tw-scale-y:1;--tw-pan-x: ;--tw-pan-y: ;--tw-pinch-zoom: ;--tw-scroll-snap-strictness:proximity;--tw-ordinal: ;--tw-slashed-zero: ;--tw-numeric-figure: ;--tw-numeric-spacing: ;--tw-numeric-fraction: ;--tw-ring-inset: ;--tw-ring-offset-width:0px;--tw-ring-offset-color:#fff;--tw-ring-color:rgba(59,130,246,.5);--tw-ring-offset-shadow:0 0 #0000;--tw-ring-shadow:0 0 #0000;--tw-shadow:0 0 #0000;--tw-shadow-colored:0 0 #0000;--tw-blur: ;--tw-brightness: ;--tw-contrast: ;--tw-grayscale: ;--tw-hue-rotate: ;--tw-invert: ;--tw-saturate: ;--tw-sepia: ;--tw-drop-shadow: ;--tw-backdrop-blur: ;--tw-backdrop-brightness: ;--tw-backdrop-contrast: ;--tw-backdrop-grayscale: ;--tw-backdrop-hue-rotate: ;--tw-backdrop-invert: ;--tw-backdrop-opacity: ;--tw-backdrop-saturate: ;--tw-backdrop-sepia: }::backdrop{--tw-border-spacing-x:0;--tw-border-spacing-y:0;--tw-translate-x:0;--tw-translate-y:0;--tw-rotate:0;--tw-skew-x:0;--tw-skew-y:0;--tw-scale-x:1;--tw-scale-y:1;--tw-pan-x: ;--tw-pan-y: ;--tw-pinch-zoom: ;--tw-scroll-snap-strictness:proximity;--tw-ordinal: ;--tw-slashed-zero: ;--tw-numeric-figure: ;--tw-numeric-spacing: ;--tw-numeric-fraction: ;--tw-ring-inset: ;--tw-ring-offset-width:0px;--tw-ring-offset-color:#fff;--tw-ring-color:rgba(59,130,246,.5);--tw-ring-offset-shadow:0 0 #0000;--tw-ring-shadow:0 0 #0000;--tw-shadow:0 0 #0000;--tw-shadow-colored:0 0 #0000;--tw-blur: ;--tw-brightness: ;--tw-contrast: ;--tw-grayscale: ;--tw-hue-rotate: ;--tw-invert: ;--tw-saturate: ;--tw-sepia: ;--tw-drop-shadow: ;--tw-backdrop-blur: ;--tw-backdrop-brightness: ;--tw-backdrop-contrast: ;--tw-backdrop-grayscale: ;--tw-backdrop-hue-rotate: ;--tw-backdrop-invert: ;--tw-backdrop-opacity: ;--tw-backdrop-saturate: ;--tw-backdrop-sepia: }.form-input,.form-multiselect,.form-select,.form-textarea{-webkit-appearance:none;-moz-appearance:none;appearance:none;background-color:#fff;border-color:#6b7280;border-width:1px;border-radius:0;padding:.5rem .75rem;font-size:1rem;line-height:1.5rem;--tw-shadow:0 0 #0000}.form-input:focus,.form-multiselect:focus,.form-select:focus,.form-textarea: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::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-day-field,.form-input::-webkit-datetime-edit-hour-field,.form-input::-webkit-datetime-edit-meridiem-field,.form-input::-webkit-datetime-edit-millisecond-field,.form-input::-webkit-datetime-edit-minute-field,.form-input::-webkit-datetime-edit-month-field,.form-input::-webkit-datetime-edit-second-field,.form-input::-webkit-datetime-edit-year-field{padding-top:0;padding-bottom:0}.sr-only{position:absolute;width:1px;height:1px;padding:0;margin:-1px;overflow:hidden;clip:rect(0,0,0,0);white-space:nowrap;border-width:0}.fixed{position:fixed}.absolute{position:absolute}.relative{position:relative}.sticky{position:-webkit-sticky;position:sticky}.top-0{top:0}.left-0{left:0}.bottom-0{bottom:0}.left-1{left:.25rem}.top-1{top:.25rem}.z-50{z-index:50}.z-40{z-index:40}.col-span-3{grid-column:span 3/span 3}.m-auto{margin:auto}.my-3{margin-top:.75rem;margin-bottom:.75rem}.mx-auto{margin-left:auto;margin-right:auto}.mx-1{margin-left:.25rem;margin-right:.25rem}.ml-3{margin-left:.75rem}.-ml-16{margin-left:-4rem}.-mt-2{margin-top:-.5rem}.mb-6{margin-bottom:1.5rem}.mb-2{margin-bottom:.5rem}.mr-2{margin-right:.5rem}.ml-auto{margin-left:auto}.mb-4{margin-bottom:1rem}.mr-8{margin-right:2rem}.ml-2{margin-left:.5rem}.mt-4{margin-top:1rem}.mt-1{margin-top:.25rem}.mb-8{margin-bottom:2rem}.mr-1{margin-right:.25rem}.block{display:block}.flex{display:flex}.grid{display:grid}.contents{display:contents}.hidden{display:none}.h-4{height:1rem}.h-5{height:1.25rem}.h-8{height:2rem}.h-12{height:3rem}.h-screen{height:100vh}.h-6{height:1.5rem}.h-10{height:2.5rem}.h-16{height:4rem}.h-full{height:100%}.h-48{height:12rem}.h-20{height:5rem}.h-2{height:.5rem}.h-64{height:16rem}.h-40{height:10rem}.max-h-16{max-height:4rem}.w-4{width:1rem}.w-5{width:1.25rem}.w-8{width:2rem}.w-screen{width:100vw}.w-12{width:3rem}.w-\[30rem\]{width:30rem}.w-full{width:100%}.w-48{width:12rem}.w-6{width:1.5rem}.w-auto{width:auto}.w-16{width:4rem}.w-20{width:5rem}.w-2{width:.5rem}.w-14{width:3.5rem}.min-w-max{min-width:-webkit-max-content;min-width:-moz-max-content;min-width:max-content}.flex-none{flex:none}.flex-1{flex:1 1 0%}.flex-grow,.grow{flex-grow:1}@-webkit-keyframes spin{to{transform:rotate(1turn)}}@keyframes spin{to{transform:rotate(1turn)}}.animate-spin{-webkit-animation:spin 1s linear infinite;animation:spin 1s linear infinite}@-webkit-keyframes wiggle{0%,to{transform:rotate(-6deg)}50%{transform:rotate(6deg)}}.animate-wiggle-short{-webkit-animation:wiggle 1s ease-in-out 10;animation:wiggle 1s ease-in-out 10}.cursor-pointer{cursor:pointer}.list-none{list-style-type:none}.grid-cols-3{grid-template-columns:repeat(3,minmax(0,1fr))}.flex-row{flex-direction:row}.flex-col{flex-direction:column}.flex-nowrap{flex-wrap:nowrap}.place-content-center{place-content:center}.place-items-center{place-items:center}.content-center{align-content:center}.items-center{align-items:center}.justify-end{justify-content:flex-end}.justify-center{justify-content:center}.gap-4{gap:1rem}.gap-1{gap:.25rem}.gap-8{gap:2rem}.gap-2{gap:.5rem}.divide-y>:not([hidden])~:not([hidden]){--tw-divide-y-reverse:0;border-top-width:calc(1px*(1 - var(--tw-divide-y-reverse)));border-bottom-width:calc(1px*var(--tw-divide-y-reverse))}.divide-neutral-600>:not([hidden])~:not([hidden]){--tw-divide-opacity:1;border-color:rgb(82 82 82/var(--tw-divide-opacity))}.place-self-end{place-self:end}.self-start{align-self:flex-start}.justify-self-end{justify-self:end}.overflow-hidden{overflow:hidden}.overflow-y-auto{overflow-y:auto}.overflow-x-hidden{overflow-x:hidden}.truncate{overflow:hidden;white-space:nowrap}.text-ellipsis,.truncate{text-overflow:ellipsis}.rounded-lg{border-radius:.5rem}.rounded{border-radius:.25rem}.rounded-md{border-radius:.375rem}.rounded-full{border-radius:9999px}.rounded-xl{border-radius:.75rem}.rounded-l-lg{border-top-left-radius:.5rem;border-bottom-left-radius:.5rem}.rounded-r-lg{border-top-right-radius:.5rem;border-bottom-right-radius:.5rem}.border{border-width:1px}.border-b-2{border-bottom-width:2px}.border-t{border-top-width:1px}.border-t-2{border-top-width:2px}.border-neutral-600{--tw-border-opacity:1;border-color:rgb(82 82 82/var(--tw-border-opacity))}.border-stone-900{--tw-border-opacity:1;border-color:rgb(28 25 23/var(--tw-border-opacity))}.border-green-500{--tw-border-opacity:1;border-color:rgb(34 197 94/var(--tw-border-opacity))}.border-transparent{border-color:transparent}.border-neutral-500{--tw-border-opacity:1;border-color:rgb(115 115 115/var(--tw-border-opacity))}.border-neutral-700{--tw-border-opacity:1;border-color:rgb(64 64 64/var(--tw-border-opacity))}.border-stone-800{--tw-border-opacity:1;border-color:rgb(41 37 36/var(--tw-border-opacity))}.bg-transparent{background-color:initial}.bg-cyan-700{--tw-bg-opacity:1;background-color:rgb(14 116 144/var(--tw-bg-opacity))}.bg-neutral-800{--tw-bg-opacity:1;background-color:rgb(38 38 38/var(--tw-bg-opacity))}.bg-stone-800{--tw-bg-opacity:1;background-color:rgb(41 37 36/var(--tw-bg-opacity))}.bg-cyan-900{--tw-bg-opacity:1;background-color:rgb(22 78 99/var(--tw-bg-opacity))}.bg-neutral-700{--tw-bg-opacity:1;background-color:rgb(64 64 64/var(--tw-bg-opacity))}.bg-neutral-900{--tw-bg-opacity:1;background-color:rgb(23 23 23/var(--tw-bg-opacity))}.bg-stone-700{--tw-bg-opacity:1;background-color:rgb(68 64 60/var(--tw-bg-opacity))}.bg-stone-900{--tw-bg-opacity:1;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))}.bg-lime-600{--tw-bg-opacity:1;background-color:rgb(101 163 13/var(--tw-bg-opacity))}.bg-lime-800{--tw-bg-opacity:1;background-color:rgb(63 98 18/var(--tw-bg-opacity))}.bg-neutral-400{--tw-bg-opacity:1;background-color:rgb(163 163 163/var(--tw-bg-opacity))}.bg-white{--tw-bg-opacity:1;background-color:rgb(255 255 255/var(--tw-bg-opacity))}.stroke-slate-400{stroke:#94a3b8}.p-2{padding:.5rem}.p-3{padding:.75rem}.p-4{padding:1rem}.p-16{padding:4rem}.p-0{padding:0}.p-0\.5{padding:.125rem}.p-1\.5{padding:.375rem}.p-1{padding:.25rem}.px-8{padding-left:2rem;padding-right:2rem}.px-4{padding-left:1rem;padding-right:1rem}.py-2{padding-top:.5rem;padding-bottom:.5rem}.py-4{padding-top:1rem;padding-bottom:1rem}.py-1{padding-top:.25rem;padding-bottom:.25rem}.px-2{padding-left:.5rem;padding-right:.5rem}.px-0\.5{padding-left:.125rem;padding-right:.125rem}.px-0{padding-left:0;padding-right:0}.px-6{padding-left:1.5rem;padding-right:1.5rem}.py-12{padding-top:3rem;padding-bottom:3rem}.pl-1{padding-left:.25rem}.pt-4{padding-top:1rem}.pb-4{padding-bottom:1rem}.pl-6{padding-left:1.5rem}.pl-4{padding-left:1rem}.pr-4{padding-right:1rem}.pb-8{padding-bottom:2rem}.pb-2{padding-bottom:.5rem}.pr-0{padding-right:0}.pt-2{padding-top:.5rem}.pt-8{padding-top:2rem}.pb-16{padding-bottom:4rem}.pr-2{padding-right:.5rem}.text-center{text-align:center}.text-right{text-align:right}.align-middle{vertical-align:middle}.text-sm{font-size:.875rem;line-height:1.25rem}.text-4xl{font-size:2.25rem;line-height:2.5rem}.text-xl{font-size:1.25rem;line-height:1.75rem}.text-xs{font-size:.75rem;line-height:1rem}.text-lg{font-size:1.125rem;line-height:1.75rem}.text-2xl{font-size:1.5rem;line-height:2rem}.text-\[8px\]{font-size:8px}.text-5xl{font-size:3rem;line-height:1}.font-medium{font-weight:500}.font-bold{font-weight:700}.uppercase{text-transform:uppercase}.leading-relaxed{line-height:1.625}.text-white{--tw-text-opacity:1;color:rgb(255 255 255/var(--tw-text-opacity))}.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-cyan-500{--tw-text-opacity:1;color:rgb(6 182 212/var(--tw-text-opacity))}.text-neutral-400{--tw-text-opacity:1;color:rgb(163 163 163/var(--tw-text-opacity))}.text-gray-500{--tw-text-opacity:1;color:rgb(107 114 128/var(--tw-text-opacity))}.text-red-400{--tw-text-opacity:1;color:rgb(248 113 113/var(--tw-text-opacity))}.text-neutral-300{--tw-text-opacity:1;color:rgb(212 212 212/var(--tw-text-opacity))}.text-neutral-500{--tw-text-opacity:1;color:rgb(115 115 115/var(--tw-text-opacity))}.text-cyan-400{--tw-text-opacity:1;color:rgb(34 211 238/var(--tw-text-opacity))}.text-green-400{--tw-text-opacity:1;color:rgb(74 222 128/var(--tw-text-opacity))}.text-cyan-600{--tw-text-opacity:1;color:rgb(8 145 178/var(--tw-text-opacity))}.text-black{--tw-text-opacity:1;color:rgb(0 0 0/var(--tw-text-opacity))}.text-stone-500{--tw-text-opacity:1;color:rgb(120 113 108/var(--tw-text-opacity))}.text-yellow-500{--tw-text-opacity:1;color:rgb(234 179 8/var(--tw-text-opacity))}.text-red-500{--tw-text-opacity:1;color:rgb(239 68 68/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)}.transition{transition-property:color,background-color,border-color,fill,stroke,opacity,box-shadow,transform,filter,-webkit-text-decoration-color,-webkit-backdrop-filter;transition-property:color,background-color,border-color,text-decoration-color,fill,stroke,opacity,box-shadow,transform,filter,backdrop-filter;transition-property:color,background-color,border-color,text-decoration-color,fill,stroke,opacity,box-shadow,transform,filter,backdrop-filter,-webkit-text-decoration-color,-webkit-backdrop-filter;transition-timing-function:cubic-bezier(.4,0,.2,1);transition-duration:.15s}input:checked~.dot{transform:translateX(100%);background-color:#48bb78}*{scrollbar-width:thin;scrollbar-color:#404040 #171717}::-webkit-scrollbar{width:10px}::-webkit-scrollbar-track{background:#171717}::-webkit-scrollbar-thumb{background:#404040;border-radius:100vh;border:2px solid #171717}::-webkit-scrollbar-thumb:hover{background:#164e63}@keyframes wiggle{0%,to{transform:rotate(-6deg)}50%{transform:rotate(6deg)}}.hover\:animate-wiggle:hover{-webkit-animation:wiggle 1s ease-in-out infinite;animation:wiggle 1s ease-in-out infinite}.hover\:border-green-500:hover{--tw-border-opacity:1;border-color:rgb(34 197 94/var(--tw-border-opacity))}.hover\:bg-neutral-600:hover{--tw-bg-opacity:1;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\:bg-cyan-900:hover{--tw-bg-opacity:1;background-color:rgb(22 78 99/var(--tw-bg-opacity))}.hover\:bg-blue-900:hover{--tw-bg-opacity:1;background-color:rgb(30 58 138/var(--tw-bg-opacity))}.hover\:bg-amber-700:hover{--tw-bg-opacity:1;background-color:rgb(180 83 9/var(--tw-bg-opacity))}.hover\:bg-indigo-900:hover{--tw-bg-opacity:1;background-color:rgb(49 46 129/var(--tw-bg-opacity))}.hover\:bg-green-900:hover{--tw-bg-opacity:1;background-color:rgb(20 83 45/var(--tw-bg-opacity))}.hover\:text-red-600:hover{--tw-text-opacity:1;color:rgb(220 38 38/var(--tw-text-opacity))}.hover\:text-cyan-500:hover{--tw-text-opacity:1;color:rgb(6 182 212/var(--tw-text-opacity))}.hover\:text-red-500:hover{--tw-text-opacity:1;color:rgb(239 68 68/var(--tw-text-opacity))}.hover\:text-white:hover{--tw-text-opacity:1;color:rgb(255 255 255/var(--tw-text-opacity))}.focus\:outline-none:focus{outline:2px solid transparent;outline-offset:2px}.active\:bg-neutral-700:active{--tw-bg-opacity:1;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}.group:hover .group-hover\:fill-red-400{fill:#f87171}.group:hover .group-hover\:stroke-white{stroke:#fff}.group:hover .group-hover\:text-neutral-400{--tw-text-opacity:1;color:rgb(163 163 163/var(--tw-text-opacity))} \ No newline at end of file +/*! tailwindcss v3.1.8 | MIT License | https://tailwindcss.com*/*,:after,:before{box-sizing:border-box;border:0 solid #e5e7eb}:after,:before{--tw-content:""}html{line-height:1.5;-webkit-text-size-adjust:100%;-moz-tab-size:4;-o-tab-size:4;tab-size:4;font-family:ui-sans-serif,system-ui,-apple-system,BlinkMacSystemFont,Segoe UI,Roboto,Helvetica Neue,Arial,Noto Sans,sans-serif,Apple Color Emoji,Segoe UI Emoji,Segoe UI Symbol,Noto Color Emoji}body{margin:0;line-height:inherit}hr{height:0;color:inherit;border-top-width:1px}abbr:where([title]){-webkit-text-decoration:underline dotted;text-decoration:underline dotted}h1,h2,h3,h4,h5,h6{font-size:inherit;font-weight:inherit}a{color:inherit;text-decoration:inherit}b,strong{font-weight:bolder}code,kbd,pre,samp{font-family:ui-monospace,SFMono-Regular,Menlo,Monaco,Consolas,Liberation Mono,Courier New,monospace;font-size:1em}small{font-size:80%}sub,sup{font-size:75%;line-height:0;position:relative;vertical-align:initial}sub{bottom:-.25em}sup{top:-.5em}table{text-indent:0;border-color:inherit;border-collapse:collapse}button,input,optgroup,select,textarea{font-family:inherit;font-size:100%;font-weight:inherit;line-height:inherit;color:inherit;margin:0;padding:0}button,select{text-transform:none}[type=button],[type=reset],[type=submit],button{-webkit-appearance:button;background-color:initial;background-image:none}:-moz-focusring{outline:auto}:-moz-ui-invalid{box-shadow:none}progress{vertical-align:initial}::-webkit-inner-spin-button,::-webkit-outer-spin-button{height:auto}[type=search]{-webkit-appearance:textfield;outline-offset:-2px}::-webkit-search-decoration{-webkit-appearance:none}::-webkit-file-upload-button{-webkit-appearance:button;font:inherit}summary{display:list-item}blockquote,dd,dl,figure,h1,h2,h3,h4,h5,h6,hr,p,pre{margin:0}fieldset{margin:0}fieldset,legend{padding:0}menu,ol,ul{list-style:none;margin:0;padding:0}textarea{resize:vertical}input::-moz-placeholder,textarea::-moz-placeholder{opacity:1;color:#9ca3af}input::placeholder,textarea::placeholder{opacity:1;color:#9ca3af}[role=button],button{cursor:pointer}:disabled{cursor:default}audio,canvas,embed,iframe,img,object,svg,video{display:block;vertical-align:middle}img,video{max-width:100%;height:auto}*,:after,:before{--tw-border-spacing-x:0;--tw-border-spacing-y:0;--tw-translate-x:0;--tw-translate-y:0;--tw-rotate:0;--tw-skew-x:0;--tw-skew-y:0;--tw-scale-x:1;--tw-scale-y:1;--tw-pan-x: ;--tw-pan-y: ;--tw-pinch-zoom: ;--tw-scroll-snap-strictness:proximity;--tw-ordinal: ;--tw-slashed-zero: ;--tw-numeric-figure: ;--tw-numeric-spacing: ;--tw-numeric-fraction: ;--tw-ring-inset: ;--tw-ring-offset-width:0px;--tw-ring-offset-color:#fff;--tw-ring-color:rgba(59,130,246,.5);--tw-ring-offset-shadow:0 0 #0000;--tw-ring-shadow:0 0 #0000;--tw-shadow:0 0 #0000;--tw-shadow-colored:0 0 #0000;--tw-blur: ;--tw-brightness: ;--tw-contrast: ;--tw-grayscale: ;--tw-hue-rotate: ;--tw-invert: ;--tw-saturate: ;--tw-sepia: ;--tw-drop-shadow: ;--tw-backdrop-blur: ;--tw-backdrop-brightness: ;--tw-backdrop-contrast: ;--tw-backdrop-grayscale: ;--tw-backdrop-hue-rotate: ;--tw-backdrop-invert: ;--tw-backdrop-opacity: ;--tw-backdrop-saturate: ;--tw-backdrop-sepia: }::-webkit-backdrop{--tw-border-spacing-x:0;--tw-border-spacing-y:0;--tw-translate-x:0;--tw-translate-y:0;--tw-rotate:0;--tw-skew-x:0;--tw-skew-y:0;--tw-scale-x:1;--tw-scale-y:1;--tw-pan-x: ;--tw-pan-y: ;--tw-pinch-zoom: ;--tw-scroll-snap-strictness:proximity;--tw-ordinal: ;--tw-slashed-zero: ;--tw-numeric-figure: ;--tw-numeric-spacing: ;--tw-numeric-fraction: ;--tw-ring-inset: ;--tw-ring-offset-width:0px;--tw-ring-offset-color:#fff;--tw-ring-color:rgba(59,130,246,.5);--tw-ring-offset-shadow:0 0 #0000;--tw-ring-shadow:0 0 #0000;--tw-shadow:0 0 #0000;--tw-shadow-colored:0 0 #0000;--tw-blur: ;--tw-brightness: ;--tw-contrast: ;--tw-grayscale: ;--tw-hue-rotate: ;--tw-invert: ;--tw-saturate: ;--tw-sepia: ;--tw-drop-shadow: ;--tw-backdrop-blur: ;--tw-backdrop-brightness: ;--tw-backdrop-contrast: ;--tw-backdrop-grayscale: ;--tw-backdrop-hue-rotate: ;--tw-backdrop-invert: ;--tw-backdrop-opacity: ;--tw-backdrop-saturate: ;--tw-backdrop-sepia: }::backdrop{--tw-border-spacing-x:0;--tw-border-spacing-y:0;--tw-translate-x:0;--tw-translate-y:0;--tw-rotate:0;--tw-skew-x:0;--tw-skew-y:0;--tw-scale-x:1;--tw-scale-y:1;--tw-pan-x: ;--tw-pan-y: ;--tw-pinch-zoom: ;--tw-scroll-snap-strictness:proximity;--tw-ordinal: ;--tw-slashed-zero: ;--tw-numeric-figure: ;--tw-numeric-spacing: ;--tw-numeric-fraction: ;--tw-ring-inset: ;--tw-ring-offset-width:0px;--tw-ring-offset-color:#fff;--tw-ring-color:rgba(59,130,246,.5);--tw-ring-offset-shadow:0 0 #0000;--tw-ring-shadow:0 0 #0000;--tw-shadow:0 0 #0000;--tw-shadow-colored:0 0 #0000;--tw-blur: ;--tw-brightness: ;--tw-contrast: ;--tw-grayscale: ;--tw-hue-rotate: ;--tw-invert: ;--tw-saturate: ;--tw-sepia: ;--tw-drop-shadow: ;--tw-backdrop-blur: ;--tw-backdrop-brightness: ;--tw-backdrop-contrast: ;--tw-backdrop-grayscale: ;--tw-backdrop-hue-rotate: ;--tw-backdrop-invert: ;--tw-backdrop-opacity: ;--tw-backdrop-saturate: ;--tw-backdrop-sepia: }.form-input,.form-multiselect,.form-select,.form-textarea{-webkit-appearance:none;-moz-appearance:none;appearance:none;background-color:#fff;border-color:#6b7280;border-width:1px;border-radius:0;padding:.5rem .75rem;font-size:1rem;line-height:1.5rem;--tw-shadow:0 0 #0000}.form-input:focus,.form-multiselect:focus,.form-select:focus,.form-textarea: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::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-day-field,.form-input::-webkit-datetime-edit-hour-field,.form-input::-webkit-datetime-edit-meridiem-field,.form-input::-webkit-datetime-edit-millisecond-field,.form-input::-webkit-datetime-edit-minute-field,.form-input::-webkit-datetime-edit-month-field,.form-input::-webkit-datetime-edit-second-field,.form-input::-webkit-datetime-edit-year-field{padding-top:0;padding-bottom:0}.sr-only{position:absolute;width:1px;height:1px;padding:0;margin:-1px;overflow:hidden;clip:rect(0,0,0,0);white-space:nowrap;border-width:0}.fixed{position:fixed}.absolute{position:absolute}.relative{position:relative}.sticky{position:-webkit-sticky;position:sticky}.top-0{top:0}.left-0{left:0}.bottom-0{bottom:0}.left-1{left:.25rem}.top-1{top:.25rem}.z-50{z-index:50}.z-40{z-index:40}.col-span-3{grid-column:span 3/span 3}.m-auto{margin:auto}.my-3{margin-top:.75rem;margin-bottom:.75rem}.mx-auto{margin-left:auto;margin-right:auto}.mx-1{margin-left:.25rem;margin-right:.25rem}.ml-3{margin-left:.75rem}.-ml-16{margin-left:-4rem}.-mt-2{margin-top:-.5rem}.mb-6{margin-bottom:1.5rem}.mb-2{margin-bottom:.5rem}.mr-2{margin-right:.5rem}.ml-auto{margin-left:auto}.mb-4{margin-bottom:1rem}.mr-8{margin-right:2rem}.ml-2{margin-left:.5rem}.mt-4{margin-top:1rem}.mt-1{margin-top:.25rem}.mb-8{margin-bottom:2rem}.mr-1{margin-right:.25rem}.block{display:block}.flex{display:flex}.grid{display:grid}.contents{display:contents}.hidden{display:none}.h-4{height:1rem}.h-5{height:1.25rem}.h-8{height:2rem}.h-12{height:3rem}.h-screen{height:100vh}.h-6{height:1.5rem}.h-10{height:2.5rem}.h-16{height:4rem}.h-full{height:100%}.h-48{height:12rem}.h-20{height:5rem}.h-2{height:.5rem}.h-64{height:16rem}.h-40{height:10rem}.max-h-16{max-height:4rem}.w-4{width:1rem}.w-5{width:1.25rem}.w-8{width:2rem}.w-screen{width:100vw}.w-12{width:3rem}.w-\[30rem\]{width:30rem}.w-full{width:100%}.w-48{width:12rem}.w-6{width:1.5rem}.w-auto{width:auto}.w-16{width:4rem}.w-20{width:5rem}.w-2{width:.5rem}.w-14{width:3.5rem}.min-w-max{min-width:-webkit-max-content;min-width:-moz-max-content;min-width:max-content}.flex-none{flex:none}.flex-1{flex:1 1 0%}.flex-grow,.grow{flex-grow:1}@-webkit-keyframes spin{to{transform:rotate(1turn)}}@keyframes spin{to{transform:rotate(1turn)}}.animate-spin{-webkit-animation:spin 1s linear infinite;animation:spin 1s linear infinite}@-webkit-keyframes wiggle{0%,to{transform:rotate(-6deg)}50%{transform:rotate(6deg)}}.animate-wiggle-short{-webkit-animation:wiggle 1s ease-in-out 10;animation:wiggle 1s ease-in-out 10}.cursor-pointer{cursor:pointer}.list-none{list-style-type:none}.grid-cols-3{grid-template-columns:repeat(3,minmax(0,1fr))}.flex-row{flex-direction:row}.flex-col{flex-direction:column}.flex-nowrap{flex-wrap:nowrap}.place-content-center{place-content:center}.place-items-center{place-items:center}.content-center{align-content:center}.items-center{align-items:center}.justify-end{justify-content:flex-end}.justify-center{justify-content:center}.gap-4{gap:1rem}.gap-1{gap:.25rem}.gap-8{gap:2rem}.gap-2{gap:.5rem}.divide-y>:not([hidden])~:not([hidden]){--tw-divide-y-reverse:0;border-top-width:calc(1px*(1 - var(--tw-divide-y-reverse)));border-bottom-width:calc(1px*var(--tw-divide-y-reverse))}.divide-neutral-600>:not([hidden])~:not([hidden]){--tw-divide-opacity:1;border-color:rgb(82 82 82/var(--tw-divide-opacity))}.place-self-end{place-self:end}.self-start{align-self:flex-start}.justify-self-end{justify-self:end}.overflow-hidden{overflow:hidden}.overflow-y-auto{overflow-y:auto}.overflow-x-hidden{overflow-x:hidden}.truncate{overflow:hidden;white-space:nowrap}.text-ellipsis,.truncate{text-overflow:ellipsis}.rounded-lg{border-radius:.5rem}.rounded{border-radius:.25rem}.rounded-md{border-radius:.375rem}.rounded-full{border-radius:9999px}.rounded-xl{border-radius:.75rem}.rounded-l-lg{border-top-left-radius:.5rem;border-bottom-left-radius:.5rem}.rounded-r-lg{border-top-right-radius:.5rem;border-bottom-right-radius:.5rem}.border{border-width:1px}.border-b-2{border-bottom-width:2px}.border-t{border-top-width:1px}.border-t-2{border-top-width:2px}.border-neutral-600{--tw-border-opacity:1;border-color:rgb(82 82 82/var(--tw-border-opacity))}.border-stone-900{--tw-border-opacity:1;border-color:rgb(28 25 23/var(--tw-border-opacity))}.border-green-500{--tw-border-opacity:1;border-color:rgb(34 197 94/var(--tw-border-opacity))}.border-transparent{border-color:transparent}.border-neutral-500{--tw-border-opacity:1;border-color:rgb(115 115 115/var(--tw-border-opacity))}.border-neutral-700{--tw-border-opacity:1;border-color:rgb(64 64 64/var(--tw-border-opacity))}.border-stone-800{--tw-border-opacity:1;border-color:rgb(41 37 36/var(--tw-border-opacity))}.bg-transparent{background-color:initial}.bg-cyan-700{--tw-bg-opacity:1;background-color:rgb(14 116 144/var(--tw-bg-opacity))}.bg-neutral-800{--tw-bg-opacity:1;background-color:rgb(38 38 38/var(--tw-bg-opacity))}.bg-stone-800{--tw-bg-opacity:1;background-color:rgb(41 37 36/var(--tw-bg-opacity))}.bg-cyan-900{--tw-bg-opacity:1;background-color:rgb(22 78 99/var(--tw-bg-opacity))}.bg-neutral-700{--tw-bg-opacity:1;background-color:rgb(64 64 64/var(--tw-bg-opacity))}.bg-neutral-900{--tw-bg-opacity:1;background-color:rgb(23 23 23/var(--tw-bg-opacity))}.bg-stone-700{--tw-bg-opacity:1;background-color:rgb(68 64 60/var(--tw-bg-opacity))}.bg-stone-900{--tw-bg-opacity:1;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))}.bg-lime-600{--tw-bg-opacity:1;background-color:rgb(101 163 13/var(--tw-bg-opacity))}.bg-lime-800{--tw-bg-opacity:1;background-color:rgb(63 98 18/var(--tw-bg-opacity))}.bg-neutral-400{--tw-bg-opacity:1;background-color:rgb(163 163 163/var(--tw-bg-opacity))}.bg-white{--tw-bg-opacity:1;background-color:rgb(255 255 255/var(--tw-bg-opacity))}.stroke-slate-400{stroke:#94a3b8}.p-2{padding:.5rem}.p-3{padding:.75rem}.p-4{padding:1rem}.p-16{padding:4rem}.p-0{padding:0}.p-0\.5{padding:.125rem}.p-1\.5{padding:.375rem}.p-1{padding:.25rem}.px-8{padding-left:2rem;padding-right:2rem}.px-4{padding-left:1rem;padding-right:1rem}.py-2{padding-top:.5rem;padding-bottom:.5rem}.py-4{padding-top:1rem;padding-bottom:1rem}.py-1{padding-top:.25rem;padding-bottom:.25rem}.px-2{padding-left:.5rem;padding-right:.5rem}.px-0\.5{padding-left:.125rem;padding-right:.125rem}.px-0{padding-left:0;padding-right:0}.px-6{padding-left:1.5rem;padding-right:1.5rem}.py-12{padding-top:3rem;padding-bottom:3rem}.pl-1{padding-left:.25rem}.pt-4{padding-top:1rem}.pb-4{padding-bottom:1rem}.pl-6{padding-left:1.5rem}.pr-2{padding-right:.5rem}.pl-4{padding-left:1rem}.pr-4{padding-right:1rem}.pb-8{padding-bottom:2rem}.pb-2{padding-bottom:.5rem}.pr-0{padding-right:0}.pt-2{padding-top:.5rem}.pt-8{padding-top:2rem}.pb-16{padding-bottom:4rem}.text-center{text-align:center}.text-right{text-align:right}.align-middle{vertical-align:middle}.text-sm{font-size:.875rem;line-height:1.25rem}.text-4xl{font-size:2.25rem;line-height:2.5rem}.text-xl{font-size:1.25rem;line-height:1.75rem}.text-xs{font-size:.75rem;line-height:1rem}.text-lg{font-size:1.125rem;line-height:1.75rem}.text-2xl{font-size:1.5rem;line-height:2rem}.text-\[8px\]{font-size:8px}.text-5xl{font-size:3rem;line-height:1}.font-medium{font-weight:500}.font-bold{font-weight:700}.uppercase{text-transform:uppercase}.leading-relaxed{line-height:1.625}.text-white{--tw-text-opacity:1;color:rgb(255 255 255/var(--tw-text-opacity))}.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-cyan-500{--tw-text-opacity:1;color:rgb(6 182 212/var(--tw-text-opacity))}.text-neutral-400{--tw-text-opacity:1;color:rgb(163 163 163/var(--tw-text-opacity))}.text-gray-500{--tw-text-opacity:1;color:rgb(107 114 128/var(--tw-text-opacity))}.text-red-400{--tw-text-opacity:1;color:rgb(248 113 113/var(--tw-text-opacity))}.text-neutral-300{--tw-text-opacity:1;color:rgb(212 212 212/var(--tw-text-opacity))}.text-neutral-500{--tw-text-opacity:1;color:rgb(115 115 115/var(--tw-text-opacity))}.text-cyan-400{--tw-text-opacity:1;color:rgb(34 211 238/var(--tw-text-opacity))}.text-green-400{--tw-text-opacity:1;color:rgb(74 222 128/var(--tw-text-opacity))}.text-cyan-600{--tw-text-opacity:1;color:rgb(8 145 178/var(--tw-text-opacity))}.text-black{--tw-text-opacity:1;color:rgb(0 0 0/var(--tw-text-opacity))}.text-stone-500{--tw-text-opacity:1;color:rgb(120 113 108/var(--tw-text-opacity))}.text-yellow-500{--tw-text-opacity:1;color:rgb(234 179 8/var(--tw-text-opacity))}.text-red-500{--tw-text-opacity:1;color:rgb(239 68 68/var(--tw-text-opacity))}.caret-white{caret-color:#fff}.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)}.transition{transition-property:color,background-color,border-color,fill,stroke,opacity,box-shadow,transform,filter,-webkit-text-decoration-color,-webkit-backdrop-filter;transition-property:color,background-color,border-color,text-decoration-color,fill,stroke,opacity,box-shadow,transform,filter,backdrop-filter;transition-property:color,background-color,border-color,text-decoration-color,fill,stroke,opacity,box-shadow,transform,filter,backdrop-filter,-webkit-text-decoration-color,-webkit-backdrop-filter;transition-timing-function:cubic-bezier(.4,0,.2,1);transition-duration:.15s}input:checked~.dot{transform:translateX(100%);background-color:#48bb78}*{scrollbar-width:thin;scrollbar-color:#404040 #171717}::-webkit-scrollbar{width:10px}::-webkit-scrollbar-track{background:#171717}::-webkit-scrollbar-thumb{background:#404040;border-radius:100vh;border:2px solid #171717}::-webkit-scrollbar-thumb:hover{background:#164e63}@keyframes wiggle{0%,to{transform:rotate(-6deg)}50%{transform:rotate(6deg)}}.hover\:animate-wiggle:hover{-webkit-animation:wiggle 1s ease-in-out infinite;animation:wiggle 1s ease-in-out infinite}.hover\:border-green-500:hover{--tw-border-opacity:1;border-color:rgb(34 197 94/var(--tw-border-opacity))}.hover\:bg-neutral-600:hover{--tw-bg-opacity:1;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\:bg-cyan-900:hover{--tw-bg-opacity:1;background-color:rgb(22 78 99/var(--tw-bg-opacity))}.hover\:bg-blue-900:hover{--tw-bg-opacity:1;background-color:rgb(30 58 138/var(--tw-bg-opacity))}.hover\:bg-amber-700:hover{--tw-bg-opacity:1;background-color:rgb(180 83 9/var(--tw-bg-opacity))}.hover\:bg-indigo-900:hover{--tw-bg-opacity:1;background-color:rgb(49 46 129/var(--tw-bg-opacity))}.hover\:bg-green-900:hover{--tw-bg-opacity:1;background-color:rgb(20 83 45/var(--tw-bg-opacity))}.hover\:text-red-600:hover{--tw-text-opacity:1;color:rgb(220 38 38/var(--tw-text-opacity))}.hover\:text-cyan-500:hover{--tw-text-opacity:1;color:rgb(6 182 212/var(--tw-text-opacity))}.hover\:text-red-500:hover{--tw-text-opacity:1;color:rgb(239 68 68/var(--tw-text-opacity))}.hover\:text-white:hover{--tw-text-opacity:1;color:rgb(255 255 255/var(--tw-text-opacity))}.focus\:outline-none:focus{outline:2px solid transparent;outline-offset:2px}.active\:bg-neutral-700:active{--tw-bg-opacity:1;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}.group:hover .group-hover\:fill-red-400{fill:#f87171}.group:hover .group-hover\:stroke-white{stroke:#fff}.group:hover .group-hover\:text-neutral-400{--tw-text-opacity:1;color:rgb(163 163 163/var(--tw-text-opacity))} \ No newline at end of file diff --git a/crates/client/src/pages/admin.rs b/crates/client/src/pages/admin.rs index 84f5bb7fd..c4706ec55 100644 --- a/crates/client/src/pages/admin.rs +++ b/crates/client/src/pages/admin.rs @@ -65,7 +65,7 @@ pub struct SettingsPageProps { #[function_component(SettingsPage)] pub fn settings_page(props: &SettingsPageProps) -> Html { - let history = use_history().unwrap(); + let history = use_history().expect("History not available in this browser"); spawn_local(async move { let cb = Closure::wrap(Box::new(move |payload: JsValue| { diff --git a/crates/client/src/pages/crawl_stats.rs b/crates/client/src/pages/crawl_stats.rs index f39b19ff7..6bb245cba 100644 --- a/crates/client/src/pages/crawl_stats.rs +++ b/crates/client/src/pages/crawl_stats.rs @@ -15,10 +15,12 @@ fn fetch_crawl_stats( spawn_local(async move { match invoke(ClientInvoke::GetCrawlStats.as_ref(), JsValue::NULL).await { Ok(results) => { - let results: CrawlStats = serde_wasm_bindgen::from_value(results).unwrap(); - let mut sorted = results.by_domain; - sorted.sort_by(|(_, a), (_, b)| b.num_completed.cmp(&a.num_completed)); - stats_handle.set(sorted); + if let Ok(results) = serde_wasm_bindgen::from_value::(results) { + let mut sorted = results.by_domain; + sorted.sort_by(|(_, a), (_, b)| b.num_completed.cmp(&a.num_completed)); + stats_handle.set(sorted); + } + request_finished.set(true); } Err(e) => { diff --git a/crates/client/src/pages/lens_manager.rs b/crates/client/src/pages/lens_manager.rs index 39d1db495..3403055c7 100644 --- a/crates/client/src/pages/lens_manager.rs +++ b/crates/client/src/pages/lens_manager.rs @@ -123,7 +123,7 @@ pub fn lens_component(props: &LensProps) -> Html { } } else { - html! { } + html! { } }; let view_link = if result.html_url.is_some() { diff --git a/crates/client/src/pages/search.rs b/crates/client/src/pages/search.rs index ba81911d8..59759034b 100644 --- a/crates/client/src/pages/search.rs +++ b/crates/client/src/pages/search.rs @@ -33,6 +33,7 @@ pub enum ResultDisplay { #[derive(Clone, Debug)] pub enum Msg { + ClearFilters, ClearQuery, ClearResults, Focus, @@ -109,7 +110,7 @@ impl SearchPage { fn request_resize(&self) { if let Some(node) = self.search_wrapper_ref.cast::() { spawn_local(async move { - resize_window(node.offset_height() as f64).await.unwrap(); + let _ = resize_window(node.offset_height() as f64).await; }); } } @@ -139,7 +140,11 @@ impl Component for SearchPage { let link = link.clone(); spawn_local(async move { let cb = Closure::wrap(Box::new(move |_| { - link.send_message(Msg::ClearQuery); + link.send_message_batch(vec![ + Msg::ClearFilters, + Msg::ClearResults, + Msg::ClearQuery, + ]); }) as Box); let _ = listen(ClientEvent::ClearSearch.as_ref(), &cb).await; @@ -176,10 +181,14 @@ impl Component for SearchPage { fn update(&mut self, ctx: &Context, msg: Self::Message) -> bool { let link = ctx.link(); match msg { + Msg::ClearFilters => { + self.lens.clear(); + true + } Msg::ClearResults => { self.selected_idx = 0; - self.docs_results = Vec::new(); - self.lens_results = Vec::new(); + self.docs_results.clear(); + self.lens_results.clear(); self.search_meta = None; self.result_display = ResultDisplay::None; self.request_resize(); @@ -187,8 +196,8 @@ impl Component for SearchPage { } Msg::ClearQuery => { self.selected_idx = 0; - self.docs_results = Vec::new(); - self.lens_results = Vec::new(); + self.docs_results.clear(); + self.lens_results.clear(); self.search_meta = None; self.query = "".to_string(); if let Some(el) = self.search_input_ref.cast::() { @@ -199,14 +208,14 @@ impl Component for SearchPage { true } Msg::Focus => { - if let Some(el) = self.search_input_ref.cast::() { + if let Some(el) = self.search_input_ref.cast::() { let _ = el.focus(); } self.request_resize(); true } Msg::HandleError(msg) => { - let window = window().unwrap(); + let window = window().expect("Unable to get window"); let _ = window.alert_with_message(&msg); false } @@ -306,9 +315,12 @@ impl Component for SearchPage { let query = self.query.clone(); link.send_future(async move { - match search_docs(serde_wasm_bindgen::to_value(&lenses).unwrap(), query).await { - Ok(results) => match serde_wasm_bindgen::from_value(results) { - Ok(deser) => Msg::UpdateDocsResults(deser), + match serde_wasm_bindgen::to_value(&lenses) { + Ok(lenses) => match search_docs(lenses, query).await { + Ok(results) => match serde_wasm_bindgen::from_value(results) { + Ok(deser) => Msg::UpdateDocsResults(deser), + Err(e) => Msg::HandleError(format!("Error: {:?}", e)), + }, Err(e) => Msg::HandleError(format!("Error: {:?}", e)), }, Err(e) => Msg::HandleError(format!("Error: {:?}", e)), @@ -436,17 +448,21 @@ impl Component for SearchPage { }; html! { -
+
diff --git a/crates/entities/src/models/crawl_queue.rs b/crates/entities/src/models/crawl_queue.rs index 3b828bdc9..f6a6bb8ef 100644 --- a/crates/entities/src/models/crawl_queue.rs +++ b/crates/entities/src/models/crawl_queue.rs @@ -135,7 +135,7 @@ pub async fn queue_stats( Ok(res) } -pub async fn reset_processing(db: &DatabaseConnection) { +pub async fn reset_processing(db: &DatabaseConnection) -> anyhow::Result<()> { Entity::update_many() .col_expr( Column::Status, @@ -145,8 +145,9 @@ pub async fn reset_processing(db: &DatabaseConnection) { ) .filter(Column::Status.eq(CrawlStatus::Processing)) .exec(db) - .await - .unwrap(); + .await?; + + Ok(()) } #[derive(Debug, FromQueryResult)] @@ -431,15 +432,18 @@ pub async fn enqueue_all( let mut result = None; if !is_indexed.contains(&url) { if let Ok(parsed) = Url::parse(&url) { - if let Some(domain) = parsed.host_str() { - result = Some(ActiveModel { - domain: Set(domain.to_string()), - crawl_type: Set(overrides.crawl_type.clone()), - url: Set(url.to_string()), - pipeline: Set(pipeline.clone()), - ..Default::default() - }); - } + let domain = match parsed.scheme() { + "file" => "localhost", + _ => parsed.host_str().expect("Invalid URL host"), + }; + + result = Some(ActiveModel { + domain: Set(domain.to_string()), + crawl_type: Set(overrides.crawl_type.clone()), + url: Set(url.to_string()), + pipeline: Set(pipeline.clone()), + ..Default::default() + }); } } result diff --git a/crates/entities/src/models/fetch_history.rs b/crates/entities/src/models/fetch_history.rs index 225773944..e2c0383d7 100644 --- a/crates/entities/src/models/fetch_history.rs +++ b/crates/entities/src/models/fetch_history.rs @@ -68,7 +68,7 @@ pub async fn find_by_url( url: &Url, ) -> anyhow::Result, sea_orm::DbErr> { Entity::find() - .filter(Column::Domain.eq(url.host_str().unwrap().to_string())) + .filter(Column::Domain.eq(url.host_str().unwrap_or_default().to_string())) .filter(Column::Path.eq(url.path())) .one(db) .await diff --git a/crates/entities/src/models/mod.rs b/crates/entities/src/models/mod.rs index 97b4b311b..356587e86 100644 --- a/crates/entities/src/models/mod.rs +++ b/crates/entities/src/models/mod.rs @@ -22,7 +22,11 @@ pub async fn create_connection( } else { format!( "sqlite://{}?mode=rwc", - config.data_dir().join("db.sqlite").to_str().unwrap() + config + .data_dir() + .join("db.sqlite") + .to_str() + .expect("Unable to create db") ) }; diff --git a/crates/migrations/Cargo.toml b/crates/migrations/Cargo.toml index 48537e533..2c04df1b6 100644 --- a/crates/migrations/Cargo.toml +++ b/crates/migrations/Cargo.toml @@ -14,4 +14,5 @@ entities = { path = "../entities" } shared = { path = "../shared" } tantivy = "0.18" rayon = "1.5" -tar = "0.4" \ No newline at end of file +tar = "0.4" +url = "2.3" \ No newline at end of file diff --git a/crates/migrations/src/lib.rs b/crates/migrations/src/lib.rs index a5fa1d439..e7cc1dfff 100644 --- a/crates/migrations/src/lib.rs +++ b/crates/migrations/src/lib.rs @@ -14,6 +14,9 @@ mod m20221031_000001_add_error_column_to_crawl_queue; mod m20221101_000001_add_open_url_col; mod m20221107_000001_recreate_connection_table; mod m20221109_add_tags_table; +mod m20221115_000001_local_file_pathfix; +mod m20221116_000001_add_connection_constraint; + mod utils; pub struct Migrator; @@ -33,6 +36,8 @@ impl MigratorTrait for Migrator { Box::new(m20221101_000001_add_open_url_col::Migration), Box::new(m20221107_000001_recreate_connection_table::Migration), Box::new(m20221109_add_tags_table::Migration), + Box::new(m20221115_000001_local_file_pathfix::Migration), + Box::new(m20221116_000001_add_connection_constraint::Migration), ] } } diff --git a/crates/migrations/src/m20221115_000001_local_file_pathfix.rs b/crates/migrations/src/m20221115_000001_local_file_pathfix.rs new file mode 100644 index 000000000..e74368701 --- /dev/null +++ b/crates/migrations/src/m20221115_000001_local_file_pathfix.rs @@ -0,0 +1,183 @@ +use entities::schema::{DocFields, SearchDocument}; +use sea_orm_migration::prelude::*; + +use entities::models::{crawl_queue, indexed_document}; +use entities::sea_orm::{ActiveModelTrait, ColumnTrait, EntityTrait, QueryFilter, Set}; +use shared::config::Config; +use tantivy::collector::TopDocs; +use tantivy::directory::MmapDirectory; +use tantivy::query::TermQuery; +use tantivy::schema::IndexRecordOption; +use tantivy::{Document, Index, IndexReader, IndexWriter, ReloadPolicy, Term}; +use url::Url; + +pub struct Migration; + +impl Migration { + fn fix_url(&self, url: &str) -> Option { + if let Ok(mut parsed) = Url::parse(url) { + // Switch host to localhost so that we can use `to_file_path` + let _ = parsed.set_host(Some("localhost")); + + // If we're on Windows, fix the path bug we found where the `ignore` + // overescapes windows paths. + if cfg!(target_os = "windows") { + if let Ok(path_str) = parsed.to_file_path() { + let path_str = path_str.display().to_string(); + parsed.set_path(&path_str.replace("\\\\", "\\")); + } + } + + return Some(parsed.to_string()); + } + + None + } + + fn open_index(&self) -> (IndexWriter, IndexReader) { + let config = Config::new(); + let schema = DocFields::as_schema(); + + let dir = MmapDirectory::open(config.index_dir()).expect("Unable to create MmapDirectory"); + let index = Index::open_or_create(dir, schema).expect("Unable to open / create directory"); + + let writer = index + .writer(50_000_000) + .expect("Unable to create index_writer"); + + // For a search server you will typically create on reader for the entire + // lifetime of your program. + let reader = index + .reader_builder() + .reload_policy(ReloadPolicy::OnCommit) + .try_into() + .expect("Unable to create reader"); + + (writer, reader) + } + + fn update_index( + &self, + writer: &IndexWriter, + reader: &IndexReader, + doc_id: &str, + updated_url: &str, + ) { + let fields = DocFields::as_fields(); + + let searcher = reader.searcher(); + let query = TermQuery::new( + Term::from_field_text(fields.id, doc_id), + IndexRecordOption::Basic, + ); + + let res = searcher + .search(&query, &TopDocs::with_limit(1)) + .map_or(Vec::new(), |x| x) + .pop(); + + let doc = if let Some((_, doc_address)) = res { + searcher.doc(doc_address).ok() + } else { + None + }; + + // Doc exists! Lets remove it and update it with the new URL + if let Some(doc) = doc { + // Remove the old one + writer.delete_term(Term::from_field_text(fields.id, doc_id)); + // Re-add the document w/ the updated domain & url + let mut new_doc = Document::default(); + new_doc.add_text(fields.id, doc_id); + new_doc.add_text(fields.domain, "localhost"); + new_doc.add_text(fields.url, updated_url); + // Everything else stays the same + new_doc.add_text( + fields.content, + doc.get_first(fields.content).unwrap().as_text().unwrap(), + ); + new_doc.add_text( + fields.description, + doc.get_first(fields.description) + .unwrap() + .as_text() + .unwrap(), + ); + new_doc.add_text( + fields.title, + doc.get_first(fields.title).unwrap().as_text().unwrap(), + ); + + let _ = writer.add_document(new_doc); + } + } +} + +impl MigrationName for Migration { + fn name(&self) -> &str { + "m20221115_000001_local_file_pathfix" + } +} + +#[async_trait::async_trait] +impl MigrationTrait for Migration { + async fn up(&self, manager: &SchemaManager) -> Result<(), DbErr> { + let (mut iwriter, ireader) = self.open_index(); + let db = manager.get_connection(); + + println!("Updating crawl_queue"); + let queued = crawl_queue::Entity::find() + .filter(crawl_queue::Column::Url.starts_with("file://")) + .all(db) + .await + .expect("Unable to query crawl_queue table."); + + for doc in &queued { + if let Some(updated_url) = self.fix_url(&doc.url) { + let mut update: crawl_queue::ActiveModel = doc.to_owned().into(); + update.domain = Set("localhost".to_string()); + update.url = Set(updated_url); + let _ = update.save(db).await; + } + } + + let docs = indexed_document::Entity::find() + .filter(indexed_document::Column::Url.starts_with("file://")) + .all(db) + .await + .expect("Unable to query indexed_document table."); + + // No docs yet, nothing to migrate. + if docs.is_empty() { + return Ok(()); + } + + println!("Updating index"); + for doc in &docs { + if let Some(updated_url) = self.fix_url(&doc.url) { + // Update the document in the index + self.update_index(&iwriter, &ireader, &doc.doc_id, &updated_url); + + // Update document in the db + let mut update: indexed_document::ActiveModel = doc.to_owned().into(); + update.domain = Set("localhost".to_string()); + update.url = Set(updated_url.clone()); + update.open_url = Set(Some(updated_url.clone())); + let _ = update.save(db).await; + } + } + + if let Err(err) = iwriter.commit() { + return Err(DbErr::Custom(format!( + "Unable to save changes to index: {}", + err + ))); + } + + Ok(()) + } + + async fn down(&self, _: &SchemaManager) -> Result<(), DbErr> { + Ok(()) + } +} diff --git a/crates/migrations/src/m20221116_000001_add_connection_constraint.rs b/crates/migrations/src/m20221116_000001_add_connection_constraint.rs new file mode 100644 index 000000000..58929f06d --- /dev/null +++ b/crates/migrations/src/m20221116_000001_add_connection_constraint.rs @@ -0,0 +1,32 @@ +use crate::sea_orm::Statement; +use sea_orm_migration::prelude::*; +use sea_orm_migration::sea_orm::ConnectionTrait; + +pub struct Migration; + +impl MigrationName for Migration { + fn name(&self) -> &str { + "m20221116_000001_add_connection_constraint" + } +} + +#[async_trait::async_trait] +impl MigrationTrait for Migration { + async fn up(&self, manager: &SchemaManager) -> Result<(), DbErr> { + // Create index on (api_id, account). Should only every be one instance of a + // an account per service. + manager + .get_connection() + .execute(Statement::from_string( + manager.get_database_backend(), + "CREATE UNIQUE INDEX `idx-connections-api-id-account` ON `connections` (`api_id`, `account`);" + .to_string(), + )) + .await?; + Ok(()) + } + + async fn down(&self, _: &SchemaManager) -> Result<(), DbErr> { + Ok(()) + } +} diff --git a/crates/shared/src/config.rs b/crates/shared/src/config.rs index f320cf40b..76da88f03 100644 --- a/crates/shared/src/config.rs +++ b/crates/shared/src/config.rs @@ -218,8 +218,8 @@ impl Config { match prefs_path.exists() { true => { - let mut settings: UserSettings = - ron::from_str(&fs::read_to_string(prefs_path).unwrap())?; + let contents = &fs::read_to_string(prefs_path).unwrap_or_default(); + let mut settings: UserSettings = ron::from_str(contents)?; settings.constraint_limits(); Ok(settings) } @@ -228,7 +228,8 @@ impl Config { // Write out default settings fs::write( prefs_path, - ron::ser::to_string_pretty(&settings, Default::default()).unwrap(), + ron::ser::to_string_pretty(&settings, Default::default()) + .expect("Unable to serialize settings."), ) .expect("Unable to save user preferences file."); @@ -278,7 +279,8 @@ impl Config { } pub fn default_data_dir() -> PathBuf { - let proj_dirs = ProjectDirs::from("com", "athlabs", &Config::app_identifier()).unwrap(); + let proj_dirs = ProjectDirs::from("com", "athlabs", &Config::app_identifier()) + .expect("Unable to find a default data directory"); proj_dirs.data_dir().to_path_buf() } @@ -299,7 +301,8 @@ impl Config { } pub fn prefs_dir() -> PathBuf { - let proj_dirs = ProjectDirs::from("com", "athlabs", &Config::app_identifier()).unwrap(); + let proj_dirs = ProjectDirs::from("com", "athlabs", &Config::app_identifier()) + .expect("Unable to find a suitable settings directory"); proj_dirs.preference_dir().to_path_buf() } diff --git a/crates/spyglass-plugin/src/utils.rs b/crates/spyglass-plugin/src/utils.rs index 33cdcea87..9cd450cd9 100644 --- a/crates/spyglass-plugin/src/utils.rs +++ b/crates/spyglass-plugin/src/utils.rs @@ -3,20 +3,51 @@ use url::Url; // Create a file URI pub fn path_to_uri(path: PathBuf) -> String { - let path_str = path.to_str().expect("Unable to convert path to string"); - + let path_str = path.display().to_string(); // Eventually this will be away to keep track of multiple devices and searching across - // them. Might make sense to generate a UUID and assign to this computer(?) hostname - // can be changed by the user. - let host = if let Ok(hname) = std::env::var("HOST_NAME") { - hname - } else { - "home.local".into() - }; + // them. + let host = "localhost"; let mut new_url = Url::parse("file://").expect("Base URI"); - let _ = new_url.set_host(Some(&host)); + let _ = new_url.set_host(Some(host)); // Fixes issues handling windows drive letters - new_url.set_path(&path_str.replace(':', "%3A")); + let path_str = path_str.replace(':', "%3A"); + // Fixes an issue where DirEntry adds too many escapes. + let path_str = path_str.replace("\\\\", "\\"); + new_url.set_path(&path_str); new_url.to_string() } + +#[cfg(test)] +mod test { + use super::path_to_uri; + use std::path::Path; + use url::Url; + + #[test] + fn test_path_to_uri() { + #[cfg(target_os = "windows")] + let test_folder = Path::new("C:\\tmp\\path_to_uri"); + + #[cfg(not(target_os = "windows"))] + let test_folder = Path::new("/tmp/path_to_uri"); + + std::fs::create_dir_all(test_folder).expect("Unable to create test dir"); + + let test_path = test_folder.join("test.txt"); + let uri = path_to_uri(test_path.to_path_buf()); + + #[cfg(target_os = "windows")] + assert_eq!(uri, "file://localhost/C%3A/tmp/path_to_uri/test.txt"); + #[cfg(not(target_os = "windows"))] + assert_eq!(uri, "file://localhost/tmp/path_to_uri/test.txt"); + + let url = Url::parse(&uri).unwrap(); + let file_path = url.to_file_path().unwrap(); + assert_eq!(file_path, test_path); + + if test_folder.exists() { + std::fs::remove_dir_all(test_folder).expect("Unable to clean up test folder"); + } + } +} diff --git a/crates/spyglass/Cargo.toml b/crates/spyglass/Cargo.toml index e471a2793..76d322657 100644 --- a/crates/spyglass/Cargo.toml +++ b/crates/spyglass/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "spyglass" -version = "22.11.5" +version = "22.11.6" edition = "2021" [dependencies] @@ -34,6 +34,7 @@ reqwest = "0.11" ron = "0.8" rusqlite = { version = "*", features = ["bundled"] } sentry = "0.28.0" +sentry-tracing = "0.28.0" serde = { version = "1.0", features = ["derive"] } sha2 = "0.10" shared = { path = "../shared" } diff --git a/crates/spyglass/src/api/route.rs b/crates/spyglass/src/api/route.rs index 80ab240e1..5643afb06 100644 --- a/crates/spyglass/src/api/route.rs +++ b/crates/spyglass/src/api/route.rs @@ -105,13 +105,20 @@ pub async fn authorize_connection(state: AppState, api_id: String) -> Result<(), auth.scopes, ); let res = new_conn.insert(&state.db).await; - log::debug!("saved connection: {:?}", res); - let _ = state - .schedule_work(ManagerCommand::Collect(CollectTask::ConnectionSync { - api_id, - account: user.email, - })) - .await; + match res { + Ok(_) => { + log::debug!("saved connection {} for {}", user.email.clone(), api_id); + let _ = state + .schedule_work(ManagerCommand::Collect( + CollectTask::ConnectionSync { + api_id, + account: user.email, + }, + )) + .await; + } + Err(err) => log::error!("Unable to save connection: {}", err.to_string()), + } } Err(err) => log::error!("unable to exchange token: {}", err), } @@ -380,8 +387,7 @@ pub async fn search( .collect::>(); let docs = - Searcher::search_with_lens(state.db.clone(), &applied, &index.reader, &search_req.query) - .await; + Searcher::search_with_lens(state.db.clone(), &applied, index, &search_req.query).await; let mut results: Vec = Vec::new(); for (score, doc_addr) in docs { diff --git a/crates/spyglass/src/connection/mod.rs b/crates/spyglass/src/connection/mod.rs index f5c3cc639..10bb53f15 100644 --- a/crates/spyglass/src/connection/mod.rs +++ b/crates/spyglass/src/connection/mod.rs @@ -32,10 +32,14 @@ pub async fn load_connection( ) -> Result> { match api_id { "calendar.google.com" => Ok(Box::new( - gcal::GCalConnection::new(state, account).await.unwrap(), + gcal::GCalConnection::new(state, account) + .await + .expect("Unable to create gcal connection"), )), "drive.google.com" => Ok(Box::new( - gdrive::DriveConnection::new(state, account).await.unwrap(), + gdrive::DriveConnection::new(state, account) + .await + .expect("Unable to create gdrive connection"), )), _ => Err(anyhow::anyhow!("Not suppported connection")), } diff --git a/crates/spyglass/src/crawler/mod.rs b/crates/spyglass/src/crawler/mod.rs index d99dfa985..6b1656b0d 100644 --- a/crates/spyglass/src/crawler/mod.rs +++ b/crates/spyglass/src/crawler/mod.rs @@ -12,7 +12,6 @@ use url::{Host, Url}; use entities::models::{crawl_queue, fetch_history, tag}; use entities::sea_orm::prelude::*; -use shared::url_to_file_path; use crate::connection::load_connection; use crate::crawler::bootstrap::create_archive_url; @@ -340,24 +339,9 @@ impl Crawler { url: &Url, ) -> Result { // Attempt to convert from the URL to a file path - #[allow(unused_assignments)] - let mut url_path = url - .to_file_path() - .map(|p| p.display().to_string()) - .unwrap_or_else(|_| url.path().to_string()); + let file_path = url.to_file_path().expect("Invalid file URL"); - #[cfg(not(target_os = "windows"))] - { - url_path = url_to_file_path(&url_path, false); - } - - // Fixes issues handling Windows drive paths - #[cfg(target_os = "windows")] - { - url_path = url_to_file_path(url.path(), true); - } - - let path = Path::new(&url_path); + let path = Path::new(&file_path); // Is this a file and does this exist? if !path.exists() || !path.is_file() { return Err(CrawlError::NotFound); @@ -498,10 +482,11 @@ mod test { use entities::models::{crawl_queue, resource_rule}; use entities::sea_orm::{ActiveModelTrait, Set}; use entities::test::setup_test_db; + use spyglass_plugin::utils::path_to_uri; use crate::crawler::{determine_canonical, normalize_href, Crawler}; use crate::state::AppState; - + use std::path::Path; use url::Url; #[tokio::test] @@ -683,4 +668,37 @@ mod test { let res = determine_canonical(&a, &b); assert_eq!(res, "https://en.wikipedia.org/"); } + + #[tokio::test] + async fn test_file_fetch() { + let crawler = Crawler::new(); + + let db = setup_test_db().await; + let state = AppState::builder().with_db(db).build(); + + #[cfg(target_os = "windows")] + let test_folder = Path::new("C:\\tmp\\path_to_uri"); + #[cfg(not(target_os = "windows"))] + let test_folder = Path::new("/tmp/path_to_uri"); + + std::fs::create_dir_all(test_folder).expect("Unable to create test dir"); + + let test_path = test_folder.join("test.txt"); + std::fs::write(test_path.clone(), "test_content").expect("Unable to write test file"); + + let uri = path_to_uri(test_path.to_path_buf()); + let url = Url::parse(&uri).unwrap(); + + let query = crawl_queue::ActiveModel { + domain: Set("localhost".to_string()), + url: Set(url.to_string()), + crawl_type: Set(crawl_queue::CrawlType::Bootstrap), + ..Default::default() + }; + let model = query.insert(&state.db).await.unwrap(); + + // Add resource rule to stop the crawl above + let res = crawler.fetch_by_job(&state, model.id, true).await; + assert!(res.is_ok()); + } } diff --git a/crates/spyglass/src/main.rs b/crates/spyglass/src/main.rs index fdabf6302..b555282f4 100644 --- a/crates/spyglass/src/main.rs +++ b/crates/spyglass/src/main.rs @@ -38,6 +38,7 @@ fn main() -> Result<(), Box> { "https://5c1196909a4e4e5689406705be13aad3@o1334159.ingest.sentry.io/6600345", sentry::ClientOptions { release: sentry::release_name!(), + traces_sample_rate: 0.1, ..Default::default() }, ))) @@ -65,7 +66,8 @@ fn main() -> Result<(), Box> { .add_directive("docx=WARN".parse().expect("Invalid EnvFilter")), ) .with(fmt::Layer::new().with_writer(io::stdout)) - .with(fmt::Layer::new().with_ansi(false).with_writer(non_blocking)); + .with(fmt::Layer::new().with_ansi(false).with_writer(non_blocking)) + .with(sentry_tracing::layer()); tracing::subscriber::set_global_default(subscriber).expect("Unable to set a global subscriber"); LogTracer::init()?; @@ -112,7 +114,7 @@ fn main() -> Result<(), Box> { async fn start_backend(state: &mut AppState, config: &Config) { // Initialize crawl_queue, requeue all in-flight tasks. - crawl_queue::reset_processing(&state.db).await; + let _ = crawl_queue::reset_processing(&state.db).await; if let Err(e) = lens::reset(&state.db).await { log::error!("Unable to reset lenses: {}", e); } diff --git a/crates/spyglass/src/plugin/exports.rs b/crates/spyglass/src/plugin/exports.rs index 23d20aa31..24cd7c829 100644 --- a/crates/spyglass/src/plugin/exports.rs +++ b/crates/spyglass/src/plugin/exports.rs @@ -211,7 +211,9 @@ async fn handle_walk_and_enqueue( path: PathBuf, supported_exts: &HashSet, ) -> WalkStats { - let walker = WalkBuilder::new(path).standard_filters(true).build(); + let walker = WalkBuilder::new(path.clone()) + .standard_filters(true) + .build(); let enqueue_settings = EnqueueSettings { force_allow: true, ..Default::default() @@ -231,7 +233,7 @@ async fn handle_walk_and_enqueue( if let Some(ext) = ext { if supported_exts.contains(ext) { - to_enqueue.push(path_to_uri(entry.path().to_path_buf())); + to_enqueue.push(path_to_uri(entry.into_path())); stats.files += 1; } else { stats.skipped += 1; diff --git a/crates/spyglass/src/search/mod.rs b/crates/spyglass/src/search/mod.rs index 5efa9765c..5ce76476c 100644 --- a/crates/spyglass/src/search/mod.rs +++ b/crates/spyglass/src/search/mod.rs @@ -184,15 +184,17 @@ impl Searcher { pub async fn search_with_lens( _db: DatabaseConnection, applied_lenses: &Vec, - reader: &IndexReader, + searcher: &Searcher, query_string: &str, ) -> Vec { let start_timer = Instant::now(); + let index = &searcher.index; + let reader = &searcher.reader; let fields = DocFields::as_fields(); let searcher = reader.searcher(); - - let query = build_query(fields.clone(), query_string); + let tokenizers = index.tokenizers().clone(); + let query = build_query(index.schema(), tokenizers, fields.clone(), query_string); let mut allowed = Vec::new(); let mut skipped = Vec::new(); @@ -379,7 +381,7 @@ mod test { _build_test_index(&mut searcher); let query = "salinas"; - let results = Searcher::search_with_lens(db, &applied_lens, &searcher.reader, query).await; + let results = Searcher::search_with_lens(db, &applied_lens, &searcher, query).await; assert_eq!(results.len(), 1); } @@ -404,7 +406,7 @@ mod test { _build_test_index(&mut searcher); let query = "salinas"; - let results = Searcher::search_with_lens(db, &applied_lens, &searcher.reader, query).await; + let results = Searcher::search_with_lens(db, &applied_lens, &searcher, query).await; assert_eq!(results.len(), 1); } @@ -430,7 +432,7 @@ mod test { _build_test_index(&mut searcher); let query = "salinas"; - let results = Searcher::search_with_lens(db, &applied_lens, &searcher.reader, query).await; + let results = Searcher::search_with_lens(db, &applied_lens, &searcher, query).await; assert_eq!(results.len(), 0); } } diff --git a/crates/spyglass/src/search/query.rs b/crates/spyglass/src/search/query.rs index 1f82f99c0..48fe7d277 100644 --- a/crates/spyglass/src/search/query.rs +++ b/crates/spyglass/src/search/query.rs @@ -1,15 +1,16 @@ -use tantivy::query::{BooleanQuery, BoostQuery, Occur, Query, TermQuery}; +use tantivy::query::{BooleanQuery, BoostQuery, Occur, PhraseQuery, Query, TermQuery}; use tantivy::schema::*; +use tantivy::tokenizer::TokenizerManager; use tantivy::Score; use super::DocFields; type QueryVec = Vec<(Occur, Box)>; -fn _boosted_term(field: Field, term: &str, boost: Score) -> Box { +fn _boosted_term(term: Term, boost: Score) -> Box { Box::new(BoostQuery::new( Box::new(TermQuery::new( - Term::from_field_text(field, term), + term, // Needs WithFreqs otherwise scoring is wonky. IndexRecordOption::WithFreqs, )), @@ -17,34 +18,73 @@ fn _boosted_term(field: Field, term: &str, boost: Score) -> Box { )) } -pub fn build_query(fields: DocFields, query_string: &str) -> BooleanQuery { - // Tokenize query string - let query_string = query_string.to_lowercase(); - let terms: Vec<&str> = query_string - .split(' ') - .into_iter() - .map(|token| token.trim()) - .collect(); +fn _boosted_phrase(terms: Vec, boost: Score) -> Box { + Box::new(BoostQuery::new(Box::new(PhraseQuery::new(terms)), boost)) +} + +pub fn build_query( + schema: Schema, + tokenizers: TokenizerManager, + fields: DocFields, + query_string: &str, +) -> BooleanQuery { + let content_terms = terms_for_field(&schema, &tokenizers, query_string, fields.content); + let title_terms: Vec = terms_for_field(&schema, &tokenizers, query_string, fields.title); let mut term_query: QueryVec = Vec::new(); // Boost exact matches to the full query string - if terms.len() > 1 { - term_query.push(( - Occur::Should, - _boosted_term(fields.title, &query_string, 5.0), - )); - term_query.push(( - Occur::Should, - _boosted_term(fields.content, &query_string, 5.0), - )); + if content_terms.len() > 1 { + // boosting phrases relative to the number of segments in a + // continuous phrase + let boost = 2.0 * content_terms.len() as f32; + term_query.push((Occur::Should, _boosted_phrase(content_terms.clone(), boost))); + } + + // Boost exact matches to the full query string + if title_terms.len() > 1 { + // boosting phrases relative to the number of segments in a + // continuous phrase, base score higher for title + // than content + let boost = 2.5 * title_terms.len() as f32; + term_query.push((Occur::Should, _boosted_phrase(title_terms.clone(), boost))); + } + + for term in content_terms { + term_query.push((Occur::Should, _boosted_term(term, 1.0))); } - for term in terms { - // Emphasize matches in the title more than words in the content - term_query.push((Occur::Should, _boosted_term(fields.content, term, 1.0))); - term_query.push((Occur::Should, _boosted_term(fields.title, term, 5.0))); + for term in title_terms { + term_query.push((Occur::Should, _boosted_term(term, 2.0))); } BooleanQuery::new(vec![(Occur::Must, Box::new(BooleanQuery::new(term_query)))]) } + +/** + * Responsible for parsing the input query for a particular field. The tokenizer for the field + * is used to ensure consistent tokens between indexing and queries. + */ +fn terms_for_field( + schema: &Schema, + tokenizers: &TokenizerManager, + query: &str, + field: Field, +) -> Vec { + let mut terms = Vec::new(); + + let field_entry = schema.get_field_entry(field); + let field_type = field_entry.field_type(); + if let FieldType::Str(ref str_options) = field_type { + let option = str_options.get_indexing_options().unwrap(); + let text_analyzer = tokenizers.get(option.tokenizer()).unwrap(); + + let mut token_stream = text_analyzer.token_stream(query); + token_stream.process(&mut |token| { + let term = Term::from_field_text(field, &token.text); + terms.push(term); + }); + } + + terms +} diff --git a/crates/spyglass/src/task/worker.rs b/crates/spyglass/src/task/worker.rs index e43b52ea4..92b13939a 100644 --- a/crates/spyglass/src/task/worker.rs +++ b/crates/spyglass/src/task/worker.rs @@ -98,7 +98,10 @@ pub async fn handle_fetch(state: AppState, task: CrawlTask) -> FetchResult { // Add / update search index w/ crawl result. if let Some(content) = crawl_result.content { let url = Url::parse(&crawl_result.url).expect("Invalid crawl URL"); - let url_host = url.host_str().expect("Invalid URL host"); + let url_host = match url.scheme() { + "file" => "localhost", + _ => url.host_str().expect("Invalid URL host"), + }; let existing = indexed_document::Entity::find() .filter(indexed_document::Column::Url.eq(url.as_str())) diff --git a/crates/tauri/Cargo.toml b/crates/tauri/Cargo.toml index 351bb0e44..0c8c0e25d 100644 --- a/crates/tauri/Cargo.toml +++ b/crates/tauri/Cargo.toml @@ -24,7 +24,8 @@ num-format = "0.4" open = "3" reqwest = { version = "0.11", features = ["json"] } ron = "0.8" -sentry = "0.27.0" +sentry = "0.28.0" +sentry-tracing = "0.28.0" serde = { version = "1.0", features = ["derive"] } serde_json = "1.0" shared = { path = "../shared" } @@ -42,6 +43,7 @@ url = "2.2" [target.'cfg(target_os = "macos")'.dependencies] cocoa = "0.24" +objc = "0.2.7" [features] default = [ "custom-protocol" ] diff --git a/crates/tauri/src/cmd.rs b/crates/tauri/src/cmd.rs index 0809b1642..c8ffecc24 100644 --- a/crates/tauri/src/cmd.rs +++ b/crates/tauri/src/cmd.rs @@ -81,12 +81,17 @@ pub async fn open_result(_: tauri::Window, url: &str) -> Result<(), String> { { use shared::url_to_file_path; let path = url_to_file_path(url.path(), true); - open::that(format!("file://{}", path)).unwrap(); + if let Err(err) = open::that(format!("file://{}", path)) { + log::error!("Unable to open file://{} due to: {}", path, err); + } + return Ok(()); } } - open::that(url.to_string()).unwrap(); + if let Err(err) = open::that(url.to_string()) { + log::error!("Unable to open {} due to: {}", url.to_string(), err); + } } Ok(()) } diff --git a/crates/tauri/src/main.rs b/crates/tauri/src/main.rs index dc6612346..0fd5fb003 100644 --- a/crates/tauri/src/main.rs +++ b/crates/tauri/src/main.rs @@ -31,15 +31,21 @@ mod cmd; mod constants; mod menu; use menu::MenuID; +mod platform; mod plugins; mod rpc; mod window; use window::{ show_connection_manager_window, show_crawl_stats_window, show_lens_manager_window, - show_plugin_manager, show_search_bar, show_user_settings, show_wizard_window, + show_plugin_manager, show_search_bar, show_update_window, show_user_settings, + show_wizard_window, }; -use crate::window::show_update_window; +const LOG_LEVEL: tracing::Level = tracing::Level::INFO; +#[cfg(not(debug_assertions))] +const SPYGLASS_LEVEL: &str = "spyglass_app=INFO"; +#[cfg(debug_assertions)] +const SPYGLASS_LEVEL: &str = "spyglass_app=DEBUG"; #[derive(Clone)] pub struct AppShutdown; @@ -56,6 +62,7 @@ fn main() -> Result<(), Box> { "https://13d7d51a8293459abd0aba88f99f4c18@o1334159.ingest.sentry.io/6600471", sentry::ClientOptions { release: Some(Cow::from(ctx.package_info().version.to_string())), + traces_sample_rate: 0.1, ..Default::default() }, ))) @@ -83,13 +90,18 @@ fn main() -> Result<(), Box> { let (non_blocking, _guard) = tracing_appender::non_blocking(file_appender); let subscriber = tracing_subscriber::registry() - .with(EnvFilter::from_default_env().add_directive(tracing::Level::INFO.into())) + .with( + EnvFilter::from_default_env() + .add_directive(LOG_LEVEL.into()) + .add_directive(SPYGLASS_LEVEL.parse().expect("Invalid EnvFilter")), + ) .with( fmt::Layer::new() .with_thread_names(true) .with_writer(io::stdout), ) - .with(fmt::Layer::new().with_ansi(false).with_writer(non_blocking)); + .with(fmt::Layer::new().with_ansi(false).with_writer(non_blocking)) + .with(sentry_tracing::layer()); tracing::subscriber::set_global_default(subscriber).expect("Unable to set a global subscriber"); LogTracer::init()?; @@ -141,13 +153,8 @@ fn main() -> Result<(), Box> { log::error!("Unable to copy default plugins: {}", e); } - // macOS: hide from dock (also hides menu bar) - #[cfg(target_os = "macos")] - app.set_activation_policy(tauri::ActivationPolicy::Accessory); - let window = app.get_window(constants::SEARCH_WIN_NAME).expect("Main window not found"); window::center_search_bar(&window); - // macOS: Handle multiple spaces correctly #[cfg(target_os = "macos")] { @@ -176,14 +183,7 @@ fn main() -> Result<(), Box> { if let Err(e) = shortcuts .register(&config.user_settings.shortcut, move || { let window = window_clone.clone(); - let _ = window.is_visible() - .map(|is_visible| { - if is_visible { - window::hide_search_bar(&window); - } else { - window::show_search_bar(&window); - } - }); + window::show_search_bar(&window); }) { window::alert(&window, "Error registering global shortcut", &format!("{}", e)); } @@ -192,17 +192,17 @@ fn main() -> Result<(), Box> { Err(e) => window::alert(&window_clone, "Error registering global shortcut", &format!("{}", e)) } - // Hide searchbar on start. - let _ = window.hide(); Ok(()) }) - .on_window_event(|event| { - let window = event.window(); - if window.label() == "main" { - if let tauri::WindowEvent::Focused(is_focused) = event.event() { - if !is_focused { - let handle = event.window(); - window::hide_search_bar(handle); + .on_window_event(|_event| { + #[cfg(target_os = "macos")] + { + let window = _event.window(); + if window.label() == constants::SEARCH_WIN_NAME { + if let tauri::WindowEvent::Focused(is_focused) = _event.event() { + if !is_focused { + window::hide_search_bar(window); + } } } } @@ -233,17 +233,8 @@ fn main() -> Result<(), Box> { MenuID::OPEN_LOGS_FOLDER => open_folder(config.logs_dir()), MenuID::OPEN_SETTINGS_MANAGER => { show_user_settings(app) }, MenuID::OPEN_WIZARD => { show_wizard_window(app); } - MenuID::SHOW_CRAWL_STATUS => { - show_crawl_stats_window(app); - } - MenuID::SHOW_SEARCHBAR => { - let _ = window.is_visible() - .map(|is_visible| { - if !is_visible { - window::hide_search_bar(&window); - } - }); - } + MenuID::SHOW_CRAWL_STATUS => { show_crawl_stats_window(app); } + MenuID::SHOW_SEARCHBAR => { window::show_search_bar(&window); } MenuID::QUIT => app.exit(0), MenuID::DEV_SHOW_CONSOLE => window.open_devtools(), MenuID::JOIN_DISCORD => { @@ -261,6 +252,14 @@ fn main() -> Result<(), Box> { .expect("error while running tauri application"); app.run(|app_handle, e| match e { + RunEvent::MainEventsCleared => { + #[cfg(target_os = "macos")] + { + if let Some(window) = app_handle.get_window(constants::SEARCH_WIN_NAME) { + crate::platform::mac::poll_app_events(&window); + } + } + } RunEvent::ExitRequested { .. } => { // Do some cleanup for long running tasks let shutdown_tx = app_handle.state::>(); diff --git a/crates/tauri/src/menu.rs b/crates/tauri/src/menu.rs index fd3196cae..cca84401d 100644 --- a/crates/tauri/src/menu.rs +++ b/crates/tauri/src/menu.rs @@ -113,6 +113,7 @@ pub fn get_app_menu(ctx: &Context) -> Menu { .add_native_item(MenuItem::Paste) .add_native_item(MenuItem::SelectAll) .add_native_item(MenuItem::Separator) + .add_native_item(MenuItem::Hide) .add_native_item(MenuItem::Quit), )) } diff --git a/crates/tauri/src/platform/linux.rs b/crates/tauri/src/platform/linux.rs new file mode 100644 index 000000000..8a1470cc4 --- /dev/null +++ b/crates/tauri/src/platform/linux.rs @@ -0,0 +1,17 @@ +use crate::window; +use shared::event::ClientEvent; +use tauri::Window; + +pub fn show_search_bar(window: &Window) { + let _ = window.unminimize(); + window::center_search_bar(window); + let _ = window.set_focus(); + let _ = window.set_always_on_top(true); + + let _ = window.emit(ClientEvent::FocusWindow.as_ref(), true); +} + +pub fn hide_search_bar(window: &Window) { + let _ = window.minimize(); + let _ = window.emit(ClientEvent::ClearSearch.as_ref(), true); +} diff --git a/crates/tauri/src/platform/mac.rs b/crates/tauri/src/platform/mac.rs new file mode 100644 index 000000000..aae024db5 --- /dev/null +++ b/crates/tauri/src/platform/mac.rs @@ -0,0 +1,51 @@ +use cocoa::appkit::{NSApp, NSApplication, NSEvent, NSEventMask, NSEventSubtype}; +use cocoa::base::nil; +use cocoa::foundation::{NSAutoreleasePool, NSDate, NSString}; + +use tauri::Window; + +use crate::window; +use shared::event::ClientEvent; + +/// Poll for dock events +pub fn poll_app_events(window: &Window) { + unsafe { + let _pool = NSAutoreleasePool::new(nil); + let ns_event = NSApp().nextEventMatchingMask_untilDate_inMode_dequeue_( + NSEventMask::NSAppKitDefinedMask.bits(), + NSDate::distantPast(cocoa::base::nil), + // Use custom event loop name so we don't trampled on others. + NSString::alloc(nil).init_str("spyglassEventLoop"), + cocoa::base::YES, + ); + + if ns_event == nil || ns_event.eventType() as u64 == 21 { + return; + } + + let subtype = ns_event.subtype(); + match subtype { + NSEventSubtype::NSApplicationActivatedEventType => { + window::show_search_bar(window); + } + NSEventSubtype::NSApplicationDeactivatedEventType => { + window::hide_search_bar(window); + } + _ => {} + } + } +} + +pub fn show_search_bar(window: &Window) { + let _ = window.show(); + window::center_search_bar(window); + let _ = window.set_always_on_top(true); + let _ = window.set_focus(); + + let _ = window.emit(ClientEvent::FocusWindow.as_ref(), true); +} + +pub fn hide_search_bar(window: &Window) { + let _ = window.hide(); + let _ = window.emit(ClientEvent::ClearSearch.as_ref(), true); +} diff --git a/crates/tauri/src/platform/mod.rs b/crates/tauri/src/platform/mod.rs new file mode 100644 index 000000000..a0b6368ab --- /dev/null +++ b/crates/tauri/src/platform/mod.rs @@ -0,0 +1,7 @@ +/// Platform specific implementation of things +#[cfg(target_os = "linux")] +pub mod linux; +#[cfg(target_os = "macos")] +pub mod mac; +#[cfg(target_os = "windows")] +pub mod windows; diff --git a/crates/tauri/src/platform/windows.rs b/crates/tauri/src/platform/windows.rs new file mode 100644 index 000000000..2c9d1b433 --- /dev/null +++ b/crates/tauri/src/platform/windows.rs @@ -0,0 +1,16 @@ +use crate::window; +use shared::event::ClientEvent; +use tauri::Window; + +pub fn show_search_bar(window: &Window) { + let _ = window.unminimize(); + window::center_search_bar(window); + let _ = window.set_focus(); + + let _ = window.emit(ClientEvent::FocusWindow.as_ref(), true); +} + +pub fn hide_search_bar(window: &Window) { + let _ = window.minimize(); + let _ = window.emit(ClientEvent::ClearSearch.as_ref(), true); +} diff --git a/crates/tauri/src/plugins/lens_updater.rs b/crates/tauri/src/plugins/lens_updater.rs index 0af859b39..a10b85576 100644 --- a/crates/tauri/src/plugins/lens_updater.rs +++ b/crates/tauri/src/plugins/lens_updater.rs @@ -129,8 +129,12 @@ pub async fn install_lens_to_path(download_url: &str, lens_folder: PathBuf) -> a // Grab the file name from the end of the URL let url = Url::parse(download_url)?; - let mut segments = url.path_segments().map(|c| c.collect::>()).unwrap(); - let file_name = segments.pop().unwrap(); + let file_name = url + .path_segments() + .map(|c| c.collect::>()) + .and_then(|mut segs| segs.pop()) + .expect("Unable to determine filename from lens path"); + // Write file out to lens folder fs::write(lens_folder.join(file_name), file_contents)?; diff --git a/crates/tauri/src/window.rs b/crates/tauri/src/window.rs index 7b9efedd8..7d08eb918 100644 --- a/crates/tauri/src/window.rs +++ b/crates/tauri/src/window.rs @@ -1,4 +1,4 @@ -use crate::constants; +use crate::{constants, platform}; use shared::event::ClientEvent; use tauri::api::dialog::{MessageDialogBuilder, MessageDialogButtons, MessageDialogKind}; use tauri::{AppHandle, LogicalSize, Manager, Monitor, Size, Window, WindowBuilder, WindowUrl}; @@ -38,16 +38,25 @@ pub fn center_search_bar(window: &Window) { } pub fn show_search_bar(window: &Window) { - let _ = window.emit(ClientEvent::FocusWindow.as_ref(), true); - let _ = window.show(); - let _ = window.set_focus(); - let _ = window.set_always_on_top(true); - center_search_bar(window); + #[cfg(target_os = "linux")] + platform::linux::show_search_bar(window); + + #[cfg(target_os = "macos")] + platform::mac::show_search_bar(window); + + #[cfg(target_os = "windows")] + platform::windows::show_search_bar(window); } pub fn hide_search_bar(window: &Window) { - let _ = window.hide(); - let _ = window.emit(ClientEvent::ClearSearch.as_ref(), true); + #[cfg(target_os = "linux")] + platform::linux::hide_search_bar(window); + + #[cfg(target_os = "macos")] + platform::mac::hide_search_bar(window); + + #[cfg(target_os = "windows")] + platform::windows::hide_search_bar(window); } pub async fn resize_window(window: &Window, height: f64) { diff --git a/crates/tauri/tauri.conf.json b/crates/tauri/tauri.conf.json index e8c8066e0..3343b82f4 100644 --- a/crates/tauri/tauri.conf.json +++ b/crates/tauri/tauri.conf.json @@ -1,7 +1,7 @@ { "package": { "productName": "Spyglass", - "version": "22.11.5" + "version": "22.11.6" }, "build": { "distDir": "../client/dist", @@ -74,7 +74,7 @@ "width": 640, "height": 96, "transparent": true, - "skipTaskbar": true + "skipTaskbar": false }], "security": { "csp": "default-src blob: data: filesystem: ws: wss: http: https: tauri: 'unsafe-eval' 'unsafe-inline' 'self'; script-src 'unsafe-eval'"