Merge branch 'master' into ian/gb-220-ai-chat-loading-state-ux
1
.gitignore
vendored
@ -28,6 +28,7 @@ dist-ssr
|
||||
/package
|
||||
!.env.example
|
||||
vite.config.js.timestamp-*
|
||||
vite.config.ts.timestamp-*
|
||||
|
||||
# gitbutler
|
||||
.git/gb-*
|
||||
|
@ -37,6 +37,7 @@
|
||||
"@lezer/javascript": "^1.4.1",
|
||||
"@lezer/lr": "^1.3.3",
|
||||
"@replit/codemirror-lang-svelte": "^6.0.0",
|
||||
"@sentry/sveltekit": "^7.49.0",
|
||||
"@square/svelte-store": "^1.0.14",
|
||||
"@storybook/addon-essentials": "next",
|
||||
"@storybook/addon-interactions": "next",
|
||||
|
901
pnpm-lock.yaml
@ -178,7 +178,7 @@ trap "rm -rf '$TMP_DIR'" exit
|
||||
jq '.package.version="'"$VERSION"'"' "$PWD/../src-tauri/tauri.conf.release.json" >"$TMP_DIR/tauri.conf.json"
|
||||
|
||||
# build the app with release config
|
||||
tauri build --config "$TMP_DIR/tauri.conf.json"
|
||||
SENTRY_RELEASE="$VERSION" tauri build --config "$TMP_DIR/tauri.conf.json"
|
||||
|
||||
BUNDLE_DIR="$PWD/../src-tauri/target/release/bundle"
|
||||
MACOS_DMG="$(find "$BUNDLE_DIR/dmg" -depth 1 -type f -name "*.dmg")"
|
||||
|
327
src-tauri/Cargo.lock
generated
@ -66,6 +66,9 @@ name = "anyhow"
|
||||
version = "1.0.70"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "7de8ce5e0f9f8d88245311066a578d72b7af3e7088f32783804676302df237e4"
|
||||
dependencies = [
|
||||
"backtrace",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "arc-swap"
|
||||
@ -528,16 +531,16 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "crash-handler"
|
||||
version = "0.3.3"
|
||||
version = "0.4.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "3b333adce79bc5ee4f421211a160514ed5a3f4dc9cd4202021d70ebdd7657e50"
|
||||
checksum = "37571ca6b1166c54bdde2b482eea4c935207d5d82889382b327ad6fcf88ce656"
|
||||
dependencies = [
|
||||
"cfg-if",
|
||||
"crash-context",
|
||||
"libc",
|
||||
"mach2",
|
||||
"parking_lot",
|
||||
"windows-sys 0.36.1",
|
||||
"windows-sys 0.42.0",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@ -985,6 +988,18 @@ dependencies = [
|
||||
"windows-sys 0.48.0",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "findshlibs"
|
||||
version = "0.10.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "40b9e59cd0f7e0806cca4be089683ecb6434e602038df21fe6bf6711b2f07f64"
|
||||
dependencies = [
|
||||
"cc",
|
||||
"lazy_static",
|
||||
"libc",
|
||||
"winapi",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "flate2"
|
||||
version = "1.0.25"
|
||||
@ -1333,8 +1348,10 @@ dependencies = [
|
||||
"portable-pty",
|
||||
"reqwest",
|
||||
"scopeguard",
|
||||
"sentry",
|
||||
"sentry-tauri",
|
||||
"sentry 0.31.0",
|
||||
"sentry-anyhow",
|
||||
"sentry-debug-images 0.31.0",
|
||||
"sentry-rust-minidump",
|
||||
"serde",
|
||||
"serde-jsonlines",
|
||||
"serde_json",
|
||||
@ -2226,9 +2243,9 @@ checksum = "6877bb514081ee2a7ff5ef9de3281f14a4dd4bceac4c09388074a6b5df8a139a"
|
||||
|
||||
[[package]]
|
||||
name = "minidump-common"
|
||||
version = "0.12.0"
|
||||
version = "0.14.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "a134e10507b4352836be67e13cfa06e7881df5b4e091999e75c05eb449dc6985"
|
||||
checksum = "97dbaf56dfe28d07e1fecffce410976774dcac1f0d7f6d797b437468e989e687"
|
||||
dependencies = [
|
||||
"bitflags 1.3.2",
|
||||
"debugid",
|
||||
@ -2242,9 +2259,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "minidump-writer"
|
||||
version = "0.4.0"
|
||||
version = "0.5.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "0e663ad99c0ad5f62dec920482cae3dc84d4138395efbac39a3c1ab817fe9d59"
|
||||
checksum = "8c8c8fcc3823f1f4eec257a24990ab4c6a20ef9bcbc515400d0eb4a99620b943"
|
||||
dependencies = [
|
||||
"byteorder",
|
||||
"cfg-if",
|
||||
@ -2255,18 +2272,18 @@ dependencies = [
|
||||
"memmap2",
|
||||
"memoffset 0.6.5",
|
||||
"minidump-common",
|
||||
"nix 0.24.3",
|
||||
"nix",
|
||||
"scroll",
|
||||
"tempfile",
|
||||
"thiserror",
|
||||
"windows-sys 0.36.1",
|
||||
"windows-sys 0.42.0",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "minidumper"
|
||||
version = "0.5.1"
|
||||
version = "0.6.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "7bc0932e7cf3760d8ea4784bbe7ec45cc1970c08d1f6e7d108d8da1890d26586"
|
||||
checksum = "a8e7577424f7abeacef16980ed8e200c84e74aaabb799c3ee30159ea877534fb"
|
||||
dependencies = [
|
||||
"cfg-if",
|
||||
"crash-context",
|
||||
@ -2278,7 +2295,19 @@ dependencies = [
|
||||
"scroll",
|
||||
"thiserror",
|
||||
"uds",
|
||||
"windows-sys 0.36.1",
|
||||
"windows-sys 0.42.0",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "minidumper-child"
|
||||
version = "0.1.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "c5f6a7c1a63d65dd21fa335135e5a614a7adb03560b2feb9fcb70eef34439bc8"
|
||||
dependencies = [
|
||||
"crash-handler",
|
||||
"minidumper",
|
||||
"thiserror",
|
||||
"uuid 1.3.1",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@ -2373,17 +2402,6 @@ version = "1.0.4"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "e4a24736216ec316047a1fc4252e27dabb04218aa4a3f37c6e7ddbf1f9782b54"
|
||||
|
||||
[[package]]
|
||||
name = "nix"
|
||||
version = "0.24.3"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "fa52e972a9a719cecb6864fb88568781eb706bac2cd1d4f04a648542dbf78069"
|
||||
dependencies = [
|
||||
"bitflags 1.3.2",
|
||||
"cfg-if",
|
||||
"libc",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "nix"
|
||||
version = "0.25.1"
|
||||
@ -2638,6 +2656,17 @@ dependencies = [
|
||||
"vcpkg",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "os_info"
|
||||
version = "3.7.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "006e42d5b888366f1880eda20371fedde764ed2213dc8496f49622fa0c99cd5e"
|
||||
dependencies = [
|
||||
"log",
|
||||
"serde",
|
||||
"winapi",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "overload"
|
||||
version = "0.1.1"
|
||||
@ -2897,7 +2926,7 @@ dependencies = [
|
||||
"lazy_static",
|
||||
"libc",
|
||||
"log",
|
||||
"nix 0.25.1",
|
||||
"nix",
|
||||
"serde",
|
||||
"serde_derive",
|
||||
"serial",
|
||||
@ -3436,99 +3465,205 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "sentry"
|
||||
version = "0.27.0"
|
||||
version = "0.30.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "73642819e7fa63eb264abc818a2f65ac8764afbe4870b5ee25bcecc491be0d4c"
|
||||
checksum = "b5ce6d3512e2617c209ec1e86b0ca2fea06454cd34653c91092bf0f3ec41f8e3"
|
||||
dependencies = [
|
||||
"httpdate",
|
||||
"native-tls",
|
||||
"reqwest",
|
||||
"sentry-backtrace",
|
||||
"sentry-contexts",
|
||||
"sentry-core",
|
||||
"sentry-panic",
|
||||
"sentry-backtrace 0.30.0",
|
||||
"sentry-contexts 0.30.0",
|
||||
"sentry-core 0.30.0",
|
||||
"sentry-debug-images 0.30.0",
|
||||
"sentry-panic 0.30.0",
|
||||
"tokio",
|
||||
"ureq",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "sentry"
|
||||
version = "0.31.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "6c3d7f8bf7373e75222452fcdd9347d857452a92d0eec738f941bc4656c5b5df"
|
||||
dependencies = [
|
||||
"httpdate",
|
||||
"native-tls",
|
||||
"reqwest",
|
||||
"sentry-anyhow",
|
||||
"sentry-backtrace 0.31.0",
|
||||
"sentry-contexts 0.31.0",
|
||||
"sentry-core 0.31.0",
|
||||
"sentry-debug-images 0.31.0",
|
||||
"sentry-panic 0.31.0",
|
||||
"tokio",
|
||||
"ureq",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "sentry-anyhow"
|
||||
version = "0.31.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "9ef7f47c57a1146d553b4976f20e8bba370195a88858bdf6945a63c529549236"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"sentry-backtrace 0.31.0",
|
||||
"sentry-core 0.31.0",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "sentry-backtrace"
|
||||
version = "0.27.0"
|
||||
version = "0.30.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "49bafa55eefc6dbc04c7dac91e8c8ab9e89e9414f3193c105cabd991bbc75134"
|
||||
checksum = "0e7fe408d4d1f8de188a9309916e02e129cbe51ca19e55badea5a64899399b1a"
|
||||
dependencies = [
|
||||
"backtrace",
|
||||
"once_cell",
|
||||
"regex",
|
||||
"sentry-core",
|
||||
"sentry-core 0.30.0",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "sentry-backtrace"
|
||||
version = "0.31.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "03b7cdefbdca51f1146f0f24a3cb4ecb6428951f030ff5c720cfb5c60bd174c0"
|
||||
dependencies = [
|
||||
"backtrace",
|
||||
"once_cell",
|
||||
"regex",
|
||||
"sentry-core 0.31.0",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "sentry-contexts"
|
||||
version = "0.27.0"
|
||||
version = "0.30.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "c63317c4051889e73f0b00ce4024cae3e6a225f2e18a27d2c1522eb9ce2743da"
|
||||
checksum = "5695096a059a89973ec541062d331ff4c9aeef9c2951416c894f0fff76340e7d"
|
||||
dependencies = [
|
||||
"hostname",
|
||||
"libc",
|
||||
"os_info",
|
||||
"rustc_version",
|
||||
"sentry-core",
|
||||
"sentry-core 0.30.0",
|
||||
"uname",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "sentry-contexts"
|
||||
version = "0.31.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "6af4cb29066e0e8df0cc3111211eb93543ccb09e1ccbe71de6d88b4bb459a2b1"
|
||||
dependencies = [
|
||||
"hostname",
|
||||
"libc",
|
||||
"os_info",
|
||||
"rustc_version",
|
||||
"sentry-core 0.31.0",
|
||||
"uname",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "sentry-core"
|
||||
version = "0.27.0"
|
||||
version = "0.30.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "5a4591a2d128af73b1b819ab95f143bc6a2fbe48cd23a4c45e1ee32177e66ae6"
|
||||
checksum = "5b22828bfd118a7b660cf7a155002a494755c0424cebb7061e4743ecde9c7dbc"
|
||||
dependencies = [
|
||||
"once_cell",
|
||||
"rand 0.8.5",
|
||||
"sentry-types",
|
||||
"sentry-types 0.30.0",
|
||||
"serde",
|
||||
"serde_json",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "sentry-panic"
|
||||
version = "0.27.0"
|
||||
name = "sentry-core"
|
||||
version = "0.31.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "696c74c5882d5a0d5b4a31d0ff3989b04da49be7983b7f52a52c667da5b480bf"
|
||||
checksum = "5e781b55761e47a60d1ff326ae8059de22b0e6b0cee68eab1c5912e4fb199a76"
|
||||
dependencies = [
|
||||
"sentry-backtrace",
|
||||
"sentry-core",
|
||||
"once_cell",
|
||||
"rand 0.8.5",
|
||||
"sentry-types 0.31.0",
|
||||
"serde",
|
||||
"serde_json",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "sentry-debug-images"
|
||||
version = "0.30.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "0a9164d44a2929b1b7670afd7e87552514b70d3ae672ca52884639373d912a3d"
|
||||
dependencies = [
|
||||
"findshlibs",
|
||||
"once_cell",
|
||||
"sentry-core 0.30.0",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "sentry-debug-images"
|
||||
version = "0.31.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "e758030b31ee2cd97424a980dfa34a12dcd8477424861cf81ae3aa1f9f616a8c"
|
||||
dependencies = [
|
||||
"findshlibs",
|
||||
"once_cell",
|
||||
"sentry-core 0.31.0",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "sentry-panic"
|
||||
version = "0.30.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "1f4ced2a7a8c14899d58eec402d946f69d5ed26a3fc363a7e8b1e5cb88473a01"
|
||||
dependencies = [
|
||||
"sentry-backtrace 0.30.0",
|
||||
"sentry-core 0.30.0",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "sentry-panic"
|
||||
version = "0.31.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "0e0b877981990d9e84ae6916df61993d188fdf76afb59521f0aeaf9b8e6d26d0"
|
||||
dependencies = [
|
||||
"sentry-backtrace 0.31.0",
|
||||
"sentry-core 0.31.0",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "sentry-rust-minidump"
|
||||
version = "0.1.3"
|
||||
version = "0.5.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "d986b33fedaa7638225e07ac1cc0217894ee2958798a09e2d61b2fe5f486bc89"
|
||||
checksum = "a489204e5cb676310d7ef1506eefb76d006a82f27b648f5477ea74733467d5a1"
|
||||
dependencies = [
|
||||
"crash-handler",
|
||||
"dirs-next",
|
||||
"minidumper",
|
||||
"sentry",
|
||||
"thiserror",
|
||||
"uuid 1.3.1",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "sentry-tauri"
|
||||
version = "0.1.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "a13a2132c1f33f471f3842001c1aee74df0b94c10628833da798275285bcf0f9"
|
||||
dependencies = [
|
||||
"sentry",
|
||||
"sentry-rust-minidump",
|
||||
"serde",
|
||||
"tauri",
|
||||
"minidumper-child",
|
||||
"sentry 0.30.0",
|
||||
"thiserror",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "sentry-types"
|
||||
version = "0.27.0"
|
||||
version = "0.30.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "823923ae5f54a729159d720aa12181673044ee5c79cbda3be09e56f885e5468f"
|
||||
checksum = "360ee3270f7a4a1eee6c667f7d38360b995431598a73b740dfe420da548d9cc9"
|
||||
dependencies = [
|
||||
"debugid",
|
||||
"getrandom 0.2.9",
|
||||
"hex",
|
||||
"serde",
|
||||
"serde_json",
|
||||
"thiserror",
|
||||
"time",
|
||||
"url",
|
||||
"uuid 1.3.1",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "sentry-types"
|
||||
version = "0.31.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "d642a04657cc77d8de52ae7c6d93a15cb02284eb219344a89c1e2b26bbaf578c"
|
||||
dependencies = [
|
||||
"debugid",
|
||||
"getrandom 0.2.9",
|
||||
@ -4745,6 +4880,19 @@ version = "0.1.10"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "c0edd1e5b14653f783770bce4a4dabb4a5108a5370a5f5d8cfe8710c361f6c8b"
|
||||
|
||||
[[package]]
|
||||
name = "ureq"
|
||||
version = "2.6.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "338b31dd1314f68f3aabf3ed57ab922df95ffcd902476ca7ba3c4ce7b908c46d"
|
||||
dependencies = [
|
||||
"base64 0.13.1",
|
||||
"log",
|
||||
"native-tls",
|
||||
"once_cell",
|
||||
"url",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "url"
|
||||
version = "2.3.1"
|
||||
@ -5123,19 +5271,6 @@ version = "0.39.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "9ee5e275231f07c6e240d14f34e1b635bf1faa1c76c57cfd59a5cdb9848e4278"
|
||||
|
||||
[[package]]
|
||||
name = "windows-sys"
|
||||
version = "0.36.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "ea04155a16a59f9eab786fe12a4a450e75cdb175f9e0d80da1e17db09f55b8d2"
|
||||
dependencies = [
|
||||
"windows_aarch64_msvc 0.36.1",
|
||||
"windows_i686_gnu 0.36.1",
|
||||
"windows_i686_msvc 0.36.1",
|
||||
"windows_x86_64_gnu 0.36.1",
|
||||
"windows_x86_64_msvc 0.36.1",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "windows-sys"
|
||||
version = "0.42.0"
|
||||
@ -5217,12 +5352,6 @@ version = "0.48.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "91ae572e1b79dba883e0d315474df7305d12f569b400fcf90581b06062f7e1bc"
|
||||
|
||||
[[package]]
|
||||
name = "windows_aarch64_msvc"
|
||||
version = "0.36.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "9bb8c3fd39ade2d67e9874ac4f3db21f0d710bee00fe7cab16949ec184eeaa47"
|
||||
|
||||
[[package]]
|
||||
name = "windows_aarch64_msvc"
|
||||
version = "0.37.0"
|
||||
@ -5247,12 +5376,6 @@ version = "0.48.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "b2ef27e0d7bdfcfc7b868b317c1d32c641a6fe4629c171b8928c7b08d98d7cf3"
|
||||
|
||||
[[package]]
|
||||
name = "windows_i686_gnu"
|
||||
version = "0.36.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "180e6ccf01daf4c426b846dfc66db1fc518f074baa793aa7d9b9aaeffad6a3b6"
|
||||
|
||||
[[package]]
|
||||
name = "windows_i686_gnu"
|
||||
version = "0.37.0"
|
||||
@ -5277,12 +5400,6 @@ version = "0.48.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "622a1962a7db830d6fd0a69683c80a18fda201879f0f447f065a3b7467daa241"
|
||||
|
||||
[[package]]
|
||||
name = "windows_i686_msvc"
|
||||
version = "0.36.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "e2e7917148b2812d1eeafaeb22a97e4813dfa60a3f8f78ebe204bcc88f12f024"
|
||||
|
||||
[[package]]
|
||||
name = "windows_i686_msvc"
|
||||
version = "0.37.0"
|
||||
@ -5307,12 +5424,6 @@ version = "0.48.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "4542c6e364ce21bf45d69fdd2a8e455fa38d316158cfd43b3ac1c5b1b19f8e00"
|
||||
|
||||
[[package]]
|
||||
name = "windows_x86_64_gnu"
|
||||
version = "0.36.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "4dcd171b8776c41b97521e5da127a2d86ad280114807d0b2ab1e462bc764d9e1"
|
||||
|
||||
[[package]]
|
||||
name = "windows_x86_64_gnu"
|
||||
version = "0.37.0"
|
||||
@ -5349,12 +5460,6 @@ version = "0.48.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "7896dbc1f41e08872e9d5e8f8baa8fdd2677f29468c4e156210174edc7f7b953"
|
||||
|
||||
[[package]]
|
||||
name = "windows_x86_64_msvc"
|
||||
version = "0.36.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "c811ca4a8c853ef420abd8592ba53ddbbac90410fab6903b3e79972a631f7680"
|
||||
|
||||
[[package]]
|
||||
name = "windows_x86_64_msvc"
|
||||
version = "0.37.0"
|
||||
|
@ -26,8 +26,7 @@ uuid = "1.3.0"
|
||||
git2 = { version = "0.16.1", features = ["vendored-openssl", "vendored-libgit2"] }
|
||||
filetime = "0.2.19"
|
||||
sha2 = "0.10.6"
|
||||
sentry-tauri = "0.1.0"
|
||||
sentry = "0.27"
|
||||
sentry = {version = "0.31.0", features = ["backtrace", "contexts", "panic", "transport", "anyhow", "debug-images", "reqwest", "native-tls" ] }
|
||||
walkdir = "2.3.2"
|
||||
anyhow = "1.0.69"
|
||||
tempfile = "3.3.0"
|
||||
@ -48,6 +47,9 @@ timed = "0.2.1"
|
||||
serde-jsonlines = "0.4.0"
|
||||
crossbeam-channel = "0.5.8"
|
||||
scopeguard = "1.1.0"
|
||||
sentry-anyhow = "0.31.0"
|
||||
sentry-rust-minidump = "0.5.1"
|
||||
sentry-debug-images = "0.31.0"
|
||||
|
||||
[features]
|
||||
# by default Tauri runs in production mode
|
||||
|
@ -493,7 +493,6 @@ fn build_wd_tree(
|
||||
abs_path.display(),
|
||||
e
|
||||
);
|
||||
println!("failed to read file {}: {:#}", abs_path.display(), e);
|
||||
continue;
|
||||
}
|
||||
};
|
||||
|
@ -2,7 +2,6 @@ use std::{collections::HashMap, env};
|
||||
|
||||
use anyhow::{Context, Result};
|
||||
use serde::Serialize;
|
||||
use tauri::regex::Regex;
|
||||
use walkdir::WalkDir;
|
||||
|
||||
use crate::{git::activity, projects};
|
||||
@ -211,25 +210,23 @@ impl<'repository> Repository<'repository> {
|
||||
.workdir()
|
||||
.with_context(|| "failed to get working directory")?;
|
||||
|
||||
let pattern = Regex::new(pattern).with_context(|| "regex parse error");
|
||||
match pattern {
|
||||
Ok(pattern) => {
|
||||
let mut files = vec![];
|
||||
for entry in WalkDir::new(workdir)
|
||||
let pattern = pattern.to_lowercase();
|
||||
let mut files = vec![];
|
||||
for entry in WalkDir::new(workdir)
|
||||
.into_iter()
|
||||
.filter_entry(|e| {
|
||||
.filter_entry(|entry| {
|
||||
// need to remove workdir so we're not matching it
|
||||
let match_string = e
|
||||
let relative_path = entry
|
||||
.path()
|
||||
.strip_prefix::<&std::path::Path>(workdir.as_ref())
|
||||
.strip_prefix(workdir)
|
||||
.unwrap()
|
||||
.to_str()
|
||||
.unwrap();
|
||||
// this is to make it faster, so we dont have to traverse every directory if it is ignored by git
|
||||
e.path().to_str() == workdir.to_str() // but we need to traverse the first one
|
||||
|| ((e.file_type().is_dir() // traverse all directories if they are not ignored by git
|
||||
|| pattern.is_match(match_string)) // but only pass on files that match the regex
|
||||
&& !self.git_repository.is_path_ignored(&e.path()).unwrap_or(true))
|
||||
entry.path().to_str() == workdir.to_str() // but we need to traverse the first one
|
||||
|| ((entry.file_type().is_dir() // traverse all directories if they are not ignored by git
|
||||
|| relative_path.to_lowercase().contains(&pattern)) // but only pass on files that match the regex
|
||||
&& !self.git_repository.is_path_ignored(&entry.path()).unwrap_or(true))
|
||||
})
|
||||
.filter_map(Result::ok)
|
||||
{
|
||||
@ -248,13 +245,8 @@ impl<'repository> Repository<'repository> {
|
||||
files.push(path);
|
||||
}
|
||||
}
|
||||
files.sort();
|
||||
return Ok(files);
|
||||
}
|
||||
Err(e) => {
|
||||
return Err(e);
|
||||
}
|
||||
}
|
||||
files.sort();
|
||||
return Ok(files);
|
||||
}
|
||||
|
||||
pub fn git_branches(&self) -> Result<Vec<String>> {
|
||||
|
@ -91,7 +91,6 @@ pub fn get_delta_operations(initial_text: &str, final_text: &str) -> Vec<Operati
|
||||
|
||||
let mut offset = 0;
|
||||
for change in changeset.iter_all_changes() {
|
||||
println!("{:?}", change);
|
||||
match change.tag() {
|
||||
ChangeTag::Delete => {
|
||||
deltas.push(Operation::Delete((
|
||||
|
@ -58,6 +58,7 @@ impl From<projects::CreateError> for Error {
|
||||
|
||||
impl From<anyhow::Error> for Error {
|
||||
fn from(e: anyhow::Error) -> Self {
|
||||
sentry_anyhow::capture_anyhow(&e);
|
||||
log::error!("{:#}", e);
|
||||
Error::Unknown
|
||||
}
|
||||
@ -65,14 +66,6 @@ impl From<anyhow::Error> for Error {
|
||||
|
||||
const IS_DEV: bool = cfg!(debug_assertions);
|
||||
|
||||
fn app_title() -> String {
|
||||
if IS_DEV {
|
||||
"GitButler (dev)".to_string()
|
||||
} else {
|
||||
"GitButler".to_string()
|
||||
}
|
||||
}
|
||||
|
||||
fn build_asset_url(path: &str) -> String {
|
||||
format!("asset://localhost/{}", urlencoding::encode(path))
|
||||
}
|
||||
@ -410,15 +403,27 @@ async fn git_commit(
|
||||
}
|
||||
|
||||
fn main() {
|
||||
let tauri_context = generate_context!();
|
||||
|
||||
let _guard = sentry::init(("https://9d407634d26b4d30b6a42d57a136d255@o4504644069687296.ingest.sentry.io/4504649768108032", sentry::ClientOptions {
|
||||
release: Some(tauri_context.package_info().version.to_string().into()),
|
||||
attach_stacktrace: true,
|
||||
default_integrations: true,
|
||||
..Default::default()
|
||||
}));
|
||||
|
||||
let app_title = tauri_context.package_info().name.clone();
|
||||
|
||||
let quit = tauri::CustomMenuItem::new("quit".to_string(), "Quit");
|
||||
let hide = tauri::CustomMenuItem::new("toggle".to_string(), format!("Hide {}", app_title()));
|
||||
let hide = tauri::CustomMenuItem::new("toggle".to_string(), format!("Hide {}", app_title));
|
||||
let tray_menu = tauri::SystemTrayMenu::new().add_item(hide).add_item(quit);
|
||||
let tray = tauri::SystemTray::new().with_menu(tray_menu);
|
||||
|
||||
let tauri_app_builder = tauri::Builder::default()
|
||||
tauri::Builder::default()
|
||||
.system_tray(tray)
|
||||
.on_system_tray_event(|app_handle, event| match event {
|
||||
tauri::SystemTrayEvent::MenuItemClick { id, .. } => {
|
||||
let app_title = app_handle.package_info().name.clone();
|
||||
let item_handle = app_handle.tray_handle().get_item(&id);
|
||||
match id.as_str() {
|
||||
"quit" => {
|
||||
@ -429,20 +434,20 @@ fn main() {
|
||||
if window.is_visible().unwrap() {
|
||||
window.hide().unwrap();
|
||||
item_handle
|
||||
.set_title(format!("Show {}", app_title()))
|
||||
.set_title(format!("Show {}", app_title))
|
||||
.unwrap();
|
||||
} else {
|
||||
window.show().unwrap();
|
||||
window.set_focus().unwrap();
|
||||
item_handle
|
||||
.set_title(format!("Hide {}", app_title()))
|
||||
.set_title(format!("Hide {}", app_title))
|
||||
.unwrap();
|
||||
}
|
||||
}
|
||||
None => {
|
||||
create_window(&app_handle).expect("Failed to create window");
|
||||
item_handle
|
||||
.set_title(format!("Hide {}", app_title()))
|
||||
.set_title(format!("Hide {}", app_title))
|
||||
.unwrap();
|
||||
}
|
||||
},
|
||||
@ -455,12 +460,13 @@ fn main() {
|
||||
tauri::WindowEvent::CloseRequested { api, .. } => {
|
||||
api.prevent_close();
|
||||
let window = event.window();
|
||||
let app_handle = window.app_handle();
|
||||
let app_title = app_handle.package_info().name.clone();
|
||||
|
||||
window
|
||||
.app_handle()
|
||||
app_handle
|
||||
.tray_handle()
|
||||
.get_item("toggle")
|
||||
.set_title(format!("Show {}", app_title()))
|
||||
.set_title(format!("Show {}", app_title))
|
||||
.expect("Failed to set tray item title");
|
||||
|
||||
window.hide().expect("Failed to hide window");
|
||||
@ -544,37 +550,16 @@ fn main() {
|
||||
git_stage,
|
||||
git_unstage,
|
||||
git_wd_diff,
|
||||
]);
|
||||
|
||||
let tauri_context = generate_context!();
|
||||
let app_version = tauri_context.package_info().version.to_string();
|
||||
|
||||
sentry_tauri::init(
|
||||
app_version.clone(),
|
||||
|_| {
|
||||
sentry::init((
|
||||
"https://9d407634d26b4d30b6a42d57a136d255@o4504644069687296.ingest.sentry.io/4504649768108032",
|
||||
sentry::ClientOptions {
|
||||
release: Some(std::borrow::Cow::from(app_version)),
|
||||
..Default::default()
|
||||
},
|
||||
))
|
||||
},
|
||||
|sentry_plugin| {
|
||||
let tauri_app = tauri_app_builder
|
||||
.plugin(sentry_plugin)
|
||||
.build(tauri_context)
|
||||
.expect("Failed to build tauri app");
|
||||
|
||||
tauri_app.run(|app_handle, event| match event {
|
||||
tauri::RunEvent::ExitRequested { api, .. } => {
|
||||
hide_window(&app_handle).expect("Failed to hide window");
|
||||
api.prevent_exit();
|
||||
}
|
||||
_ => {}
|
||||
});
|
||||
},
|
||||
);
|
||||
])
|
||||
.build(tauri_context)
|
||||
.expect("Failed to build tauri app")
|
||||
.run(|app_handle, event| match event {
|
||||
tauri::RunEvent::ExitRequested { api, .. } => {
|
||||
hide_window(&app_handle).expect("Failed to hide window");
|
||||
api.prevent_exit();
|
||||
}
|
||||
_ => {}
|
||||
});
|
||||
}
|
||||
|
||||
fn init(app_handle: tauri::AppHandle) -> Result<()> {
|
||||
@ -616,9 +601,10 @@ fn get_window(handle: &tauri::AppHandle) -> Option<tauri::Window> {
|
||||
#[cfg(not(target_os = "macos"))]
|
||||
fn create_window(handle: &tauri::AppHandle) -> tauri::Result<tauri::Window> {
|
||||
log::info!("Creating window");
|
||||
let app_title = handle.package_info().name.clone();
|
||||
tauri::WindowBuilder::new(handle, "main", tauri::WindowUrl::App("index.html".into()))
|
||||
.resizable(true)
|
||||
.title(app_title())
|
||||
.title(app_title)
|
||||
.theme(Some(tauri::Theme::Dark))
|
||||
.min_inner_size(600.0, 300.0)
|
||||
.inner_size(800.0, 600.0)
|
||||
@ -630,7 +616,7 @@ fn create_window(handle: &tauri::AppHandle) -> tauri::Result<tauri::Window> {
|
||||
log::info!("Creating window");
|
||||
tauri::WindowBuilder::new(handle, "main", tauri::WindowUrl::App("index.html".into()))
|
||||
.resizable(true)
|
||||
.title(app_title())
|
||||
.title(handle.package_info().name.clone())
|
||||
.theme(Some(tauri::Theme::Dark))
|
||||
.min_inner_size(1024.0, 600.0)
|
||||
.inner_size(1024.0, 600.0)
|
||||
@ -643,7 +629,7 @@ fn hide_window(handle: &tauri::AppHandle) -> tauri::Result<()> {
|
||||
handle
|
||||
.tray_handle()
|
||||
.get_item("toggle")
|
||||
.set_title(format!("Show {}", app_title()))?;
|
||||
.set_title(format!("Show {}", handle.package_info().name))?;
|
||||
|
||||
match get_window(handle) {
|
||||
Some(window) => {
|
||||
|
@ -221,7 +221,6 @@ fn test_simple() -> Result<()> {
|
||||
offset: None,
|
||||
range: Range { start: 0, end: 10 },
|
||||
});
|
||||
println!("{:?}", search_result1);
|
||||
assert!(search_result1.is_ok());
|
||||
let search_result1 = search_result1.unwrap();
|
||||
assert_eq!(search_result1.total, 1);
|
||||
|
@ -14,7 +14,7 @@
|
||||
"all": false,
|
||||
"shell": {
|
||||
"all": false,
|
||||
"open": true
|
||||
"open": "^(https://)|(mailto:)?."
|
||||
},
|
||||
"dialog": {
|
||||
"all": false,
|
||||
|
@ -15,7 +15,7 @@
|
||||
"csp": {
|
||||
"default-src": "'self'",
|
||||
"img-src": "'self' asset: https://asset.localhost",
|
||||
"connect-src": "'self' https://eu.posthog.com https://app.gitbutler.com ws://localhost:7703",
|
||||
"connect-src": "'self' https://eu.posthog.com https://app.gitbutler.com https://o4504644069687296.ingest.sentry.io ws://localhost:7703",
|
||||
"script-src": "'self' https://eu.posthog.com",
|
||||
"style-src": "'self' 'unsafe-inline'"
|
||||
}
|
||||
|
@ -95,5 +95,5 @@ input:focus {
|
||||
padding: 24px;
|
||||
}
|
||||
.xterm-screen {
|
||||
wdith: 100% !important;
|
||||
width: 100% !important;
|
||||
}
|
||||
|
@ -1,9 +1,19 @@
|
||||
import type { HandleClientError } from '@sveltejs/kit';
|
||||
import { handleErrorWithSentry, init } from '@sentry/sveltekit';
|
||||
import type { NavigationEvent } from '@sveltejs/kit';
|
||||
import { dev } from '$app/environment';
|
||||
import { log } from '$lib';
|
||||
|
||||
// This will catch errors in load functions from +page.ts files
|
||||
export const handleError = (({ error, event }: { error: any; event: any }) => {
|
||||
console.error(error, event);
|
||||
return {
|
||||
message: error.message
|
||||
};
|
||||
}) satisfies HandleClientError;
|
||||
init({
|
||||
enabled: !dev,
|
||||
dsn: 'https://9d407634d26b4d30b6a42d57a136d255@o4504644069687296.ingest.sentry.io/4504649768108032',
|
||||
environment: dev ? 'development' : 'production',
|
||||
tracesSampleRate: 1.0
|
||||
});
|
||||
|
||||
log.info(`sentry init`);
|
||||
|
||||
const myErrorHandler = ({ error, event }: { error: any; event: NavigationEvent }) => {
|
||||
console.error('An error occurred on the client side:', error, event);
|
||||
};
|
||||
|
||||
export const handleError = handleErrorWithSentry(myErrorHandler);
|
||||
|
2
src/lib/api/cloud/index.ts
Normal file
@ -0,0 +1,2 @@
|
||||
export { default as Api } from './api';
|
||||
export type { User, LoginToken, Project } from './api';
|
2
src/lib/api/index.ts
Normal file
@ -0,0 +1,2 @@
|
||||
export * from './ipc';
|
||||
export { Api as CloudApi, type User, type LoginToken } from './cloud';
|
@ -1,8 +1,7 @@
|
||||
import { log } from '$lib';
|
||||
import { invoke } from '@tauri-apps/api';
|
||||
import { appWindow } from '@tauri-apps/api/window';
|
||||
import { writable, type Readable } from 'svelte/store';
|
||||
import { clone } from './utils';
|
||||
import { clone } from '$lib/utils';
|
||||
import { writable } from 'svelte/store';
|
||||
|
||||
export type OperationDelete = { delete: [number, number] };
|
||||
export type OperationInsert = { insert: [number, string] };
|
||||
@ -19,11 +18,6 @@ export namespace Operation {
|
||||
|
||||
export type Delta = { timestampMs: number; operations: Operation[] };
|
||||
|
||||
export type DeltasEvent = {
|
||||
deltas: Delta[];
|
||||
filePath: string;
|
||||
};
|
||||
|
||||
const cache: Record<string, Record<string, Promise<Record<string, Delta[]>>>> = {};
|
||||
|
||||
export const list = async (params: { projectId: string; sessionId: string; paths?: string[] }) => {
|
||||
@ -53,22 +47,27 @@ export const list = async (params: { projectId: string; sessionId: string; paths
|
||||
);
|
||||
};
|
||||
|
||||
export default async (params: { projectId: string; sessionId: string }) => {
|
||||
const init = await list(params);
|
||||
|
||||
const store = writable<Record<string, Delta[]>>(init);
|
||||
appWindow.listen<DeltasEvent>(
|
||||
export const subscribe = (
|
||||
params: { projectId: string; sessionId: string },
|
||||
callback: (params: {
|
||||
projectId: string;
|
||||
sessionId: string;
|
||||
filePath: string;
|
||||
deltas: Delta[];
|
||||
}) => Promise<void> | void
|
||||
) =>
|
||||
appWindow.listen<{ deltas: Delta[]; filePath: string }>(
|
||||
`project://${params.projectId}/sessions/${params.sessionId}/deltas`,
|
||||
(event) => {
|
||||
log.info(
|
||||
`Received deltas for ${params.projectId}, ${params.sessionId}, ${event.payload.filePath}`
|
||||
);
|
||||
store.update((deltas) => ({
|
||||
...deltas,
|
||||
[event.payload.filePath]: event.payload.deltas
|
||||
}));
|
||||
}
|
||||
(event) => callback({ ...params, ...event.payload })
|
||||
);
|
||||
|
||||
return store as Readable<Record<string, Delta[]>>;
|
||||
export const Deltas = async (params: { projectId: string; sessionId: string }) => {
|
||||
const store = writable(await list(params));
|
||||
subscribe(params, ({ filePath, deltas }) => {
|
||||
store.update((deltasCache) => {
|
||||
deltasCache[filePath] = deltas;
|
||||
return deltasCache;
|
||||
});
|
||||
});
|
||||
return { subscribe: store.subscribe };
|
||||
};
|
31
src/lib/api/ipc/files.ts
Normal file
@ -0,0 +1,31 @@
|
||||
import { invoke } from '@tauri-apps/api';
|
||||
import { clone } from '$lib/utils';
|
||||
|
||||
const cache: Record<string, Record<string, Promise<Record<string, string>>>> = {};
|
||||
|
||||
export const list = async (params: { projectId: string; sessionId: string; paths?: string[] }) => {
|
||||
const sessionFilesCache = cache[params.projectId] || {};
|
||||
if (params.sessionId in sessionFilesCache) {
|
||||
return sessionFilesCache[params.sessionId].then((files) =>
|
||||
Object.fromEntries(
|
||||
Object.entries(clone(files)).filter(([path]) =>
|
||||
params.paths ? params.paths.includes(path) : true
|
||||
)
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
const promise = invoke<Record<string, string>>('list_session_files', {
|
||||
sessionId: params.sessionId,
|
||||
projectId: params.projectId
|
||||
});
|
||||
sessionFilesCache[params.sessionId] = promise;
|
||||
cache[params.projectId] = sessionFilesCache;
|
||||
return promise.then((files) =>
|
||||
Object.fromEntries(
|
||||
Object.entries(clone(files)).filter(([path]) =>
|
||||
params.paths ? params.paths.includes(path) : true
|
||||
)
|
||||
)
|
||||
);
|
||||
};
|
28
src/lib/api/ipc/git/activities.ts
Normal file
@ -0,0 +1,28 @@
|
||||
import { invoke } from '@tauri-apps/api';
|
||||
import { appWindow } from '@tauri-apps/api/window';
|
||||
import { get, writable } from 'svelte/store';
|
||||
|
||||
export type Activity = {
|
||||
type: string;
|
||||
timestampMs: number;
|
||||
message: string;
|
||||
};
|
||||
|
||||
export const list = (params: { projectId: string; startTimeMs?: number }) =>
|
||||
invoke<Activity[]>('git_activity', params);
|
||||
|
||||
export const subscribe = (
|
||||
params: { projectId: string },
|
||||
callback: (params: { projectId: string }) => Promise<void> | void
|
||||
) => appWindow.listen(`project://${params.projectId}/git/activity`, () => callback(params));
|
||||
|
||||
export const Activities = async (params: { projectId: string }) => {
|
||||
const store = writable<Activity[]>(await list(params));
|
||||
subscribe(params, async () => {
|
||||
const activity = get(store);
|
||||
const startTimeMs = activity.at(-1)?.timestampMs;
|
||||
const newActivities = await list({ projectId: params.projectId, startTimeMs });
|
||||
store.update((activities) => [...activities, ...newActivities]);
|
||||
});
|
||||
return { subscribe: store.subscribe };
|
||||
};
|
13
src/lib/api/ipc/git/diffs.ts
Normal file
@ -0,0 +1,13 @@
|
||||
import { invoke } from '@tauri-apps/api';
|
||||
import { writable } from 'svelte/store';
|
||||
import { sessions, git } from '$lib/api';
|
||||
|
||||
const list = (params: { projectId: string }) =>
|
||||
invoke<Record<string, string>>('git_wd_diff', params);
|
||||
|
||||
export const Diffs = async (params: { projectId: string }) => {
|
||||
const store = writable(await list(params));
|
||||
git.activities.subscribe(params, ({ projectId }) => list({ projectId }).then(store.set));
|
||||
sessions.subscribe(params, () => list(params).then(store.set));
|
||||
return { subscribe: store.subscribe };
|
||||
};
|
19
src/lib/api/ipc/git/heads.ts
Normal file
@ -0,0 +1,19 @@
|
||||
import { invoke } from '@tauri-apps/api';
|
||||
import { appWindow } from '@tauri-apps/api/window';
|
||||
import { derived, writable } from 'svelte/store';
|
||||
|
||||
export const get = (params: { projectId: string }) => invoke<string>('git_head', params);
|
||||
|
||||
export const subscribe = (
|
||||
params: { projectId: string },
|
||||
callback: (params: { projectId: string; head: string }) => Promise<void> | void
|
||||
) =>
|
||||
appWindow.listen<{ head: string }>(`project://${params.projectId}/git/head`, (event) =>
|
||||
callback({ ...params, ...event.payload })
|
||||
);
|
||||
|
||||
export const Head = async (params: { projectId: string }) => {
|
||||
const store = writable(await get(params));
|
||||
subscribe(params, ({ head }) => store.set(head));
|
||||
return derived(store, (head) => head.replace('refs/heads/', ''));
|
||||
};
|
@ -1,7 +1,12 @@
|
||||
import { invoke } from '@tauri-apps/api';
|
||||
export * as statuses from './statuses';
|
||||
export { Status } from './statuses';
|
||||
export * as activities from './activities';
|
||||
export type { Activity } from './activities';
|
||||
export * as heads from './heads';
|
||||
export * as diffs from './diffs';
|
||||
export * as indexes from './indexes';
|
||||
|
||||
export { default as statuses } from './statuses';
|
||||
export { default as activity } from './activity';
|
||||
import { invoke } from '@tauri-apps/api';
|
||||
|
||||
export const commit = (params: { projectId: string; message: string; push: boolean }) =>
|
||||
invoke<boolean>('git_commit', params);
|
6
src/lib/api/ipc/git/indexes.ts
Normal file
@ -0,0 +1,6 @@
|
||||
import { appWindow } from '@tauri-apps/api/window';
|
||||
|
||||
export const subscribe = (
|
||||
params: { projectId: string },
|
||||
callback: (params: { projectId: string }) => Promise<void>
|
||||
) => appWindow.listen(`project://${params.projectId}/git/activity`, () => callback({ ...params }));
|
@ -1,6 +1,6 @@
|
||||
import { invoke } from '@tauri-apps/api';
|
||||
import { appWindow } from '@tauri-apps/api/window';
|
||||
import { writable, type Readable } from 'svelte/store';
|
||||
import { writable } from 'svelte/store';
|
||||
import { sessions, git } from '$lib/api';
|
||||
|
||||
type FileStatus = 'added' | 'modified' | 'deleted' | 'renamed' | 'typeChange' | 'other';
|
||||
|
||||
@ -16,22 +16,13 @@ export namespace Status {
|
||||
'unstaged' in status && status.unstaged !== null;
|
||||
}
|
||||
|
||||
const list = (params: { projectId: string }) =>
|
||||
export const list = (params: { projectId: string }) =>
|
||||
invoke<Record<string, Status>>('git_status', params);
|
||||
|
||||
export default async (params: { projectId: string }) => {
|
||||
const statuses = await list(params);
|
||||
const store = writable(statuses);
|
||||
|
||||
[
|
||||
`project://${params.projectId}/git/index`,
|
||||
`project://${params.projectId}/git/activity`,
|
||||
`project://${params.projectId}/sessions`
|
||||
].forEach((eventName) => {
|
||||
appWindow.listen(eventName, async () => {
|
||||
store.set(await list(params));
|
||||
});
|
||||
});
|
||||
|
||||
return store as Readable<Record<string, Status>>;
|
||||
export const Statuses = async (params: { projectId: string }) => {
|
||||
const store = writable(await list(params));
|
||||
sessions.subscribe(params, () => list(params).then(store.set));
|
||||
git.activities.subscribe(params, () => list(params).then(store.set));
|
||||
git.indexes.subscribe(params, () => list(params).then(store.set));
|
||||
return { subscribe: store.subscribe };
|
||||
};
|
12
src/lib/api/ipc/index.ts
Normal file
@ -0,0 +1,12 @@
|
||||
export * as git from './git';
|
||||
export { Status, type Activity } from './git';
|
||||
export * as deltas from './deltas';
|
||||
export { type Delta, Operation } from './deltas';
|
||||
export * as sessions from './sessions';
|
||||
export { Session } from './sessions';
|
||||
export * as users from './users';
|
||||
export * as projects from './projects';
|
||||
export type { Project } from './projects';
|
||||
export * as searchResults from './search';
|
||||
export { type SearchResult } from './search';
|
||||
export * as files from './files';
|
@ -1,6 +1,6 @@
|
||||
import { invoke } from '@tauri-apps/api';
|
||||
import { derived, writable } from 'svelte/store';
|
||||
import type { Project as ApiProject } from '$lib/api';
|
||||
import type { Project as ApiProject } from '$lib/api/cloud';
|
||||
import { derived, readable, writable } from 'svelte/store';
|
||||
|
||||
export type Project = {
|
||||
id: string;
|
||||
@ -9,9 +9,9 @@ export type Project = {
|
||||
api: ApiProject & { sync: boolean };
|
||||
};
|
||||
|
||||
const list = () => invoke<Project[]>('list_projects');
|
||||
export const list = () => invoke<Project[]>('list_projects');
|
||||
|
||||
const update = (params: {
|
||||
export const update = (params: {
|
||||
project: {
|
||||
id: string;
|
||||
title?: string;
|
||||
@ -19,14 +19,12 @@ const update = (params: {
|
||||
};
|
||||
}) => invoke<Project>('update_project', params);
|
||||
|
||||
const add = (params: { path: string }) => invoke<Project>('add_project', params);
|
||||
export const add = (params: { path: string }) => invoke<Project>('add_project', params);
|
||||
|
||||
const del = (params: { id: string }) => invoke('delete_project', params);
|
||||
|
||||
export default async () => {
|
||||
const init = await list();
|
||||
const store = writable<Project[]>(init);
|
||||
export const del = (params: { id: string }) => invoke('delete_project', params);
|
||||
|
||||
export const Projects = async () => {
|
||||
const store = writable(await list());
|
||||
return {
|
||||
subscribe: store.subscribe,
|
||||
get: (id: string) => {
|
@ -10,7 +10,7 @@ export type SearchResult = {
|
||||
highlighted: string[];
|
||||
};
|
||||
|
||||
export const search = (params: {
|
||||
export const list = (params: {
|
||||
projectId: string;
|
||||
query: string;
|
||||
limit?: number;
|
63
src/lib/api/ipc/sessions.ts
Normal file
@ -0,0 +1,63 @@
|
||||
import { invoke } from '@tauri-apps/api';
|
||||
import { appWindow } from '@tauri-apps/api/window';
|
||||
import { clone } from '$lib/utils';
|
||||
import { writable } from 'svelte/store';
|
||||
|
||||
export namespace Session {
|
||||
export const within = (session: Session | undefined, timestampMs: number) => {
|
||||
if (!session) return false;
|
||||
const { startTimestampMs, lastTimestampMs } = session.meta;
|
||||
return startTimestampMs <= timestampMs && timestampMs <= lastTimestampMs;
|
||||
};
|
||||
}
|
||||
|
||||
export type Session = {
|
||||
id: string;
|
||||
hash?: string;
|
||||
meta: {
|
||||
startTimestampMs: number;
|
||||
lastTimestampMs: number;
|
||||
branch?: string;
|
||||
commit?: string;
|
||||
};
|
||||
};
|
||||
const cache: Record<string, Promise<Session[]>> = {};
|
||||
|
||||
export const list = async (params: { projectId: string; earliestTimestampMs?: number }) => {
|
||||
if (params.projectId in cache) {
|
||||
return cache[params.projectId].then((sessions) =>
|
||||
clone(sessions).filter((s) =>
|
||||
params.earliestTimestampMs ? s.meta.startTimestampMs >= params.earliestTimestampMs : true
|
||||
)
|
||||
);
|
||||
}
|
||||
cache[params.projectId] = invoke<Session[]>('list_sessions', {
|
||||
projectId: params.projectId
|
||||
});
|
||||
return cache[params.projectId].then((sessions) =>
|
||||
clone(sessions).filter((s) =>
|
||||
params.earliestTimestampMs ? s.meta.startTimestampMs >= params.earliestTimestampMs : true
|
||||
)
|
||||
);
|
||||
};
|
||||
|
||||
export const subscribe = (
|
||||
params: { projectId: string },
|
||||
callback: (params: { projectId: string; session: Session }) => Promise<void> | void
|
||||
) =>
|
||||
appWindow.listen<Session>(`project://${params.projectId}/sessions`, async (event) =>
|
||||
callback({ ...params, session: event.payload })
|
||||
);
|
||||
|
||||
export const Sessions = async (params: { projectId: string }) => {
|
||||
const store = writable(await list(params));
|
||||
subscribe(params, ({ session }) => {
|
||||
store.update((sessions) => {
|
||||
const index = sessions.findIndex((s) => s.id === session.id);
|
||||
if (index === -1) return [...sessions, session];
|
||||
sessions[index] = session;
|
||||
return sessions;
|
||||
});
|
||||
});
|
||||
return { subscribe: store.subscribe };
|
||||
};
|
@ -1,18 +1,15 @@
|
||||
import type { User } from '$lib/api';
|
||||
import { writable } from 'svelte/store';
|
||||
import { invoke } from '@tauri-apps/api';
|
||||
import { writable } from 'svelte/store';
|
||||
|
||||
const get = () => invoke<User | undefined>('get_user');
|
||||
export const get = () => invoke<User | undefined>('get_user');
|
||||
|
||||
const set = (params: { user: User }) => invoke<void>('set_user', params);
|
||||
export const set = (params: { user: User }) => invoke<void>('set_user', params);
|
||||
|
||||
const del = () => invoke<void>('delete_user');
|
||||
export const del = () => invoke<void>('delete_user');
|
||||
|
||||
export default async () => {
|
||||
const store = writable<User | undefined>(undefined);
|
||||
|
||||
const init = await get();
|
||||
store.set(init);
|
||||
export const CurrentUser = async () => {
|
||||
const store = writable<User | undefined>(await get());
|
||||
return {
|
||||
subscribe: store.subscribe,
|
||||
set: async (user: User) => {
|
@ -1,5 +1,5 @@
|
||||
<script lang="ts">
|
||||
import type { Project } from '$lib/projects';
|
||||
import type { Project } from '$lib/api';
|
||||
import type { Readable } from 'svelte/store';
|
||||
import { IconHome } from './icons';
|
||||
import { Tooltip } from '$lib/components';
|
||||
|
@ -94,6 +94,11 @@
|
||||
text-underline-offset: 3px;
|
||||
}
|
||||
|
||||
a:focus,
|
||||
button:focus {
|
||||
@apply outline-none;
|
||||
}
|
||||
|
||||
.basic {
|
||||
@apply text-zinc-300;
|
||||
}
|
||||
|
@ -2,7 +2,7 @@
|
||||
// Use of this source code is governed by a BSD-style license that can be
|
||||
// found in the LICENSE file.
|
||||
|
||||
import { HighlightStyle, LanguageSupport } from '@codemirror/language';
|
||||
import { HighlightStyle, type LanguageSupport } from '@codemirror/language';
|
||||
import { tags, highlightTree } from '@lezer/highlight';
|
||||
import { NodeType, Tree } from '@lezer/common';
|
||||
import { javascript } from '@codemirror/lang-javascript';
|
||||
|
@ -1,5 +1,5 @@
|
||||
<script lang="ts">
|
||||
import { type Delta, Operation } from '$lib/deltas';
|
||||
import { type Delta, Operation } from '$lib/api';
|
||||
import { lineDiff } from './diff';
|
||||
import { create } from './CodeHighlighter';
|
||||
import { buildDiffRows, documentMap, RowType, type Row } from './renderer';
|
||||
|
@ -1,14 +1,16 @@
|
||||
<script lang="ts">
|
||||
import tinykeys from 'tinykeys';
|
||||
import type { Project } from '$lib/projects';
|
||||
import type { Project } from '$lib/api';
|
||||
import { derived, readable, writable, type Readable } from 'svelte/store';
|
||||
import { Modal } from '$lib/components';
|
||||
import listAvailableCommands, { Action, type Group } from './commands';
|
||||
import { goto } from '$app/navigation';
|
||||
import { onMount } from 'svelte';
|
||||
import { open } from '@tauri-apps/api/shell';
|
||||
import { IconExternalLink } from '../icons';
|
||||
|
||||
export let projects: Readable<Project[]>;
|
||||
export let addProject: (params: { path: string }) => Promise<Project>;
|
||||
export let project = readable<Project | undefined>(undefined);
|
||||
|
||||
const input = writable('');
|
||||
@ -21,37 +23,74 @@
|
||||
([projects, project, input, scopeToProject, selectedGroup]) =>
|
||||
selectedGroup !== undefined
|
||||
? [selectedGroup]
|
||||
: listAvailableCommands({ projects, project: scopeToProject ? project : undefined, input })
|
||||
: listAvailableCommands({
|
||||
addProject,
|
||||
projects,
|
||||
project: scopeToProject ? project : undefined,
|
||||
input
|
||||
})
|
||||
);
|
||||
|
||||
let selection = [0, 0] as [number, number];
|
||||
const selection = writable<[number, number]>([0, 0]);
|
||||
|
||||
commandGroups.subscribe((groups) => {
|
||||
const newGroupIndex = Math.min(selection[0], groups.length - 1);
|
||||
const newGroupIndex = Math.min($selection[0], groups.length - 1);
|
||||
Promise.resolve(groups[newGroupIndex]).then((group) => {
|
||||
const newCommandIndex = Math.min(selection[1], group.commands.length - 1);
|
||||
selection = [newGroupIndex, newCommandIndex];
|
||||
const newCommandIndex = Math.min($selection[1], group.commands.length - 1);
|
||||
$selection = [newGroupIndex, newCommandIndex];
|
||||
});
|
||||
});
|
||||
|
||||
const selectNextCommand = () => {
|
||||
selection.subscribe(() => {
|
||||
const selected = document.querySelector('.selected');
|
||||
if (selected) {
|
||||
selected.scrollIntoView({ block: 'nearest', behavior: 'smooth' });
|
||||
}
|
||||
});
|
||||
|
||||
const selectNextCommand = async () => {
|
||||
if (!modal?.isOpen()) return;
|
||||
Promise.resolve($commandGroups[selection[0]]).then((group) => {
|
||||
if (selection[1] < group.commands.length - 1) {
|
||||
selection = [selection[0], selection[1] + 1];
|
||||
} else if (selection[0] < $commandGroups.length - 1) {
|
||||
selection = [selection[0] + 1, 0];
|
||||
}
|
||||
});
|
||||
const group = await Promise.resolve($commandGroups[$selection[0]]);
|
||||
const nextCommandIndex = group.commands.findIndex((_command, index) => index > $selection[1]);
|
||||
if (nextCommandIndex > -1) {
|
||||
$selection = [$selection[0], nextCommandIndex];
|
||||
} else {
|
||||
await selectNextGroup();
|
||||
}
|
||||
};
|
||||
|
||||
const selectPreviousCommand = () => {
|
||||
const selectNextGroup = async () => {
|
||||
if (!modal?.isOpen()) return;
|
||||
if (selection[1] > 0) {
|
||||
selection = [selection[0], selection[1] - 1];
|
||||
} else if (selection[0] > 0) {
|
||||
Promise.resolve($commandGroups[selection[0] - 1]).then((previousGroup) => {
|
||||
selection = [selection[0] - 1, previousGroup.commands.length - 1];
|
||||
});
|
||||
const groups = await Promise.all($commandGroups.map((group) => Promise.resolve(group)));
|
||||
const nextGroupIndex = groups.findIndex(
|
||||
(group, index) => index > $selection[0] && group.commands.length > 0
|
||||
);
|
||||
if (nextGroupIndex > -1) {
|
||||
$selection = [nextGroupIndex, 0];
|
||||
}
|
||||
};
|
||||
|
||||
const selectPreviousCommand = async () => {
|
||||
if (!modal?.isOpen()) return;
|
||||
const group = await Promise.resolve($commandGroups[$selection[0]]);
|
||||
const previousCommandIndex = group.commands
|
||||
.map((_command, index) => index < $selection[1])
|
||||
.lastIndexOf(true);
|
||||
if (previousCommandIndex > -1) {
|
||||
$selection = [$selection[0], previousCommandIndex];
|
||||
} else {
|
||||
await selectPreviousGroup();
|
||||
}
|
||||
};
|
||||
|
||||
const selectPreviousGroup = async () => {
|
||||
if (!modal?.isOpen()) return;
|
||||
const groups = await Promise.all($commandGroups.map((group) => Promise.resolve(group)));
|
||||
const previousGroupIndex = groups
|
||||
.map((group, index) => index < $selection[0] && group.commands.length > 0)
|
||||
.lastIndexOf(true);
|
||||
if (previousGroupIndex > -1) {
|
||||
$selection = [previousGroupIndex, groups[previousGroupIndex].commands.length - 1];
|
||||
}
|
||||
};
|
||||
|
||||
@ -61,9 +100,12 @@
|
||||
action.href.startsWith('http') || action.href.startsWith('mailto')
|
||||
? open(action.href)
|
||||
: goto(action.href);
|
||||
modal?.hide();
|
||||
modal?.close();
|
||||
} else if (Action.isGroup(action)) {
|
||||
selectedGroup.set(action);
|
||||
} else if (Action.isRun(action)) {
|
||||
action();
|
||||
modal?.close();
|
||||
}
|
||||
scopeToProject.set(!!$project);
|
||||
};
|
||||
@ -74,6 +116,7 @@
|
||||
input.set('');
|
||||
scopeToProject.set(!!$project);
|
||||
selectedGroup.set(undefined);
|
||||
$selection = [0, 0];
|
||||
};
|
||||
|
||||
export const show = () => {
|
||||
@ -97,8 +140,8 @@
|
||||
'Control+p': selectPreviousCommand,
|
||||
Enter: () => {
|
||||
if (!modal?.isOpen()) return;
|
||||
Promise.resolve($commandGroups[selection[0]]).then((group) =>
|
||||
trigger(group.commands[selection[1]].action)
|
||||
Promise.resolve($commandGroups[$selection[0]]).then((group) =>
|
||||
trigger(group.commands[$selection[1]].action)
|
||||
);
|
||||
}
|
||||
})
|
||||
@ -115,11 +158,9 @@
|
||||
if (command.hotkey) {
|
||||
unregisterCommandHotkeys.push(
|
||||
tinykeys(window, {
|
||||
[command.hotkey]: (event: KeyboardEvent) => {
|
||||
const target = event.target as HTMLElement;
|
||||
if (target.tagName === 'INPUT' || target.tagName === 'TEXTAREA') return;
|
||||
// only trigger if the modal is visible
|
||||
modal?.isOpen() && trigger(command.action);
|
||||
[command.hotkey]: () => {
|
||||
if (!modal?.isOpen()) return;
|
||||
trigger(command.action);
|
||||
}
|
||||
})
|
||||
);
|
||||
@ -132,78 +173,90 @@
|
||||
</script>
|
||||
|
||||
<Modal bind:this={modal}>
|
||||
<div
|
||||
class="command-palette flex max-h-[400px] min-h-[40px] w-[640px] flex-col rounded text-zinc-400"
|
||||
>
|
||||
<!-- Search input area -->
|
||||
<header class="search-input-container flex items-center border-b border-zinc-400/20 py-2">
|
||||
<div class="ml-4 mr-2 flex w-full items-center gap-1 text-lg text-zinc-300">
|
||||
<!-- Project scope -->
|
||||
{#if $scopeToProject && $project}
|
||||
<span class="py-2 font-semibold">
|
||||
{$project.title}
|
||||
</span>
|
||||
<span>/</span>
|
||||
{/if}
|
||||
{#if $selectedGroup}
|
||||
<span class="font-semibold">
|
||||
{$selectedGroup.title}
|
||||
</span>
|
||||
{:else}
|
||||
<!-- svelte-ignore a11y-autofocus -->
|
||||
<input
|
||||
spellcheck="false"
|
||||
class="command-palette-input-field"
|
||||
bind:value={$input}
|
||||
type="text"
|
||||
autofocus
|
||||
placeholder={!$project
|
||||
? 'Search for repositories'
|
||||
: 'Search for commands, files and code changes...'}
|
||||
/>
|
||||
{/if}
|
||||
</div>
|
||||
</header>
|
||||
<div class="h-[400px]">
|
||||
<div
|
||||
class="command-palette flex max-h-[400px] min-h-[40px] w-[640px] flex-col rounded rounded-lg border-[0.5px] border-[#3F3F3f] bg-zinc-900/70 p-0 text-zinc-400 shadow-lg backdrop-blur-lg"
|
||||
>
|
||||
<!-- Search input area -->
|
||||
<header class="search-input-container flex items-center border-b border-zinc-400/20 py-2">
|
||||
<div class="ml-4 mr-2 flex w-full items-center gap-1 text-lg text-zinc-300">
|
||||
<!-- Project scope -->
|
||||
{#if $scopeToProject && $project}
|
||||
<span class="py-2 font-semibold">
|
||||
{$project.title}
|
||||
</span>
|
||||
<span>/</span>
|
||||
{/if}
|
||||
{#if $selectedGroup}
|
||||
<span class="font-semibold">
|
||||
{$selectedGroup.title}
|
||||
</span>
|
||||
{:else}
|
||||
<!-- svelte-ignore a11y-autofocus -->
|
||||
<input
|
||||
spellcheck="false"
|
||||
class="command-palette-input-field"
|
||||
bind:value={$input}
|
||||
type="text"
|
||||
autofocus
|
||||
placeholder={!$project
|
||||
? 'Search your projects'
|
||||
: 'Search for commands, files and code changes'}
|
||||
/>
|
||||
{/if}
|
||||
</div>
|
||||
</header>
|
||||
|
||||
<!-- Command list -->
|
||||
<ul class="command-pallete-content-container flex-auto overflow-y-auto pb-2">
|
||||
{#each $commandGroups as group, groupIdx}
|
||||
{#await group then group}
|
||||
<li class="w-full cursor-default select-none px-2">
|
||||
<header class="command-palette-section-header result-section-header">
|
||||
<span>{group.title}</span>
|
||||
{#if group.description}
|
||||
<span class="ml-2 font-light italic text-zinc-300/70">({group.description})</span>
|
||||
{/if}
|
||||
</header>
|
||||
<!-- Command list -->
|
||||
<ul class="command-pallete-content-container flex-auto overflow-y-auto pb-2">
|
||||
{#each $commandGroups as group, groupIdx}
|
||||
{#await group then group}
|
||||
<li
|
||||
class="w-full cursor-default select-none px-2"
|
||||
class:hidden={group.commands.length === 0}
|
||||
>
|
||||
<header class="command-palette-section-header result-section-header">
|
||||
<span>{group.title}</span>
|
||||
{#if group.description}
|
||||
<span class="ml-2 font-light italic text-zinc-300/70">({group.description})</span>
|
||||
{/if}
|
||||
</header>
|
||||
|
||||
<ul class="quick-command-list flex flex-col text-zinc-300">
|
||||
{#each group.commands as command, commandIdx}
|
||||
<li
|
||||
class="quick-command-item flex w-full cursor-default rounded-lg"
|
||||
class:selected={selection[0] === groupIdx && selection[1] === commandIdx}
|
||||
>
|
||||
<button
|
||||
on:mouseover={() => (selection = [groupIdx, commandIdx])}
|
||||
on:focus={() => (selection = [groupIdx, commandIdx])}
|
||||
on:click={() => trigger(command.action)}
|
||||
class="text-color-500 flex w-full items-center gap-2 rounded-lg p-2 px-2 outline-none"
|
||||
<ul class="quick-command-list flex flex-col text-zinc-300">
|
||||
{#each group.commands as command, commandIdx}
|
||||
<li
|
||||
class="quick-command-item flex w-full cursor-default rounded-lg"
|
||||
class:selected={$selection[0] === groupIdx && $selection[1] === commandIdx}
|
||||
>
|
||||
<svelte:component this={command.icon} class="icon h-5 w-5 text-zinc-500 " />
|
||||
<span class="quick-command flex-1 text-left font-medium">{command.title}</span>
|
||||
{#if command.hotkey}
|
||||
{#each command.hotkey.split('+') as key}
|
||||
<span class="quick-command-key">{key}</span>
|
||||
{/each}
|
||||
{/if}
|
||||
</button>
|
||||
</li>
|
||||
{/each}
|
||||
</ul>
|
||||
</li>
|
||||
{/await}
|
||||
{/each}
|
||||
</ul>
|
||||
<button
|
||||
on:mouseover={() => ($selection = [groupIdx, commandIdx])}
|
||||
on:focus={() => ($selection = [groupIdx, commandIdx])}
|
||||
on:click={() => trigger(command.action)}
|
||||
class="text-color-500 flex w-full items-center gap-2 rounded-lg p-2 px-2 outline-none"
|
||||
>
|
||||
<svelte:component this={command.icon} class="icon h-5 w-5 text-zinc-500 " />
|
||||
<span
|
||||
class="quick-command flex flex-1 items-center gap-1 text-left font-medium"
|
||||
>
|
||||
{command.title}
|
||||
{#if Action.isExternalLink(command.action)}
|
||||
<IconExternalLink class="h-4 w-4 text-zinc-600" />
|
||||
{/if}
|
||||
</span>
|
||||
{#if command.hotkey}
|
||||
{#each command.hotkey.replace('Meta', '⌘').split('+') as key}
|
||||
<span class="quick-command-key">{key}</span>
|
||||
{/each}
|
||||
{/if}
|
||||
</button>
|
||||
</li>
|
||||
{/each}
|
||||
</ul>
|
||||
</li>
|
||||
{/await}
|
||||
{/each}
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
</Modal>
|
||||
|
||||
|
@ -1,22 +1,38 @@
|
||||
import type { Project } from '$lib/projects';
|
||||
import { GitCommitIcon, IconFile, IconProject, IconTerminal, RewindIcon, FileIcon } from '../icons';
|
||||
import { matchFiles } from '$lib/git';
|
||||
import { type Project, git } from '$lib/api';
|
||||
import { open } from '@tauri-apps/api/dialog';
|
||||
import { toasts } from '$lib';
|
||||
import {
|
||||
IconGitCommit,
|
||||
IconFile,
|
||||
IconFeedback,
|
||||
IconProject,
|
||||
IconTerminal,
|
||||
IconSettings,
|
||||
IconAdjustmentsHorizontal,
|
||||
IconDiscord,
|
||||
IconSearch,
|
||||
IconRewind
|
||||
} from '../icons';
|
||||
import type { SvelteComponent } from 'svelte';
|
||||
import { format, startOfISOWeek, startOfMonth, subDays, subMonths, subWeeks } from 'date-fns';
|
||||
|
||||
type ActionLink = {
|
||||
href: string;
|
||||
};
|
||||
|
||||
type ActionRun = () => void;
|
||||
|
||||
interface Newable<ReturnType> {
|
||||
new (...args: any[]): ReturnType;
|
||||
}
|
||||
|
||||
export type Action = ActionLink | Group;
|
||||
export type Action = ActionLink | Group | ActionRun;
|
||||
|
||||
export namespace Action {
|
||||
export const isLink = (action: Action): action is ActionLink => 'href' in action;
|
||||
export const isExternalLink = (action: Action): action is ActionLink =>
|
||||
isLink(action) && (action.href.startsWith('http') || action.href.startsWith('mailto'));
|
||||
export const isGroup = (action: Action): action is Group => 'commands' in action;
|
||||
export const isRun = (action: Action): action is ActionRun => typeof action === 'function';
|
||||
}
|
||||
|
||||
export type Command = {
|
||||
@ -32,98 +48,105 @@ export type Group = {
|
||||
commands: Command[];
|
||||
};
|
||||
|
||||
const goToProjectGroup = ({ projects, input }: { projects: Project[]; input: string }): Group => ({
|
||||
title: 'Go to project',
|
||||
commands: projects
|
||||
.map((project) => ({
|
||||
title: project.title,
|
||||
action: {
|
||||
href: `/projects/${project.id}/`
|
||||
},
|
||||
icon: IconProject
|
||||
}))
|
||||
.filter(({ title }) => input.length === 0 || title.toLowerCase().includes(input.toLowerCase()))
|
||||
});
|
||||
|
||||
const actionsGroup = ({ project, input }: { project: Project; input: string }): Group => ({
|
||||
title: 'Actions',
|
||||
const projectsGroup = ({
|
||||
addProject,
|
||||
projects,
|
||||
input
|
||||
}: {
|
||||
addProject: (params: { path: string }) => Promise<Project>;
|
||||
projects: Project[];
|
||||
input: string;
|
||||
}): Group => ({
|
||||
title: 'Projects',
|
||||
commands: [
|
||||
{
|
||||
title: 'Commit',
|
||||
hotkey: 'Shift+C',
|
||||
action: {
|
||||
href: `/projects/${project.id}/commit/`
|
||||
},
|
||||
icon: GitCommitIcon
|
||||
title: 'New project...',
|
||||
hotkey: 'Meta+Shift+N',
|
||||
icon: IconProject,
|
||||
action: async () => {
|
||||
const selectedPath = await open({
|
||||
directory: true,
|
||||
recursive: true
|
||||
});
|
||||
if (selectedPath === null) return;
|
||||
if (Array.isArray(selectedPath) && selectedPath.length !== 1) return;
|
||||
const projectPath = Array.isArray(selectedPath) ? selectedPath[0] : selectedPath;
|
||||
|
||||
try {
|
||||
addProject({ path: projectPath });
|
||||
} catch (e: any) {
|
||||
toasts.error(e.message);
|
||||
}
|
||||
}
|
||||
},
|
||||
...projects
|
||||
.filter(
|
||||
({ title }) => input.length === 0 || title.toLowerCase().includes(input.toLowerCase())
|
||||
)
|
||||
.map((project, i) => ({
|
||||
title: project.title,
|
||||
hotkey: `Meta+${i + 1}`,
|
||||
action: {
|
||||
href: `/projects/${project.id}/`
|
||||
},
|
||||
icon: IconProject
|
||||
})),
|
||||
{
|
||||
title: 'Terminal',
|
||||
hotkey: 'Shift+T',
|
||||
title: 'Search all repositories',
|
||||
action: {
|
||||
href: `/projects/${project?.id}/terminal/`
|
||||
href: '/'
|
||||
},
|
||||
icon: IconTerminal
|
||||
},
|
||||
{
|
||||
title: 'Replay History',
|
||||
action: {
|
||||
title: 'Replay working history',
|
||||
commands: [
|
||||
icon: IconSearch
|
||||
}
|
||||
].filter(({ title }) => input.length === 0 || title.toLowerCase().includes(input.toLowerCase()))
|
||||
});
|
||||
|
||||
const navigateGroup = ({ project, input }: { project?: Project; input: string }): Group => ({
|
||||
title: 'Navigate',
|
||||
commands: [
|
||||
...(project
|
||||
? [
|
||||
{
|
||||
title: 'Eralier today',
|
||||
icon: RewindIcon,
|
||||
hotkey: '1',
|
||||
title: 'Commit',
|
||||
hotkey: 'Meta+Shift+C',
|
||||
action: {
|
||||
href: `/projects/${project.id}/player/${format(new Date(), 'yyyy-MM-dd')}/`
|
||||
}
|
||||
href: `/projects/${project.id}/commit/`
|
||||
},
|
||||
icon: IconGitCommit
|
||||
},
|
||||
{
|
||||
title: 'Yesterday',
|
||||
icon: RewindIcon,
|
||||
hotkey: '2',
|
||||
title: 'Replay',
|
||||
hotkey: 'Meta+R',
|
||||
action: {
|
||||
href: `/projects/${project.id}/player/${format(
|
||||
subDays(new Date(), 1),
|
||||
'yyyy-MM-dd'
|
||||
)}/`
|
||||
}
|
||||
href: `/projects/${project.id}/player/`
|
||||
},
|
||||
icon: IconRewind
|
||||
},
|
||||
{
|
||||
title: 'The day before yesterday',
|
||||
icon: RewindIcon,
|
||||
hotkey: '3',
|
||||
title: 'Terminal',
|
||||
hotkey: 'Meta+T',
|
||||
action: {
|
||||
href: `/projects/${project.id}/player/${format(
|
||||
subDays(new Date(), 2),
|
||||
'yyyy-MM-dd'
|
||||
)}/`
|
||||
}
|
||||
href: `/projects/${project?.id}/terminal/`
|
||||
},
|
||||
icon: IconTerminal
|
||||
},
|
||||
{
|
||||
title: 'The beginning of last week',
|
||||
icon: RewindIcon,
|
||||
hotkey: '4',
|
||||
title: 'Project settings',
|
||||
hotkey: 'Meta+Shift+,',
|
||||
action: {
|
||||
href: `/projects/${project.id}/player/${format(
|
||||
startOfISOWeek(subWeeks(new Date(), 1)),
|
||||
'yyyy-MM-dd'
|
||||
)}/`
|
||||
}
|
||||
},
|
||||
{
|
||||
title: 'The beginning of last month',
|
||||
icon: RewindIcon,
|
||||
hotkey: '5',
|
||||
action: {
|
||||
href: `/projects/${project.id}/player/${format(
|
||||
startOfMonth(subMonths(new Date(), 1)),
|
||||
'yyyy-MM-dd'
|
||||
)}/`
|
||||
}
|
||||
href: `/projects/${project?.id}/settings/`
|
||||
},
|
||||
icon: IconSettings
|
||||
}
|
||||
]
|
||||
]
|
||||
: []),
|
||||
{
|
||||
title: 'Settings',
|
||||
hotkey: 'Meta+,',
|
||||
action: {
|
||||
href: '/users/'
|
||||
},
|
||||
icon: RewindIcon
|
||||
icon: IconAdjustmentsHorizontal
|
||||
}
|
||||
].filter(({ title }) => input.length === 0 || title.toLowerCase().includes(input.toLowerCase()))
|
||||
});
|
||||
@ -141,7 +164,7 @@ const fileGroup = ({
|
||||
description: 'type part of a file name',
|
||||
commands: []
|
||||
}
|
||||
: matchFiles({ projectId: project.id, matchPattern: input }).then((files) => ({
|
||||
: git.matchFiles({ projectId: project.id, matchPattern: input }).then((files) => ({
|
||||
title: 'Files',
|
||||
description: files.length === 0 ? `no files containing '${input}'` : '',
|
||||
commands: files.map((file) => ({
|
||||
@ -161,24 +184,36 @@ const supportGroup = ({ input }: { input: string }): Group => ({
|
||||
action: {
|
||||
href: `https://docs.gitbutler.com`
|
||||
},
|
||||
icon: FileIcon
|
||||
icon: IconFile
|
||||
},
|
||||
{
|
||||
title: 'Discord',
|
||||
action: {
|
||||
href: `https://discord.gg/MmFkmaJ42D`
|
||||
},
|
||||
icon: GitCommitIcon
|
||||
icon: IconDiscord
|
||||
},
|
||||
{
|
||||
title: 'Send feedback',
|
||||
action: {
|
||||
href: 'mailto:hello@gitbutler.com'
|
||||
},
|
||||
icon: IconFeedback
|
||||
}
|
||||
].filter(({ title }) => input.length === 0 || title.toLowerCase().includes(input.toLowerCase()))
|
||||
});
|
||||
|
||||
export default (params: { projects: Project[]; project?: Project; input: string }) => {
|
||||
const { projects, input, project } = params;
|
||||
export default (params: {
|
||||
addProject: (params: { path: string }) => Promise<Project>;
|
||||
projects: Project[];
|
||||
project?: Project;
|
||||
input: string;
|
||||
}) => {
|
||||
const { addProject, projects, input, project } = params;
|
||||
const groups = [];
|
||||
|
||||
!project && groups.push(goToProjectGroup({ projects, input }));
|
||||
project && groups.push(actionsGroup({ project, input }));
|
||||
groups.push(navigateGroup({ project, input }));
|
||||
!project && groups.push(projectsGroup({ addProject, projects, input }));
|
||||
project && groups.push(fileGroup({ project, input }));
|
||||
groups.push(supportGroup({ input }));
|
||||
|
||||
|
@ -5,12 +5,21 @@
|
||||
import Button from '../Button/Button.svelte';
|
||||
|
||||
let dialog: Dialog;
|
||||
let count = 0;
|
||||
</script>
|
||||
|
||||
<Meta title="GitButler/Dialog" component={Dialog} />
|
||||
|
||||
<Story name="Dialog with title only">
|
||||
<Button on:click={() => dialog.show()}>Open Dialog</Button>
|
||||
<Button on:click={dialog.show}>Open Dialog</Button>
|
||||
<Dialog bind:this={dialog} />
|
||||
</Story>
|
||||
|
||||
<Story name="Dialog with content">
|
||||
<Button on:click={dialog.show}>Open Dialog</Button>
|
||||
<Dialog bind:this={dialog}>
|
||||
<p class="w-[346px]">
|
||||
GitButler offers support for generating Git commits automatically. To use this feature, you
|
||||
will need to sync with GitButler cloud.
|
||||
</p>
|
||||
</Dialog>
|
||||
</Story>
|
||||
|
@ -4,29 +4,46 @@
|
||||
import { IconClose } from '$lib/components/icons';
|
||||
|
||||
export const show = () => modal.show();
|
||||
const hide = () => modal.hide();
|
||||
|
||||
let modal: Modal;
|
||||
</script>
|
||||
|
||||
<Modal on:close bind:this={modal}>
|
||||
<div class="flex flex-col text-zinc-400">
|
||||
<div class="flex p-4">
|
||||
<div class="flex-grow text-[18px] text-zinc-300">
|
||||
<Modal bind:this={modal} let:close>
|
||||
<div class="wrapper flex w-full flex-col text-zinc-300">
|
||||
<header class="flex w-full justify-between gap-4 p-4">
|
||||
<h2 class="text-[18px] ">
|
||||
<slot name="title">Title</slot>
|
||||
</h2>
|
||||
|
||||
<Button filled={false} on:click={close} icon={IconClose} />
|
||||
</header>
|
||||
|
||||
{#if $$slots.default}
|
||||
<div class="p-4 text-base ">
|
||||
<slot />
|
||||
</div>
|
||||
<button on:click={() => modal.hide()}>
|
||||
<IconClose class="h-6 w-6" />
|
||||
</button>
|
||||
</div>
|
||||
<p class="p-4 text-base">
|
||||
<slot />
|
||||
</p>
|
||||
<div class="m-4 ml-auto flex gap-4">
|
||||
<slot name="controls" {hide} {show}>
|
||||
<Button filled on:click={hide}>Cancel</Button>
|
||||
<Button filled role="primary" on:click={hide}>Confirm</Button>
|
||||
{/if}
|
||||
|
||||
<footer class="flex w-full justify-end gap-4 p-4">
|
||||
<slot name="controls" {close}>
|
||||
<Button filled={false} outlined={true} on:click={close}>Secondary action</Button>
|
||||
<Button role="primary" on:click={close}>Primary action</Button>
|
||||
</slot>
|
||||
</div>
|
||||
</footer>
|
||||
</div>
|
||||
</Modal>
|
||||
|
||||
<style>
|
||||
.wrapper {
|
||||
background: linear-gradient(0deg, rgba(43, 43, 48, 0.8), rgba(43, 43, 48, 0.8)),
|
||||
linear-gradient(0deg, rgba(63, 63, 63, 0.5), rgba(63, 63, 63, 0.5));
|
||||
}
|
||||
|
||||
header {
|
||||
box-shadow: inset 0px -1px 0px rgba(0, 0, 0, 0.1);
|
||||
}
|
||||
|
||||
footer {
|
||||
box-shadow: inset 0px 1px 0px rgba(0, 0, 0, 0.1);
|
||||
}
|
||||
</style>
|
||||
|
@ -1,12 +1,10 @@
|
||||
<script lang="ts">
|
||||
import type Users from '$lib/users';
|
||||
import type Api from '$lib/api';
|
||||
import type { LoginToken } from '$lib/api';
|
||||
import type { LoginToken, CloudApi, users } from '$lib/api';
|
||||
import { derived, writable } from 'svelte/store';
|
||||
import { open } from '@tauri-apps/api/shell';
|
||||
|
||||
export let user: Awaited<ReturnType<typeof Users>>;
|
||||
export let api: Awaited<ReturnType<typeof Api>>;
|
||||
export let user: Awaited<ReturnType<typeof users.CurrentUser>>;
|
||||
export let api: Awaited<ReturnType<typeof CloudApi>>;
|
||||
|
||||
const pollForUser = async (token: string) => {
|
||||
const apiUser = await api.login.user.get(token).catch(() => null);
|
||||
|
@ -1,64 +1,42 @@
|
||||
<script lang="ts">
|
||||
import { createEventDispatcher } from 'svelte';
|
||||
|
||||
import { scale } from 'svelte/transition';
|
||||
|
||||
let dialog: HTMLDialogElement;
|
||||
let content: HTMLDivElement | null = null;
|
||||
|
||||
const dispatch = createEventDispatcher<{ close: void }>();
|
||||
|
||||
let open = false;
|
||||
|
||||
export const show = () => {
|
||||
open = true;
|
||||
dialog.showModal();
|
||||
};
|
||||
export const hide = () => {
|
||||
open = false;
|
||||
dialog.close();
|
||||
dispatch('close');
|
||||
open = true;
|
||||
};
|
||||
export const isOpen = () => open;
|
||||
|
||||
const handleClick = (event: MouseEvent) => {
|
||||
if (content && !content.contains(event.target as Node | null) && !event.defaultPrevented) {
|
||||
hide();
|
||||
}
|
||||
export const close = () => {
|
||||
dialog.close();
|
||||
open = false;
|
||||
};
|
||||
|
||||
const handleClick = (event: Event) => {
|
||||
if (event.defaultPrevented) return;
|
||||
if (!dialog?.open) return;
|
||||
const isClickInside = !content || content.contains(event.target as Node | null);
|
||||
if (isClickInside) return;
|
||||
close();
|
||||
};
|
||||
</script>
|
||||
|
||||
<!--
|
||||
@component
|
||||
In most cases, you should use the Dialog component, which builds on top of this, instead of this one.
|
||||
This is a base Modal component which makes sure that all mouse and keyboard events are handled correctly.
|
||||
It does minimal styling. A close event is fired when the modal is closed.
|
||||
|
||||
- Usage:
|
||||
```tsx
|
||||
<Modal>
|
||||
your content slotted in
|
||||
</Modal>
|
||||
```
|
||||
-->
|
||||
<!-- test -->
|
||||
<svelte:window on:click={handleClick} />
|
||||
|
||||
<dialog
|
||||
class="my-0 overflow-hidden bg-transparent p-0"
|
||||
on:click={handleClick}
|
||||
on:keydown={handleClick}
|
||||
class="bg-transparent"
|
||||
in:scale={{ duration: 150 }}
|
||||
bind:this={dialog}
|
||||
on:close={hide}
|
||||
on:close={close}
|
||||
>
|
||||
{#if open}
|
||||
<div class="modal-overlay relative top-[25%] h-[100vh] overflow-hidden">
|
||||
<div
|
||||
class="modal w-[640px] overflow-hidden rounded-lg border-[0.5px] border-[#3F3F3f] bg-zinc-900/70 p-0 shadow-lg backdrop-blur-lg"
|
||||
>
|
||||
<div class="flex" bind:this={content}>
|
||||
<slot />
|
||||
</div>
|
||||
</div>
|
||||
<div bind:this={content} class="flex">
|
||||
<slot {close} isOpen={open} />
|
||||
</div>
|
||||
{/if}
|
||||
</dialog>
|
||||
|
@ -1,6 +1,6 @@
|
||||
<script lang="ts">
|
||||
import { collapsable } from '$lib/paths';
|
||||
import { Status } from '$lib/git/statuses';
|
||||
import { Status } from '$lib/api';
|
||||
|
||||
export let statuses: Record<string, Status>;
|
||||
</script>
|
||||
|
@ -1,14 +0,0 @@
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
fill="none"
|
||||
viewBox="0 0 24 24"
|
||||
stroke-width="1.5"
|
||||
stroke="currentColor"
|
||||
class="h-6 w-6"
|
||||
>
|
||||
<path
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
d="M17.593 3.322c1.1.128 1.907 1.077 1.907 2.185V21L12 17.25 4.5 21V5.507c0-1.108.806-2.057 1.907-2.185a48.507 48.507 0 0111.186 0z"
|
||||
/>
|
||||
</svg>
|
Before Width: | Height: | Size: 341 B |
@ -1,14 +0,0 @@
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
fill="none"
|
||||
viewBox="0 0 24 24"
|
||||
stroke-width="1.5"
|
||||
stroke="currentColor"
|
||||
class="h-6 w-6"
|
||||
>
|
||||
<path
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
d="M6 6.878V6a2.25 2.25 0 012.25-2.25h7.5A2.25 2.25 0 0118 6v.878m-12 0c.235-.083.487-.128.75-.128h10.5c.263 0 .515.045.75.128m-12 0A2.25 2.25 0 004.5 9v.878m13.5-3A2.25 2.25 0 0119.5 9v.878m0 0a2.246 2.246 0 00-.75-.128H5.25c-.263 0-.515.045-.75.128m15 0A2.25 2.25 0 0121 12v6a2.25 2.25 0 01-2.25 2.25H5.25A2.25 2.25 0 013 18v-6c0-.98.626-1.813 1.5-2.122"
|
||||
/>
|
||||
</svg>
|
Before Width: | Height: | Size: 565 B |
@ -1,14 +0,0 @@
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
fill="none"
|
||||
viewBox="0 0 24 24"
|
||||
stroke-width="1.5"
|
||||
stroke="currentColor"
|
||||
class="h-6 w-6"
|
||||
>
|
||||
<path
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
d="M9 12.75L11.25 15 15 9.75M21 12c0 1.268-.63 2.39-1.593 3.068a3.745 3.745 0 01-1.043 3.296 3.745 3.745 0 01-3.296 1.043A3.745 3.745 0 0112 21c-1.268 0-2.39-.63-3.068-1.593a3.746 3.746 0 01-3.296-1.043 3.745 3.745 0 01-1.043-3.296A3.745 3.745 0 013 12c0-1.268.63-2.39 1.593-3.068a3.745 3.745 0 011.043-3.296 3.746 3.746 0 013.296-1.043A3.746 3.746 0 0112 3c1.268 0 2.39.63 3.068 1.593a3.746 3.746 0 013.296 1.043 3.746 3.746 0 011.043 3.296A3.745 3.745 0 0121 12z"
|
||||
/>
|
||||
</svg>
|
Before Width: | Height: | Size: 674 B |
@ -1,14 +0,0 @@
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
fill="none"
|
||||
viewBox="0 0 24 24"
|
||||
stroke-width="1.5"
|
||||
stroke="currentColor"
|
||||
class="h-6 w-6"
|
||||
>
|
||||
<path
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
d="M21.75 6.75v10.5a2.25 2.25 0 01-2.25 2.25h-15a2.25 2.25 0 01-2.25-2.25V6.75m19.5 0A2.25 2.25 0 0019.5 4.5h-15a2.25 2.25 0 00-2.25 2.25m19.5 0v.243a2.25 2.25 0 01-1.07 1.916l-7.5 4.615a2.25 2.25 0 01-2.36 0L3.32 8.91a2.25 2.25 0 01-1.07-1.916V6.75"
|
||||
/>
|
||||
</svg>
|
Before Width: | Height: | Size: 459 B |
@ -1,21 +0,0 @@
|
||||
<script lang="ts">
|
||||
let className = '';
|
||||
export { className as class };
|
||||
</script>
|
||||
|
||||
<svg
|
||||
class={className}
|
||||
width="20"
|
||||
height="20"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
fill="none"
|
||||
viewBox="0 0 24 24"
|
||||
stroke-width="1.5"
|
||||
stroke="currentColor"
|
||||
>
|
||||
<path
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
d="M19.5 14.25v-2.625a3.375 3.375 0 00-3.375-3.375h-1.5A1.125 1.125 0 0113.5 7.125v-1.5a3.375 3.375 0 00-3.375-3.375H8.25m0 12.75h7.5m-7.5 3H12M10.5 2.25H5.625c-.621 0-1.125.504-1.125 1.125v17.25c0 .621.504 1.125 1.125 1.125h12.75c.621 0 1.125-.504 1.125-1.125V11.25a9 9 0 00-9-9z"
|
||||
/>
|
||||
</svg>
|
@ -1,14 +0,0 @@
|
||||
<svg
|
||||
class="h-6 w-6 flex-none text-zinc-500"
|
||||
fill="none"
|
||||
viewBox="0 0 24 24"
|
||||
stroke-width="1.5"
|
||||
stroke="currentColor"
|
||||
aria-hidden="true"
|
||||
>
|
||||
<path
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
d="M12 10.5v6m3-3H9m4.06-7.19l-2.12-2.12a1.5 1.5 0 00-1.061-.44H4.5A2.25 2.25 0 002.25 6v12a2.25 2.25 0 002.25 2.25h15A2.25 2.25 0 0021.75 18V9a2.25 2.25 0 00-2.25-2.25h-5.379a1.5 1.5 0 01-1.06-.44z"
|
||||
/>
|
||||
</svg>
|
Before Width: | Height: | Size: 416 B |
@ -1,14 +0,0 @@
|
||||
<svg
|
||||
class="h-6 w-6 flex-none text-zinc-500"
|
||||
fill="none"
|
||||
viewBox="0 0 24 24"
|
||||
stroke-width="1.5"
|
||||
stroke="currentColor"
|
||||
aria-hidden="true"
|
||||
>
|
||||
<path
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
d="M5.25 8.25h15m-16.5 7.5h15m-1.8-13.5l-3.9 19.5m-2.1-19.5l-3.9 19.5"
|
||||
/>
|
||||
</svg>
|
Before Width: | Height: | Size: 287 B |
24
src/lib/components/icons/IconAdjustmentsHorizontal.svelte
Normal file
@ -0,0 +1,24 @@
|
||||
<script lang="ts">
|
||||
let className = '';
|
||||
export { className as class };
|
||||
</script>
|
||||
|
||||
<svg
|
||||
class={className}
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
fill="none"
|
||||
viewBox="0 0 24 24"
|
||||
stroke-width="1.5"
|
||||
stroke="currentColor"
|
||||
>
|
||||
<path stroke="none" d="M0 0h24v24H0z" fill="none" />
|
||||
<path d="M14 6m-2 0a2 2 0 1 0 4 0a2 2 0 1 0 -4 0" />
|
||||
<path d="M4 6l8 0" />
|
||||
<path d="M16 6l4 0" />
|
||||
<path d="M8 12m-2 0a2 2 0 1 0 4 0a2 2 0 1 0 -4 0" />
|
||||
<path d="M4 12l2 0" />
|
||||
<path d="M10 12l10 0" />
|
||||
<path d="M17 18m-2 0a2 2 0 1 0 4 0a2 2 0 1 0 -4 0" />
|
||||
<path d="M4 18l11 0" />
|
||||
<path d="M19 18l1 0" />
|
||||
</svg>
|
@ -1,14 +0,0 @@
|
||||
<svg
|
||||
{...$$restProps}
|
||||
width="15"
|
||||
height="15"
|
||||
viewBox="0 0 20 20"
|
||||
fill="currentColor"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
>
|
||||
<path
|
||||
fill-rule="evenodd"
|
||||
clip-rule="evenodd"
|
||||
d="M0 10C0 4.486 4.486 0 10 0C15.514 0 20 4.486 20 10C20 15.514 15.514 20 10 20C4.486 20 0 15.514 0 10ZM7.70711 6.29289C7.31658 5.90237 6.68342 5.90237 6.29289 6.29289C5.90237 6.68342 5.90237 7.31658 6.29289 7.70711L8.58579 10L6.29289 12.2929C5.90237 12.6834 5.90237 13.3166 6.29289 13.7071C6.68342 14.0976 7.31658 14.0976 7.70711 13.7071L10 11.4142L12.2929 13.7071C12.6834 14.0976 13.3166 14.0976 13.7071 13.7071C14.0976 13.3166 14.0976 12.6834 13.7071 12.2929L11.4142 10L13.7071 7.70711C14.0976 7.31658 14.0976 6.68342 13.7071 6.29289C13.3166 5.90237 12.6834 5.90237 12.2929 6.29289L10 8.58579L7.70711 6.29289Z"
|
||||
/>
|
||||
</svg>
|
Before Width: | Height: | Size: 806 B |
@ -1,19 +0,0 @@
|
||||
<svg
|
||||
{...$$restProps}
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
width="24"
|
||||
height="24"
|
||||
viewBox="0 0 24 24"
|
||||
stroke-width="2"
|
||||
stroke="currentColor"
|
||||
fill="none"
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
>
|
||||
<path stroke="none" d="M0 0h24v24H0z" fill="none" />
|
||||
<path
|
||||
d="M7 3.34a10 10 0 1 1 -4.995 8.984l-.005 -.324l.005 -.324a10 10 0 0 1 4.995 -8.336z"
|
||||
stroke-width="0"
|
||||
fill="currentColor"
|
||||
/>
|
||||
</svg>
|
Before Width: | Height: | Size: 411 B |
@ -1,19 +0,0 @@
|
||||
<svg
|
||||
{...$$restProps}
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
width="24"
|
||||
height="24"
|
||||
viewBox="0 0 24 24"
|
||||
stroke-width="2"
|
||||
stroke="currentColor"
|
||||
fill="none"
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
>
|
||||
<path stroke="none" d="M0 0h24v24H0z" fill="none" />
|
||||
<path d="M12 12m-9 0a9 9 0 1 0 18 0a9 9 0 1 0 -18 0" />
|
||||
<path d="M12 3v18" />
|
||||
<path d="M12 14l7 -7" />
|
||||
<path d="M12 19l8.5 -8.5" />
|
||||
<path d="M12 9l4.5 -4.5" />
|
||||
</svg>
|
Before Width: | Height: | Size: 436 B |
20
src/lib/components/icons/IconDiscord.svelte
Normal file
@ -0,0 +1,20 @@
|
||||
<script lang="ts">
|
||||
let className = '';
|
||||
export { className as class };
|
||||
</script>
|
||||
|
||||
<svg
|
||||
class={className}
|
||||
width="24"
|
||||
height="24"
|
||||
viewBox="0 0 24 24"
|
||||
fill="none"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
>
|
||||
<path stroke="none" d="M0 0h24v24H0z" fill="none" />
|
||||
<path
|
||||
d="M14.983 3l.123 .006c2.014 .214 3.527 .672 4.966 1.673a1 1 0 0 1 .371 .488c1.876 5.315 2.373 9.987 1.451 12.28c-1.003 2.005 -2.606 3.553 -4.394 3.553c-.94 0 -2.257 -1.596 -2.777 -2.969l-.02 .005c.838 -.131 1.69 -.323 2.572 -.574a1 1 0 1 0 -.55 -1.924c-3.32 .95 -6.13 .95 -9.45 0a1 1 0 0 0 -.55 1.924c.725 .207 1.431 .373 2.126 .499l.444 .074c-.477 1.37 -1.695 2.965 -2.627 2.965c-1.743 0 -3.276 -1.555 -4.267 -3.644c-.841 -2.206 -.369 -6.868 1.414 -12.174a1 1 0 0 1 .358 -.49c1.392 -1.016 2.807 -1.475 4.717 -1.685a1 1 0 0 1 .938 .435l.063 .107l.652 1.288l.16 -.019c.877 -.09 1.718 -.09 2.595 0l.158 .019l.65 -1.287a1 1 0 0 1 .754 -.54l.123 -.01zm-5.983 6a2 2 0 0 0 -1.977 1.697l-.018 .154l-.005 .149l.005 .15a2 2 0 1 0 1.995 -2.15zm6 0a2 2 0 0 0 -1.977 1.697l-.018 .154l-.005 .149l.005 .15a2 2 0 1 0 1.995 -2.15z"
|
||||
stroke-width="0"
|
||||
fill="currentColor"
|
||||
/>
|
||||
</svg>
|
18
src/lib/components/icons/IconExternalLink.svelte
Normal file
@ -0,0 +1,18 @@
|
||||
<script lang="ts">
|
||||
let className = '';
|
||||
export { className as class };
|
||||
</script>
|
||||
|
||||
<svg
|
||||
class={className}
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
fill="none"
|
||||
viewBox="0 0 24 24"
|
||||
stroke-width="1.5"
|
||||
stroke="currentColor"
|
||||
>
|
||||
<path stroke="none" d="M0 0h24v24H0z" fill="none" />
|
||||
<path d="M12 6h-6a2 2 0 0 0 -2 2v10a2 2 0 0 0 2 2h10a2 2 0 0 0 2 -2v-6" />
|
||||
<path d="M11 13l9 -9" />
|
||||
<path d="M15 4h5v5" />
|
||||
</svg>
|
20
src/lib/components/icons/IconFeedback.svelte
Normal file
@ -0,0 +1,20 @@
|
||||
<script lang="ts">
|
||||
let className = '';
|
||||
export { className as class };
|
||||
</script>
|
||||
|
||||
<svg
|
||||
class={className}
|
||||
width="16"
|
||||
height="16"
|
||||
viewBox="0 0 16 16"
|
||||
fill="none"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
>
|
||||
<path
|
||||
fill-rule="evenodd"
|
||||
clip-rule="evenodd"
|
||||
d="M2.40002 6.80111e-06C1.07452 6.80111e-06 0 1.07453 0 2.40002V10.4C0 11.7255 1.07452 12.8 2.40002 12.8H4.80003V15.2C4.80003 15.495 4.96236 15.7661 5.22244 15.9054C5.48266 16.0445 5.7982 16.0293 6.04369 15.8656L10.6421 12.8H13.5998C14.9255 12.8 15.9998 11.7255 15.9998 10.4V2.40002C15.9998 1.07452 14.9255 0 13.5998 0L2.40002 6.80111e-06ZM1.59992 2.40002C1.59992 1.95818 1.95817 1.59993 2.40002 1.59993H13.5999C14.0417 1.59993 14.4 1.95818 14.4 2.40002V10.4C14.4 10.8418 14.0417 11.1999 13.5999 11.1999H10.3999C10.242 11.1999 10.0876 11.2467 9.95619 11.3343L6.39971 13.7052V12C6.39971 11.7879 6.31547 11.5843 6.16547 11.4343C6.01547 11.2843 5.81191 11.1999 5.59975 11.1999H2.39978C1.95793 11.1999 1.59968 10.8418 1.59968 10.4L1.59992 2.40002Z"
|
||||
fill="currentColor"
|
||||
/>
|
||||
</svg>
|
@ -1,19 +0,0 @@
|
||||
<svg
|
||||
{...$$restProps}
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
width="24"
|
||||
height="24"
|
||||
viewBox="0 0 24 24"
|
||||
stroke-width="2"
|
||||
stroke="currentColor"
|
||||
fill="none"
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
>
|
||||
<path stroke="none" d="M0 0h24v24H0z" fill="none" />
|
||||
<path
|
||||
d="M18.364 4.636a9 9 0 0 1 .203 12.519l-.203 .21l-4.243 4.242a3 3 0 0 1 -4.097 .135l-.144 -.135l-4.244 -4.243a9 9 0 0 1 12.728 -12.728zm-6.364 3.364a3 3 0 1 0 0 6a3 3 0 0 0 0 -6z"
|
||||
stroke-width="0"
|
||||
fill="currentColor"
|
||||
/>
|
||||
</svg>
|
Before Width: | Height: | Size: 505 B |
@ -1,25 +0,0 @@
|
||||
<script lang="ts">
|
||||
let className = '';
|
||||
export { className as class };
|
||||
</script>
|
||||
|
||||
<svg
|
||||
class={className}
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
width="24"
|
||||
height="24"
|
||||
viewBox="0 0 24 24"
|
||||
stroke-width="2"
|
||||
stroke="currentColor"
|
||||
fill="none"
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
>
|
||||
<path stroke="none" d="M0 0h24v24H0z" fill="none" />
|
||||
<path d="M9 4.55a8 8 0 0 1 6 14.9m0 -4.45v5h5" />
|
||||
<path d="M5.63 7.16l0 .01" />
|
||||
<path d="M4.06 11l0 .01" />
|
||||
<path d="M4.63 15.1l0 .01" />
|
||||
<path d="M7.16 18.37l0 .01" />
|
||||
<path d="M11 19.94l0 .01" />
|
||||
</svg>
|
21
src/lib/components/icons/IconSearch.svelte
Normal file
@ -0,0 +1,21 @@
|
||||
<script lang="ts">
|
||||
let className = '';
|
||||
export { className as class };
|
||||
</script>
|
||||
|
||||
<svg
|
||||
class={className}
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
width="20"
|
||||
height="20"
|
||||
viewBox="0 0 20 20"
|
||||
stroke-width="1"
|
||||
fill="none"
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
>
|
||||
<path
|
||||
d="M8 12a4 4 0 110-8 4 4 0 010 8zm9.707 4.293l-4.82-4.82A5.968 5.968 0 0014 8 6 6 0 002 8a6 6 0 006 6 5.968 5.968 0 003.473-1.113l4.82 4.82a.997.997 0 001.414 0 .999.999 0 000-1.414z"
|
||||
fill="currentColor"
|
||||
/>
|
||||
</svg>
|
20
src/lib/components/icons/IconSettings.svelte
Normal file
@ -0,0 +1,20 @@
|
||||
<script lang="ts">
|
||||
let className = '';
|
||||
export { className as class };
|
||||
</script>
|
||||
|
||||
<svg
|
||||
class={className}
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
fill="none"
|
||||
viewBox="0 0 24 24"
|
||||
stroke-width="1.5"
|
||||
stroke="currentColor"
|
||||
>
|
||||
<path
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
d="M9.594 3.94c.09-.542.56-.94 1.11-.94h2.593c.55 0 1.02.398 1.11.94l.213 1.281c.063.374.313.686.645.87.074.04.147.083.22.127.324.196.72.257 1.075.124l1.217-.456a1.125 1.125 0 011.37.49l1.296 2.247a1.125 1.125 0 01-.26 1.431l-1.003.827c-.293.24-.438.613-.431.992a6.759 6.759 0 010 .255c-.007.378.138.75.43.99l1.005.828c.424.35.534.954.26 1.43l-1.298 2.247a1.125 1.125 0 01-1.369.491l-1.217-.456c-.355-.133-.75-.072-1.076.124a6.57 6.57 0 01-.22.128c-.331.183-.581.495-.644.869l-.213 1.28c-.09.543-.56.941-1.11.941h-2.594c-.55 0-1.02-.398-1.11-.94l-.213-1.281c-.062-.374-.312-.686-.644-.87a6.52 6.52 0 01-.22-.127c-.325-.196-.72-.257-1.076-.124l-1.217.456a1.125 1.125 0 01-1.369-.49l-1.297-2.247a1.125 1.125 0 01.26-1.431l1.004-.827c.292-.24.437-.613.43-.992a6.932 6.932 0 010-.255c.007-.378-.138-.75-.43-.99l-1.004-.828a1.125 1.125 0 01-.26-1.43l1.297-2.247a1.125 1.125 0 011.37-.491l1.216.456c.356.133.751.072 1.076-.124.072-.044.146-.087.22-.128.332-.183.582-.495.644-.869l.214-1.281z"
|
||||
/>
|
||||
<path stroke-linecap="round" stroke-linejoin="round" d="M15 12a3 3 0 11-6 0 3 3 0 016 0z" />
|
||||
</svg>
|
@ -1,19 +0,0 @@
|
||||
<svg
|
||||
{...$$restProps}
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
width="24"
|
||||
height="24"
|
||||
viewBox="0 0 24 24"
|
||||
stroke-width="2"
|
||||
stroke="currentColor"
|
||||
fill="none"
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
>
|
||||
<path stroke="none" d="M0 0h24v24H0z" fill="none" />
|
||||
<path
|
||||
d="M12 2c-.218 0 -.432 .002 -.642 .005l-.616 .017l-.299 .013l-.579 .034l-.553 .046c-4.785 .464 -6.732 2.411 -7.196 7.196l-.046 .553l-.034 .579c-.005 .098 -.01 .198 -.013 .299l-.017 .616l-.004 .318l-.001 .324c0 .218 .002 .432 .005 .642l.017 .616l.013 .299l.034 .579l.046 .553c.464 4.785 2.411 6.732 7.196 7.196l.553 .046l.579 .034c.098 .005 .198 .01 .299 .013l.616 .017l.642 .005l.642 -.005l.616 -.017l.299 -.013l.579 -.034l.553 -.046c4.785 -.464 6.732 -2.411 7.196 -7.196l.046 -.553l.034 -.579c.005 -.098 .01 -.198 .013 -.299l.017 -.616l.005 -.642l-.005 -.642l-.017 -.616l-.013 -.299l-.034 -.579l-.046 -.553c-.464 -4.785 -2.411 -6.732 -7.196 -7.196l-.553 -.046l-.579 -.034a28.058 28.058 0 0 0 -.299 -.013l-.616 -.017l-.318 -.004l-.324 -.001z"
|
||||
stroke-width="0"
|
||||
fill="currentColor"
|
||||
/>
|
||||
</svg>
|
Before Width: | Height: | Size: 1.0 KiB |
@ -1,15 +0,0 @@
|
||||
<svg
|
||||
class="h-6 w-6 flex-none text-zinc-500"
|
||||
fill="none"
|
||||
viewBox="0 0 24 24"
|
||||
stroke-width="1.5"
|
||||
stroke="currentColor"
|
||||
aria-hidden="true"
|
||||
>
|
||||
<path
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
d="M9.568 3H5.25A2.25 2.25 0 003 5.25v4.318c0 .597.237 1.17.659 1.591l9.581 9.581c.699.699 1.78.872 2.607.33a18.095 18.095 0 005.223-5.223c.542-.827.369-1.908-.33-2.607L11.16 3.66A2.25 2.25 0 009.568 3z"
|
||||
/>
|
||||
<path stroke-linecap="round" stroke-linejoin="round" d="M6 6h.008v.008H6V6z" />
|
||||
</svg>
|
Before Width: | Height: | Size: 501 B |
@ -1,14 +0,0 @@
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
fill="none"
|
||||
viewBox="0 0 24 24"
|
||||
stroke-width="1.5"
|
||||
stroke="currentColor"
|
||||
class="h-6 w-6"
|
||||
>
|
||||
<path
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
d="M2.25 12.75V12A2.25 2.25 0 014.5 9.75h15A2.25 2.25 0 0121.75 12v.75m-8.69-6.44l-2.12-2.12a1.5 1.5 0 00-1.061-.44H4.5A2.25 2.25 0 002.25 6v12a2.25 2.25 0 002.25 2.25h15A2.25 2.25 0 0021.75 18V9a2.25 2.25 0 00-2.25-2.25h-5.379a1.5 1.5 0 01-1.06-.44z"
|
||||
/>
|
||||
</svg>
|
Before Width: | Height: | Size: 460 B |
@ -1,21 +1,7 @@
|
||||
export { default as BookmarkIcon } from './BookmarkIcon.svelte';
|
||||
export { default as BranchIcon } from './BranchIcon.svelte';
|
||||
export { default as CommitIcon } from './CommitIcon.svelte';
|
||||
export { default as ContactIcon } from './ContactIcon.svelte';
|
||||
export { default as FileIcon } from './FileIcon.svelte';
|
||||
export { default as FolderIcon } from './FolderIcon.svelte';
|
||||
export { default as LabelIcon } from './LabelIcon.svelte';
|
||||
export { default as GitCommitIcon } from './GitCommitIcon.svelte';
|
||||
export { default as ProjectIcon } from './ProjectIcon.svelte';
|
||||
export { default as RewindIcon } from './RewindIcon.svelte';
|
||||
export { default as IconRotateClockwise } from './IconRotateClockwise.svelte';
|
||||
export { default as IconGitCommit } from './IconGitCommit.svelte';
|
||||
export { default as IconRewind } from './IconRewind.svelte';
|
||||
export { default as IconPlayerPauseFilled } from './IconPlayerPauseFilled.svelte';
|
||||
export { default as IconPlayerPlayFilled } from './IconPlayerPlayFilled.svelte';
|
||||
export { default as IconCircleHalf } from './IconCircleHalf.svelte';
|
||||
export { default as IconSquareRoundedFilled } from './IconSquareRoundedFilled.svelte';
|
||||
export { default as IconMapPinFilled } from './IconMapPinFilled.svelte';
|
||||
export { default as IconCircleFilled } from './IconCircleFilled.svelte';
|
||||
export { default as IconCircleCancel } from './IconCircleCancel.svelte';
|
||||
export { default as IconChevronLeft } from './IconChevronLeft.svelte';
|
||||
export { default as IconChevronRight } from './IconChevronRight.svelte';
|
||||
export { default as IconFile } from './IconFile.svelte';
|
||||
@ -25,3 +11,9 @@ export { default as IconLoading } from './IconLoading.svelte';
|
||||
export { default as IconProject } from './IconProject.svelte';
|
||||
export { default as IconTerminal } from './IconTerminal.svelte';
|
||||
export { default as IconClose } from './IconClose.svelte';
|
||||
export { default as IconSettings } from './IconSettings.svelte';
|
||||
export { default as IconAdjustmentsHorizontal } from './IconAdjustmentsHorizontal.svelte';
|
||||
export { default as IconDiscord } from './IconDiscord.svelte';
|
||||
export { default as IconExternalLink } from './IconExternalLink.svelte';
|
||||
export { default as IconFeedback } from './IconFeedback.svelte';
|
||||
export { default as IconSearch } from './IconSearch.svelte';
|
||||
|
@ -1,27 +0,0 @@
|
||||
import { invoke } from '@tauri-apps/api';
|
||||
import { appWindow } from '@tauri-apps/api/window';
|
||||
import { writable, type Readable } from 'svelte/store';
|
||||
import { log } from '$lib';
|
||||
|
||||
export type Activity = {
|
||||
type: string;
|
||||
timestampMs: number;
|
||||
message: string;
|
||||
};
|
||||
|
||||
const list = (params: { projectId: string; startTimeMs?: number }) =>
|
||||
invoke<Activity[]>('git_activity', params);
|
||||
|
||||
export default async (params: { projectId: string }) => {
|
||||
const activity = await list(params);
|
||||
const store = writable(activity);
|
||||
|
||||
appWindow.listen(`project://${params.projectId}/git/activity`, async () => {
|
||||
log.info(`Status: Received git activity event, projectId: ${params.projectId}`);
|
||||
const startTimeMs = activity.at(-1)?.timestampMs;
|
||||
const newActivities = await list({ projectId: params.projectId, startTimeMs });
|
||||
store.update((activities) => [...activities, ...newActivities]);
|
||||
});
|
||||
|
||||
return store as Readable<Activity[]>;
|
||||
};
|
@ -1,24 +0,0 @@
|
||||
import { invoke } from '@tauri-apps/api';
|
||||
import { appWindow } from '@tauri-apps/api/window';
|
||||
import { writable, type Readable } from 'svelte/store';
|
||||
import { log } from '$lib';
|
||||
|
||||
const getDiffs = (params: { projectId: string }) =>
|
||||
invoke<Record<string, string>>('git_wd_diff', params);
|
||||
|
||||
export default async (params: { projectId: string }) => {
|
||||
const diffs = await getDiffs(params);
|
||||
const store = writable(diffs);
|
||||
|
||||
appWindow.listen(`project://${params.projectId}/sessions`, async () => {
|
||||
log.info(`Status: Received sessions event, projectId: ${params.projectId}`);
|
||||
store.set(await getDiffs(params));
|
||||
});
|
||||
|
||||
appWindow.listen(`project://${params.projectId}/git/index`, async () => {
|
||||
log.info(`Status: Received git activity event, projectId: ${params.projectId}`);
|
||||
store.set(await getDiffs(params));
|
||||
});
|
||||
|
||||
return store as Readable<Record<string, string>>;
|
||||
};
|
@ -1,18 +0,0 @@
|
||||
import { invoke } from '@tauri-apps/api';
|
||||
import { appWindow } from '@tauri-apps/api/window';
|
||||
import { derived, writable } from 'svelte/store';
|
||||
import { log } from '$lib';
|
||||
|
||||
const list = (params: { projectId: string }) => invoke<string>('git_head', params);
|
||||
|
||||
export default async (params: { projectId: string }) => {
|
||||
const head = await list(params);
|
||||
const store = writable(head);
|
||||
|
||||
appWindow.listen<{ head: string }>(`project://${params.projectId}/git/head`, async (payload) => {
|
||||
log.info(`Status: Received git head event, projectId: ${params.projectId}`);
|
||||
store.set(payload.payload.head);
|
||||
});
|
||||
|
||||
return derived(store, (head) => head.replace('refs/heads/', ''));
|
||||
};
|
@ -1,8 +1,6 @@
|
||||
export * as deltas from './deltas';
|
||||
export * as projects from './projects';
|
||||
export * as api from './api';
|
||||
export * as log from './log';
|
||||
export * as toasts from './toasts';
|
||||
export * as sessions from './sessions';
|
||||
export { Toaster } from './toasts';
|
||||
export * as week from './week';
|
||||
export * as uisessions from './uisessions';
|
||||
export * from './search';
|
||||
|
21
src/lib/sentry.ts
Normal file
@ -0,0 +1,21 @@
|
||||
import { setUser } from '@sentry/sveltekit';
|
||||
import type { User } from '$lib/api';
|
||||
import * as log from '$lib/log';
|
||||
|
||||
export default () => {
|
||||
return {
|
||||
identify: (user: User | undefined) => {
|
||||
if (user) {
|
||||
log.info(`sentry identify`);
|
||||
setUser({
|
||||
id: user.id.toString(),
|
||||
email: user.email,
|
||||
username: user.name
|
||||
});
|
||||
} else {
|
||||
log.info(`sentry reset`);
|
||||
setUser(null);
|
||||
}
|
||||
}
|
||||
};
|
||||
};
|
@ -1,101 +0,0 @@
|
||||
import { invoke } from '@tauri-apps/api';
|
||||
import { appWindow } from '@tauri-apps/api/window';
|
||||
import { writable, type Readable } from 'svelte/store';
|
||||
import { log } from '$lib';
|
||||
import type { Activity } from './git/activity';
|
||||
import { clone } from './utils';
|
||||
|
||||
export namespace Session {
|
||||
export const within = (session: Session | undefined, timestampMs: number) => {
|
||||
if (!session) return false;
|
||||
const { startTimestampMs, lastTimestampMs } = session.meta;
|
||||
return startTimestampMs <= timestampMs && timestampMs <= lastTimestampMs;
|
||||
};
|
||||
}
|
||||
|
||||
export type Session = {
|
||||
id: string;
|
||||
hash?: string;
|
||||
meta: {
|
||||
startTimestampMs: number;
|
||||
lastTimestampMs: number;
|
||||
branch?: string;
|
||||
commit?: string;
|
||||
};
|
||||
activity: Activity[];
|
||||
};
|
||||
|
||||
const filesCache: Record<string, Record<string, Promise<Record<string, string>>>> = {};
|
||||
|
||||
export const listFiles = async (params: {
|
||||
projectId: string;
|
||||
sessionId: string;
|
||||
paths?: string[];
|
||||
}) => {
|
||||
const sessionFilesCache = filesCache[params.projectId] || {};
|
||||
if (params.sessionId in sessionFilesCache) {
|
||||
return sessionFilesCache[params.sessionId].then((files) =>
|
||||
Object.fromEntries(
|
||||
Object.entries(clone(files)).filter(([path]) =>
|
||||
params.paths ? params.paths.includes(path) : true
|
||||
)
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
const promise = invoke<Record<string, string>>('list_session_files', {
|
||||
sessionId: params.sessionId,
|
||||
projectId: params.projectId
|
||||
});
|
||||
sessionFilesCache[params.sessionId] = promise;
|
||||
filesCache[params.projectId] = sessionFilesCache;
|
||||
return promise.then((files) =>
|
||||
Object.fromEntries(
|
||||
Object.entries(clone(files)).filter(([path]) =>
|
||||
params.paths ? params.paths.includes(path) : true
|
||||
)
|
||||
)
|
||||
);
|
||||
};
|
||||
|
||||
const sessionsCache: Record<string, Promise<Session[]>> = {};
|
||||
|
||||
const list = async (params: { projectId: string; earliestTimestampMs?: number }) => {
|
||||
if (params.projectId in sessionsCache) {
|
||||
return sessionsCache[params.projectId].then((sessions) =>
|
||||
clone(sessions).filter((s) =>
|
||||
params.earliestTimestampMs ? s.meta.startTimestampMs >= params.earliestTimestampMs : true
|
||||
)
|
||||
);
|
||||
}
|
||||
sessionsCache[params.projectId] = invoke<Session[]>('list_sessions', {
|
||||
projectId: params.projectId
|
||||
});
|
||||
return sessionsCache[params.projectId].then((sessions) =>
|
||||
clone(sessions).filter((s) =>
|
||||
params.earliestTimestampMs ? s.meta.startTimestampMs >= params.earliestTimestampMs : true
|
||||
)
|
||||
);
|
||||
};
|
||||
|
||||
export default async (params: { projectId: string; earliestTimestampMs?: number }) => {
|
||||
const store = writable([] as Session[]);
|
||||
list(params).then((sessions) => {
|
||||
store.set(sessions);
|
||||
});
|
||||
|
||||
appWindow.listen<Session>(`project://${params.projectId}/sessions`, async (event) => {
|
||||
log.info(`Received sessions event, projectId: ${params.projectId}`);
|
||||
const session = event.payload;
|
||||
store.update((sessions) => {
|
||||
const index = sessions.findIndex((session) => session.id === event.payload.id);
|
||||
if (index === -1) {
|
||||
return [...sessions, session];
|
||||
} else {
|
||||
return [...sessions.slice(0, index), session, ...sessions.slice(index + 1)];
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
return store as Readable<Session[]>;
|
||||
};
|
@ -1,4 +1,5 @@
|
||||
import toast, { type ToastOptions, type ToastPosition } from 'svelte-french-toast';
|
||||
export { Toaster } from 'svelte-french-toast';
|
||||
|
||||
const defaultOptions = {
|
||||
position: 'bottom-center' as ToastPosition,
|
||||
|
@ -1,5 +1,4 @@
|
||||
import type { Session } from '$lib/sessions';
|
||||
import type { Delta } from '$lib/deltas';
|
||||
import type { Session, Delta } from '$lib/api';
|
||||
|
||||
export type UISession = {
|
||||
session: Session;
|
||||
|
@ -1,18 +1,18 @@
|
||||
<script lang="ts">
|
||||
import '../app.postcss';
|
||||
|
||||
import { open } from '@tauri-apps/api/dialog';
|
||||
import { toasts, Toaster } from '$lib';
|
||||
import tinykeys from 'tinykeys';
|
||||
import { Toaster } from 'svelte-french-toast';
|
||||
import type { LayoutData } from './$types';
|
||||
import { BackForwardButtons, Button } from '$lib/components';
|
||||
import Breadcrumbs from '$lib/components/Breadcrumbs.svelte';
|
||||
import { BackForwardButtons, Button, CommandPalette, Breadcrumbs } from '$lib/components';
|
||||
import { page } from '$app/stores';
|
||||
import CommandPalette from '$lib/components/CommandPalette/CommandPalette.svelte';
|
||||
import { derived } from 'svelte/store';
|
||||
import { onMount } from 'svelte';
|
||||
import { goto } from '$app/navigation';
|
||||
|
||||
export let data: LayoutData;
|
||||
const { user, posthog, projects } = data;
|
||||
const { user, posthog, projects, sentry } = data;
|
||||
|
||||
const project = derived([page, projects], ([page, projects]) =>
|
||||
projects.find((project) => project.id === page.params.projectId)
|
||||
@ -22,11 +22,28 @@
|
||||
|
||||
onMount(() =>
|
||||
tinykeys(window, {
|
||||
'Meta+k': () => commandPalette.show()
|
||||
'Meta+k': () => commandPalette.show(),
|
||||
'Meta+,': () => goto('/users/'),
|
||||
'Meta+Shift+N': async () => {
|
||||
const selectedPath = await open({
|
||||
directory: true,
|
||||
recursive: true
|
||||
});
|
||||
if (selectedPath === null) return;
|
||||
if (Array.isArray(selectedPath) && selectedPath.length !== 1) return;
|
||||
const projectPath = Array.isArray(selectedPath) ? selectedPath[0] : selectedPath;
|
||||
|
||||
try {
|
||||
await projects.add({ path: projectPath });
|
||||
} catch (e: any) {
|
||||
toasts.error(e.message);
|
||||
}
|
||||
}
|
||||
})
|
||||
);
|
||||
|
||||
user.subscribe(posthog.identify);
|
||||
user.subscribe(sentry.identify);
|
||||
</script>
|
||||
|
||||
<div class="flex h-full max-h-full min-h-full flex-col">
|
||||
@ -59,5 +76,5 @@
|
||||
<slot />
|
||||
</div>
|
||||
<Toaster />
|
||||
<CommandPalette bind:this={commandPalette} {projects} {project} />
|
||||
<CommandPalette bind:this={commandPalette} {projects} {project} addProject={projects.add} />
|
||||
</div>
|
||||
|
@ -1,27 +1,29 @@
|
||||
import { readable } from 'svelte/store';
|
||||
import type { LayoutLoad } from './$types';
|
||||
import { building } from '$app/environment';
|
||||
import type { Project } from '$lib/projects';
|
||||
import Api from '$lib/api';
|
||||
import type { Project } from '$lib/api';
|
||||
import { Api } from '$lib/api/cloud';
|
||||
import Posthog from '$lib/posthog';
|
||||
import * as log from '$lib/log';
|
||||
import Sentry from '$lib/sentry';
|
||||
import { setup as setupLogger } from '$lib/log';
|
||||
import { wrapLoadWithSentry } from '@sentry/sveltekit';
|
||||
|
||||
export const ssr = false;
|
||||
export const prerender = true;
|
||||
export const csr = true;
|
||||
|
||||
export const load: LayoutLoad = async ({ fetch }) => {
|
||||
export const load: LayoutLoad = wrapLoadWithSentry(async ({ fetch }) => {
|
||||
const projects = building
|
||||
? {
|
||||
...readable<Project[]>([]),
|
||||
add: () => {
|
||||
add: (params: { path: string }): Promise<Project> => {
|
||||
throw new Error('not implemented');
|
||||
},
|
||||
get: () => {
|
||||
throw new Error('not implemented');
|
||||
}
|
||||
}
|
||||
: await (await import('$lib/projects')).default();
|
||||
: await (await import('$lib/api')).projects.Projects();
|
||||
const user = building
|
||||
? {
|
||||
...readable<undefined>(undefined),
|
||||
@ -32,12 +34,13 @@ export const load: LayoutLoad = async ({ fetch }) => {
|
||||
throw new Error('not implemented');
|
||||
}
|
||||
}
|
||||
: await (await import('$lib/users')).default();
|
||||
await log.setup();
|
||||
: await (await import('$lib/api')).users.CurrentUser();
|
||||
setupLogger();
|
||||
return {
|
||||
projects,
|
||||
user,
|
||||
api: Api({ fetch }),
|
||||
posthog: Posthog()
|
||||
posthog: Posthog(),
|
||||
sentry: Sentry()
|
||||
};
|
||||
};
|
||||
});
|
||||
|
@ -1,6 +1,6 @@
|
||||
<script lang="ts">
|
||||
import { open } from '@tauri-apps/api/dialog';
|
||||
import type { LayoutData } from './$types';
|
||||
import { open } from '@tauri-apps/api/dialog';
|
||||
import { toasts } from '$lib';
|
||||
import { Button, Tooltip } from '$lib/components';
|
||||
|
||||
|
@ -1,18 +1,16 @@
|
||||
<script lang="ts">
|
||||
import type { LayoutData } from './$types';
|
||||
import type { Project } from '$lib/projects';
|
||||
import type { Project } from '$lib/api';
|
||||
import { Button, Tooltip } from '$lib/components';
|
||||
import { page } from '$app/stores';
|
||||
import { goto } from '$app/navigation';
|
||||
import { IconTerminal } from '$lib/components/icons';
|
||||
import { IconSearch, IconSettings, IconTerminal } from '$lib/components/icons';
|
||||
import { onMount } from 'svelte';
|
||||
import tinykeys from 'tinykeys';
|
||||
import { derived } from 'svelte/store';
|
||||
import { format } from 'date-fns';
|
||||
|
||||
export let data: LayoutData;
|
||||
const { project } = data;
|
||||
$: statuses = derived(data.statuses, (statuses) => statuses);
|
||||
|
||||
let query: string;
|
||||
|
||||
@ -30,32 +28,12 @@
|
||||
|
||||
onMount(() =>
|
||||
tinykeys(window, {
|
||||
'Shift+c': (event: KeyboardEvent) => {
|
||||
const target = event.target as HTMLElement;
|
||||
if (target.tagName === 'INPUT' || target.tagName === 'TEXTAREA') return;
|
||||
if (Object.keys($statuses).length === 0) return;
|
||||
$project && goto(`/projects/${$project.id}/commit/`);
|
||||
},
|
||||
'Shift+t': (event: KeyboardEvent) => {
|
||||
const target = event.target as HTMLElement;
|
||||
if (target.tagName === 'INPUT' || target.tagName === 'TEXTAREA') return;
|
||||
$project && goto(`/projects/${$project.id}/terminal/`);
|
||||
},
|
||||
'Shift+p': (event: KeyboardEvent) => {
|
||||
const target = event.target as HTMLElement;
|
||||
if (target.tagName === 'INPUT' || target.tagName === 'TEXTAREA') return;
|
||||
$project && goto(`/projects/${$project.id}/`);
|
||||
},
|
||||
'Meta+Shift+p': (event: KeyboardEvent) => {
|
||||
const target = event.target as HTMLElement;
|
||||
if (target.tagName === 'INPUT' || target.tagName === 'TEXTAREA') return;
|
||||
$project && goto(`/projects/${$project.id}/settings/`);
|
||||
},
|
||||
'Shift+r': (event: KeyboardEvent) => {
|
||||
const target = event.target as HTMLElement;
|
||||
if (target.tagName === 'INPUT' || target.tagName === 'TEXTAREA') return;
|
||||
$project && goto(`/projects/${$project.id}/player/${format(new Date(), 'yyyy-MM-dd')}`);
|
||||
},
|
||||
'Meta+Shift+C': () => $project && goto(`/projects/${$project.id}/commit/`),
|
||||
'Meta+T': () => $project && goto(`/projects/${$project.id}/terminal/`),
|
||||
'Meta+P': () => $project && goto(`/projects/${$project.id}/`),
|
||||
'Meta+Shift+,': () => $project && goto(`/projects/${$project.id}/settings/`),
|
||||
'Meta+R': () =>
|
||||
$project && goto(`/projects/${$project.id}/player/${format(new Date(), 'yyyy-MM-dd')}`),
|
||||
'a i p': () => $project && goto(`/projects/${$project.id}/aiplayground/`)
|
||||
})
|
||||
);
|
||||
@ -76,12 +54,7 @@
|
||||
>
|
||||
<div class="relative">
|
||||
<div class="pointer-events-none absolute inset-y-0 left-0 flex items-center pl-3">
|
||||
<svg class="mr-2 h-5 w-5" viewBox="0 0 20 20" xmlns="http://www.w3.org/2000/svg"
|
||||
><path
|
||||
d="M8 12a4 4 0 110-8 4 4 0 010 8zm9.707 4.293l-4.82-4.82A5.968 5.968 0 0014 8 6 6 0 002 8a6 6 0 006 6 5.968 5.968 0 003.473-1.113l4.82 4.82a.997.997 0 001.414 0 .999.999 0 000-1.414z"
|
||||
fill="#5C5F62"
|
||||
/></svg
|
||||
>
|
||||
<IconSearch class="h-5 w-5 text-zinc-500" />
|
||||
</div>
|
||||
<form
|
||||
on:submit|preventDefault={onSearchSubmit}
|
||||
@ -97,7 +70,6 @@
|
||||
autocomplete="off"
|
||||
aria-label="Search input"
|
||||
class="block w-full min-w-0 flex-1 rounded border border-zinc-700 bg-zinc-800 p-[3px] px-2 pl-10 text-zinc-200 placeholder:text-zinc-500 sm:text-sm sm:leading-6"
|
||||
style=""
|
||||
/>
|
||||
</form>
|
||||
</div>
|
||||
@ -126,25 +98,7 @@
|
||||
class="block rounded p-1 text-zinc-400 hover:bg-zinc-700 hover:text-zinc-200"
|
||||
href="/projects/{$project?.id}/settings"
|
||||
>
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
fill="none"
|
||||
viewBox="0 0 24 24"
|
||||
stroke-width="1.5"
|
||||
stroke="currentColor"
|
||||
class="h-6 w-6"
|
||||
>
|
||||
<path
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
d="M9.594 3.94c.09-.542.56-.94 1.11-.94h2.593c.55 0 1.02.398 1.11.94l.213 1.281c.063.374.313.686.645.87.074.04.147.083.22.127.324.196.72.257 1.075.124l1.217-.456a1.125 1.125 0 011.37.49l1.296 2.247a1.125 1.125 0 01-.26 1.431l-1.003.827c-.293.24-.438.613-.431.992a6.759 6.759 0 010 .255c-.007.378.138.75.43.99l1.005.828c.424.35.534.954.26 1.43l-1.298 2.247a1.125 1.125 0 01-1.369.491l-1.217-.456c-.355-.133-.75-.072-1.076.124a6.57 6.57 0 01-.22.128c-.331.183-.581.495-.644.869l-.213 1.28c-.09.543-.56.941-1.11.941h-2.594c-.55 0-1.02-.398-1.11-.94l-.213-1.281c-.062-.374-.312-.686-.644-.87a6.52 6.52 0 01-.22-.127c-.325-.196-.72-.257-1.076-.124l-1.217.456a1.125 1.125 0 01-1.369-.49l-1.297-2.247a1.125 1.125 0 01.26-1.431l1.004-.827c.292-.24.437-.613.43-.992a6.932 6.932 0 010-.255c.007-.378-.138-.75-.43-.99l-1.004-.828a1.125 1.125 0 01-.26-1.43l1.297-2.247a1.125 1.125 0 011.37-.491l1.216.456c.356.133.751.072 1.076-.124.072-.044.146-.087.22-.128.332-.183.582-.495.644-.869l.214-1.281z"
|
||||
/>
|
||||
<path
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
d="M15 12a3 3 0 11-6 0 3 3 0 016 0z"
|
||||
/>
|
||||
</svg>
|
||||
<IconSettings class="h-6 w-6" />
|
||||
</a>
|
||||
</Tooltip>
|
||||
</li>
|
||||
|
@ -1,27 +1,28 @@
|
||||
import { building } from '$app/environment';
|
||||
import type { Session } from '$lib/sessions';
|
||||
import type { Status } from '$lib/git/statuses';
|
||||
import type { Session, Delta, Status } from '$lib/api';
|
||||
import { readable } from 'svelte/store';
|
||||
import type { LayoutLoad } from './$types';
|
||||
import type { Delta } from '$lib/deltas';
|
||||
import type { Readable } from 'svelte/store';
|
||||
|
||||
export const prerender = false;
|
||||
|
||||
export const load: LayoutLoad = async ({ parent, params }) => {
|
||||
const { projects } = await parent();
|
||||
const sessions = building
|
||||
? readable<Session[]>([])
|
||||
: await import('$lib/sessions').then((m) => m.default({ projectId: params.projectId }));
|
||||
: await import('$lib/api').then((m) => m.sessions.Sessions({ projectId: params.projectId }));
|
||||
const statuses = building
|
||||
? readable<Record<string, Status>>({})
|
||||
: await import('$lib/git/statuses').then((m) => m.default({ projectId: params.projectId }));
|
||||
: await import('$lib/api').then((m) =>
|
||||
m.git.statuses.Statuses({ projectId: params.projectId })
|
||||
);
|
||||
const head = building
|
||||
? readable<string>('')
|
||||
: await import('$lib/git/head').then((m) => m.default({ projectId: params.projectId }));
|
||||
: await import('$lib/api').then((m) => m.git.heads.Head({ projectId: params.projectId }));
|
||||
const deltas = building
|
||||
? () => Promise.resolve(readable<Record<string, Delta[]>>({}))
|
||||
: (sessionId: string) =>
|
||||
import('$lib/deltas').then((m) => m.default({ projectId: params.projectId, sessionId }));
|
||||
import('$lib/api').then((m) => m.deltas.Deltas({ projectId: params.projectId, sessionId }));
|
||||
|
||||
const cache: Record<string, Promise<Readable<Record<string, Delta[]>>>> = {};
|
||||
const cachedDeltas = (sessionId: string) => {
|
||||
|
@ -2,10 +2,9 @@
|
||||
import { getTime, subDays } from 'date-fns';
|
||||
import type { PageData } from './$types';
|
||||
import { derived } from 'svelte/store';
|
||||
import { IconGitBranch } from '$lib/components/icons';
|
||||
import { IconGitBranch, IconLoading } from '$lib/components/icons';
|
||||
import { asyncDerived } from '@square/svelte-store';
|
||||
import type { Delta } from '$lib/deltas';
|
||||
import IconRotateClockwise from '$lib/components/icons/IconRotateClockwise.svelte';
|
||||
import type { Delta } from '$lib/api';
|
||||
import FileSummaries from './FileSummaries.svelte';
|
||||
import { Button, Statuses, Tooltip } from '$lib/components';
|
||||
|
||||
@ -124,7 +123,7 @@
|
||||
<ul class="mr-1 flex flex-col space-y-4 overflow-y-auto pl-8 pr-5 pb-8">
|
||||
{#await fileDeltas.load()}
|
||||
<li>
|
||||
<IconRotateClockwise class="animate-spin" />
|
||||
<IconLoading class="animate-spin" />
|
||||
</li>
|
||||
{:then}
|
||||
<FileSummaries projectId={$project?.id} fileDeltas={$fileDeltas} />
|
||||
|
@ -1,13 +1,16 @@
|
||||
import { building } from '$app/environment';
|
||||
import type { PageLoad } from './$types';
|
||||
import { readable } from 'svelte/store';
|
||||
import type { Activity } from '$lib/git/activity';
|
||||
import type { Activity } from '$lib/api';
|
||||
import { wrapLoadWithSentry } from '@sentry/sveltekit';
|
||||
|
||||
export const load: PageLoad = async ({ params }) => {
|
||||
export const load: PageLoad = wrapLoadWithSentry(async ({ params }) => {
|
||||
const activity = building
|
||||
? readable<Activity[]>([])
|
||||
: await import('$lib/git/activity').then((m) => m.default({ projectId: params.projectId }));
|
||||
: await import('$lib/api').then((m) =>
|
||||
m.git.activities.Activities({ projectId: params.projectId })
|
||||
);
|
||||
return {
|
||||
activity
|
||||
};
|
||||
};
|
||||
});
|
||||
|
@ -1,5 +1,5 @@
|
||||
<script lang="ts">
|
||||
import type { Delta } from '$lib/deltas';
|
||||
import type { Delta } from '$lib/api';
|
||||
import { bucketByTimestamp } from './histogram';
|
||||
|
||||
export let deltas: Delta[];
|
||||
|
@ -1,6 +1,6 @@
|
||||
<script lang="ts">
|
||||
import { format, startOfDay } from 'date-fns';
|
||||
import type { Delta } from '$lib/deltas';
|
||||
import type { Delta } from '$lib/api';
|
||||
import { derived, type Readable } from 'svelte/store';
|
||||
import { collapsable } from '$lib/paths';
|
||||
import FileActivity from './FileActivity.svelte';
|
||||
|
@ -3,8 +3,7 @@
|
||||
import { Button } from '$lib/components';
|
||||
import { collapsable } from '$lib/paths';
|
||||
import { derived, writable } from 'svelte/store';
|
||||
import * as git from '$lib/git';
|
||||
import { Status } from '$lib/git/statuses';
|
||||
import { git, Status } from '$lib/api';
|
||||
import DiffViewer from '$lib/components/DiffViewer.svelte';
|
||||
import { error, success } from '$lib/toasts';
|
||||
import { fly } from 'svelte/transition';
|
||||
@ -161,7 +160,7 @@
|
||||
|
||||
<Dialog bind:this={connectToCloudDialog}>
|
||||
<svelte:fragment slot="title">GitButler Cloud required</svelte:fragment>
|
||||
<svelte:fragment>
|
||||
<div class="w-[640px]">
|
||||
<p class="py-2">
|
||||
By connecting to GitButler Cloud you'll unlock improved, cloud only features, including
|
||||
AI-generated commit summaries, and the assurance of never losing your work with synced
|
||||
@ -181,28 +180,22 @@
|
||||
secure and easily recoverable.
|
||||
</span>
|
||||
</p>
|
||||
</svelte:fragment>
|
||||
<svelte:fragment slot="controls" let:hide>
|
||||
<Button filled on:click={hide}>Cancel</Button>
|
||||
<Button
|
||||
filled
|
||||
role="primary"
|
||||
on:click={() => {
|
||||
enableProjectSync();
|
||||
hide();
|
||||
}}>Connect</Button
|
||||
>
|
||||
</div>
|
||||
<svelte:fragment slot="controls" let:close>
|
||||
<Button filled={false} outlined={true} on:click={close}>Cancel</Button>
|
||||
<Button role="primary" on:click={() => enableProjectSync().finally(close)}>Connect</Button>
|
||||
</svelte:fragment>
|
||||
</Dialog>
|
||||
<div id="commit-page" class="flex h-full w-full">
|
||||
<div class="commit-panel-container flex flex-col border-r border-zinc-700 p-4 pt-2">
|
||||
<h1 class="pb-2 text-xl font-bold">Commit</h1>
|
||||
|
||||
<div class="commit-panel-container flex h-full flex-col border-r border-zinc-700 px-4">
|
||||
<form
|
||||
on:submit|preventDefault={onCommit}
|
||||
class="flex h-full w-1/3 min-w-[500px] flex-col gap-4"
|
||||
>
|
||||
<ul class="flex h-full w-full flex-col rounded border border-gb-700 bg-card-default pb-1">
|
||||
<h1 class="pt-2 text-xl font-bold">Commit</h1>
|
||||
<ul
|
||||
class="flex h-full w-full flex-col overflow-auto rounded border border-gb-700 bg-card-default"
|
||||
>
|
||||
<header class="flex w-full items-center rounded-tl rounded-tr bg-card-active p-2">
|
||||
<input
|
||||
type="checkbox"
|
||||
@ -220,9 +213,9 @@
|
||||
</h1>
|
||||
</header>
|
||||
|
||||
<div class="changed-file-list-container overflow-y-auto">
|
||||
<div class="changed-file-list-container h-100 overflow-y-auto">
|
||||
{#each Object.entries($statuses) as [path, status]}
|
||||
<li class="bg-card-default">
|
||||
<li class="bg-card-default last:mb-1">
|
||||
<div
|
||||
class:bg-[#3356C2]={$selectedDiffPath === path}
|
||||
class:hover:bg-divider={$selectedDiffPath !== path}
|
||||
@ -260,73 +253,75 @@
|
||||
</div>
|
||||
</ul>
|
||||
|
||||
<input
|
||||
name="summary"
|
||||
class="
|
||||
w-full rounded border border-zinc-600 bg-zinc-700 p-2 text-zinc-100
|
||||
hover:border-zinc-500/80
|
||||
focus:border-[1px] focus:focus:border-blue-600
|
||||
focus:ring-2 focus:ring-blue-600/30
|
||||
"
|
||||
disabled={isGeneratingCommitMessage || isCommitting}
|
||||
type="text"
|
||||
placeholder="Summary (required)"
|
||||
bind:value={summary}
|
||||
required
|
||||
/>
|
||||
|
||||
<div class="commit-description-container relative h-36">
|
||||
{#if isGeneratingCommitMessage}
|
||||
<div
|
||||
in:fly={{ y: 8, duration: 500 }}
|
||||
out:fly={{ y: -8, duration: 500 }}
|
||||
class="generating-commit absolute top-0 right-0 bottom-0 left-0 rounded border-2 border-[#502E5C] "
|
||||
>
|
||||
<div
|
||||
class="generating-commit-message absolute bottom-0 left-0 rounded-tr bg-[#782E94] bg-gradient-to-b from-[#623871] to-[#502E5C] py-1 px-2"
|
||||
>
|
||||
<span>✨ Summarizing changes</span>
|
||||
<span class="dot-container">
|
||||
<div class="dot" />
|
||||
<div class="dot" />
|
||||
<div class="dot" />
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
{/if}
|
||||
<textarea
|
||||
name="description"
|
||||
disabled={isGeneratingCommitMessage || isCommitting}
|
||||
<div class="bottom-controller-container flex flex-col gap-2 pb-4">
|
||||
<input
|
||||
name="summary"
|
||||
class="
|
||||
h-full w-full rounded border border-zinc-600 bg-zinc-700 p-2 text-zinc-100
|
||||
w-full rounded border border-zinc-600 bg-zinc-700 p-2 text-zinc-100
|
||||
hover:border-zinc-500/80
|
||||
focus:border-[1px] focus:focus:border-blue-600
|
||||
focus:ring-2 focus:ring-blue-600/30
|
||||
"
|
||||
rows="10"
|
||||
placeholder="Description (optional)"
|
||||
bind:value={description}
|
||||
disabled={isGeneratingCommitMessage || isCommitting}
|
||||
type="text"
|
||||
placeholder="Summary (required)"
|
||||
bind:value={summary}
|
||||
required
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div class="flex justify-between">
|
||||
<Button
|
||||
role="purple"
|
||||
disabled={!isGenerateCommitEnabled}
|
||||
on:click={onGenerateCommitMessage}
|
||||
loading={isGeneratingCommitMessage}
|
||||
>
|
||||
✨ Autowrite
|
||||
</Button>
|
||||
<div class="commit-description-container relative h-36">
|
||||
{#if isGeneratingCommitMessage}
|
||||
<div
|
||||
in:fly={{ y: 8, duration: 500 }}
|
||||
out:fly={{ y: -8, duration: 500 }}
|
||||
class="generating-commit absolute top-0 right-0 bottom-0 left-0 rounded border-2 border-[#502E5C] "
|
||||
>
|
||||
<div
|
||||
class="generating-commit-message absolute bottom-0 left-0 rounded-tr bg-[#782E94] bg-gradient-to-b from-[#623871] to-[#502E5C] py-1 px-2"
|
||||
>
|
||||
<span>✨ Summarizing changes</span>
|
||||
<span class="dot-container">
|
||||
<div class="dot" />
|
||||
<div class="dot" />
|
||||
<div class="dot" />
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
{/if}
|
||||
<textarea
|
||||
name="description"
|
||||
disabled={isGeneratingCommitMessage || isCommitting}
|
||||
class="
|
||||
h-full w-full resize-none rounded border border-zinc-600 bg-zinc-700 p-2 text-zinc-100
|
||||
hover:border-zinc-500/80
|
||||
focus:border-[1px] focus:focus:border-blue-600
|
||||
focus:ring-2 focus:ring-blue-600/30
|
||||
"
|
||||
rows="10"
|
||||
placeholder="Description (optional)"
|
||||
bind:value={description}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<Button
|
||||
loading={isCommitting}
|
||||
disabled={!isCommitEnabled || isGeneratingCommitMessage}
|
||||
role="primary"
|
||||
type="submit"
|
||||
>
|
||||
Commit changes
|
||||
</Button>
|
||||
<div class="flex justify-between">
|
||||
<Button
|
||||
role="purple"
|
||||
disabled={!isGenerateCommitEnabled}
|
||||
on:click={onGenerateCommitMessage}
|
||||
loading={isGeneratingCommitMessage}
|
||||
>
|
||||
✨ Autowrite
|
||||
</Button>
|
||||
|
||||
<Button
|
||||
loading={isCommitting}
|
||||
disabled={!isCommitEnabled || isGeneratingCommitMessage}
|
||||
role="primary"
|
||||
type="submit"
|
||||
>
|
||||
Commit changes
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
|
@ -1,14 +1,15 @@
|
||||
import { building } from '$app/environment';
|
||||
import { readable } from 'svelte/store';
|
||||
import type { PageLoad } from '../$types';
|
||||
import { wrapLoadWithSentry } from '@sentry/sveltekit';
|
||||
|
||||
export const load: PageLoad = async ({ parent, params }) => {
|
||||
export const load: PageLoad = wrapLoadWithSentry(async ({ parent, params }) => {
|
||||
const { project } = await parent();
|
||||
const diffs = building
|
||||
? readable<Record<string, string>>({})
|
||||
: await import('$lib/git/diffs').then((m) => m.default({ projectId: params.projectId }));
|
||||
: await import('$lib/api').then((m) => m.git.diffs.Diffs({ projectId: params.projectId }));
|
||||
return {
|
||||
diffs,
|
||||
project
|
||||
};
|
||||
};
|
||||
});
|
||||
|
15
src/routes/projects/[projectId]/player/+page.ts
Normal file
@ -0,0 +1,15 @@
|
||||
import { redirect } from '@sveltejs/kit';
|
||||
import { format } from 'date-fns';
|
||||
import { get } from 'svelte/store';
|
||||
import type { PageLoad } from './$types';
|
||||
import { wrapLoadWithSentry } from '@sentry/sveltekit';
|
||||
|
||||
export const load: PageLoad = wrapLoadWithSentry(async ({ parent, url }) => {
|
||||
const { sessions, projectId } = await parent();
|
||||
const date = format(new Date(), 'yyyy-MM-dd');
|
||||
const dateSessions = get(sessions).filter(
|
||||
(session) => format(session.meta.startTimestampMs, 'yyyy-MM-dd') === date
|
||||
);
|
||||
const firstSession = dateSessions[dateSessions.length - 1];
|
||||
throw redirect(302, `/projects/${projectId}/player/${date}/${firstSession.id}${url.search}`);
|
||||
});
|
@ -1,6 +1,6 @@
|
||||
<script lang="ts">
|
||||
import { page } from '$app/stores';
|
||||
import { list as listDeltas } from '$lib/deltas';
|
||||
import { deltas } from '$lib/api';
|
||||
import { asyncDerived } from '@square/svelte-store';
|
||||
import { format } from 'date-fns';
|
||||
import { derived } from 'svelte/store';
|
||||
@ -14,7 +14,7 @@
|
||||
const dates = asyncDerived([sessions, fileFilter], async ([sessions, fileFilter]) => {
|
||||
const sessionDeltas = await Promise.all(
|
||||
sessions.map((session) =>
|
||||
listDeltas({
|
||||
deltas.list({
|
||||
projectId,
|
||||
sessionId: session.id,
|
||||
paths: fileFilter ? [fileFilter] : undefined
|
||||
|
@ -2,8 +2,9 @@ import { redirect } from '@sveltejs/kit';
|
||||
import { format } from 'date-fns';
|
||||
import { get } from 'svelte/store';
|
||||
import type { PageLoad } from './$types';
|
||||
import { wrapLoadWithSentry } from '@sentry/sveltekit';
|
||||
|
||||
export const load: PageLoad = async ({ parent, params, url }) => {
|
||||
export const load: PageLoad = wrapLoadWithSentry(async ({ parent, params, url }) => {
|
||||
const { sessions, projectId } = await parent();
|
||||
const dateSessions = get(sessions).filter(
|
||||
(session) => format(session.meta.startTimestampMs, 'yyyy-MM-dd') === params.date
|
||||
@ -13,4 +14,4 @@ export const load: PageLoad = async ({ parent, params, url }) => {
|
||||
302,
|
||||
`/projects/${projectId}/player/${params.date}/${firstSession.id}${url.search}`
|
||||
);
|
||||
};
|
||||
});
|
||||
|
@ -1,20 +1,21 @@
|
||||
<script lang="ts" context="module">
|
||||
import { listFiles, type Session } from '$lib/sessions';
|
||||
import { list as listDeltas, type Delta } from '$lib/deltas';
|
||||
import { deltas, files, type Session, type Delta } from '$lib/api';
|
||||
const enrichSession = async (projectId: string, session: Session, paths?: string[]) => {
|
||||
const files = await listFiles({ projectId, sessionId: session.id, paths });
|
||||
const deltas = await listDeltas({ projectId, sessionId: session.id, paths }).then((deltas) =>
|
||||
Object.entries(deltas)
|
||||
.flatMap(([path, deltas]) => deltas.map((delta) => [path, delta] as [string, Delta]))
|
||||
.sort((a, b) => a[1].timestampMs - b[1].timestampMs)
|
||||
);
|
||||
const deltasFiles = new Set(deltas.map(([path]) => path));
|
||||
const sessionFiles = await files.list({ projectId, sessionId: session.id, paths });
|
||||
const sessionDeltas = await deltas
|
||||
.list({ projectId, sessionId: session.id, paths })
|
||||
.then((deltas) =>
|
||||
Object.entries(deltas)
|
||||
.flatMap(([path, deltas]) => deltas.map((delta) => [path, delta] as [string, Delta]))
|
||||
.sort((a, b) => a[1].timestampMs - b[1].timestampMs)
|
||||
);
|
||||
const deltasFiles = new Set(sessionDeltas.map(([path]) => path));
|
||||
return {
|
||||
...session,
|
||||
files: Object.fromEntries(
|
||||
Object.entries(files).filter(([filepath]) => deltasFiles.has(filepath))
|
||||
Object.entries(sessionFiles).filter(([filepath]) => deltasFiles.has(filepath))
|
||||
),
|
||||
deltas
|
||||
deltas: sessionDeltas
|
||||
};
|
||||
};
|
||||
</script>
|
||||
|
@ -1,7 +1,6 @@
|
||||
<script lang="ts">
|
||||
import type { Delta } from '$lib/deltas';
|
||||
import type { Delta } from '$lib/api';
|
||||
import slider from '$lib/slider';
|
||||
import { onMount } from 'svelte';
|
||||
|
||||
type RichSession = { id: string; deltas: [string, Delta][] };
|
||||
export let sessions: RichSession[];
|
||||
|
@ -1,12 +1,10 @@
|
||||
<script lang="ts">
|
||||
import type { PageData } from './$types';
|
||||
import { search, type SearchResult } from '$lib';
|
||||
import { IconChevronLeft, IconChevronRight } from '$lib/components/icons';
|
||||
import { listFiles } from '$lib/sessions';
|
||||
import { files, deltas, searchResults, type SearchResult } from '$lib/api';
|
||||
import { asyncDerived } from '@square/svelte-store';
|
||||
import { IconRotateClockwise } from '$lib/components/icons';
|
||||
import { IconLoading } from '$lib/components/icons';
|
||||
import { format, formatDistanceToNow } from 'date-fns';
|
||||
import { list as listDeltas } from '$lib/deltas';
|
||||
import { CodeViewer } from '$lib/components';
|
||||
import { page } from '$app/stores';
|
||||
import { derived } from 'svelte/store';
|
||||
@ -29,16 +27,17 @@
|
||||
index,
|
||||
highlighted
|
||||
}: SearchResult) => {
|
||||
const [doc, deltas] = await Promise.all([
|
||||
listFiles({ projectId, sessionId, paths: [filePath] }).then((r) => r[filePath] ?? ''),
|
||||
listDeltas({ projectId, sessionId, paths: [filePath] })
|
||||
const [doc, dd] = await Promise.all([
|
||||
files.list({ projectId, sessionId, paths: [filePath] }).then((r) => r[filePath] ?? ''),
|
||||
deltas
|
||||
.list({ projectId, sessionId, paths: [filePath] })
|
||||
.then((r) => r[filePath] ?? [])
|
||||
.then((d) => d.slice(0, index + 1))
|
||||
]);
|
||||
const date = format(deltas[deltas.length - 1].timestampMs, 'yyyy-MM-dd');
|
||||
const date = format(dd[dd.length - 1].timestampMs, 'yyyy-MM-dd');
|
||||
return {
|
||||
doc,
|
||||
deltas,
|
||||
deltas: dd,
|
||||
filepath: filePath,
|
||||
highlight: highlighted,
|
||||
sessionId,
|
||||
@ -47,11 +46,11 @@
|
||||
};
|
||||
};
|
||||
|
||||
const { store: searchResults, state: searchState } = asyncDerived(
|
||||
const { store: results, state: searchState } = asyncDerived(
|
||||
[query, project, offset],
|
||||
async ([query, project, offset]) => {
|
||||
if (!query || !project) return { page: [], total: 0, haveNext: false, havePrev: false };
|
||||
const results = await search({ projectId: project.id, query, limit, offset });
|
||||
const results = await searchResults.list({ projectId: project.id, query, limit, offset });
|
||||
return {
|
||||
page: await Promise.all(results.page.map(fetchResultData)),
|
||||
haveNext: offset + limit < results.total,
|
||||
@ -70,7 +69,7 @@
|
||||
</figcaption>
|
||||
|
||||
<div class="mx-auto">
|
||||
<IconRotateClockwise class="h-20 w-20 animate-spin" />
|
||||
<IconLoading class="h-20 w-20 animate-spin" />
|
||||
</div>
|
||||
{:else if $searchState?.isError}
|
||||
<figcaption>
|
||||
@ -78,16 +77,16 @@
|
||||
</figcaption>
|
||||
{:else if $searchState?.isLoaded}
|
||||
<figcaption class="mt-14">
|
||||
{#if $searchResults.total > 0}
|
||||
{#if $results.total > 0}
|
||||
<p class="mb-2 text-xl text-[#D4D4D8]">Results for "{$query}"</p>
|
||||
<p class="text-lg text-[#717179]">{$searchResults.total} change instances</p>
|
||||
<p class="text-lg text-[#717179]">{$results.total} change instances</p>
|
||||
{:else}
|
||||
<p class="mb-2 text-xl text-[#D4D4D8]">No results for "{$query}"</p>
|
||||
{/if}
|
||||
</figcaption>
|
||||
|
||||
<ul class="search-result-list -mr-14 flex flex-auto flex-col gap-6 overflow-auto pb-6">
|
||||
{#each $searchResults.page as { doc, deltas, filepath, highlight, sessionId, projectId, date }}
|
||||
{#each $results.page as { doc, deltas, filepath, highlight, sessionId, projectId, date }}
|
||||
{@const timestamp = deltas[deltas.length - 1].timestampMs}
|
||||
<li class="search-result mr-14">
|
||||
<a
|
||||
@ -111,18 +110,18 @@
|
||||
<nav class="mx-auto flex text-zinc-400">
|
||||
<button
|
||||
on:click={openPrevPage}
|
||||
disabled={!$searchResults.havePrev}
|
||||
disabled={!$results.havePrev}
|
||||
title="Back"
|
||||
class:text-zinc-50={$searchResults.havePrev}
|
||||
class:text-zinc-50={$results.havePrev}
|
||||
class="h-9 w-9 rounded-tl-md rounded-bl-md border border-r-0 border-zinc-700 hover:bg-zinc-700"
|
||||
>
|
||||
<IconChevronLeft class="ml-1 h-5 w-6" />
|
||||
</button>
|
||||
<button
|
||||
on:click={openNextPage}
|
||||
disabled={!$searchResults.haveNext}
|
||||
disabled={!$results.haveNext}
|
||||
title="Next"
|
||||
class:text-zinc-50={$searchResults.haveNext}
|
||||
class:text-zinc-50={$results.haveNext}
|
||||
class="h-9 w-9 rounded-tr-md rounded-br-md border border-l border-zinc-700 hover:bg-zinc-700"
|
||||
>
|
||||
<IconChevronRight class="ml-1 h-5 w-6" />
|
||||
|
@ -3,7 +3,7 @@
|
||||
import ResizeObserver from 'svelte-resize-observer';
|
||||
import setupTerminal from './terminal';
|
||||
import 'xterm/css/xterm.css';
|
||||
import type { Project } from '$lib/projects';
|
||||
import type { Project } from '$lib/api';
|
||||
import { debounce } from '$lib/utils';
|
||||
import { Button, Statuses } from '$lib/components';
|
||||
|
||||
|
@ -1,4 +1,4 @@
|
||||
import type { Project } from '$lib/projects';
|
||||
import type { Project } from '$lib/api';
|
||||
import { Terminal } from 'xterm';
|
||||
import { CanvasAddon } from 'xterm-addon-canvas';
|
||||
import { WebglAddon } from 'xterm-addon-webgl';
|
||||
|
@ -6,6 +6,9 @@ const config = {
|
||||
preprocess: vitePreprocess(),
|
||||
kit: {
|
||||
adapter: staticAdapter({
|
||||
pages: 'build',
|
||||
assets: 'build',
|
||||
fallback: 'index.html',
|
||||
precompress: true,
|
||||
strict: false
|
||||
})
|
||||
|
@ -1,8 +1,28 @@
|
||||
import { defineConfig } from 'vite';
|
||||
import { sveltekit } from '@sveltejs/kit/vite';
|
||||
import { sentrySvelteKit } from '@sentry/sveltekit';
|
||||
|
||||
export default defineConfig({
|
||||
plugins: [sveltekit()],
|
||||
plugins: [
|
||||
sentrySvelteKit({
|
||||
sourceMapsUploadOptions: {
|
||||
org: 'gitbutler',
|
||||
project: 'desktop',
|
||||
// this is nikita galaiko's personal sentry api token.
|
||||
authToken: '04c6bc1df15346f39ed2fbeb99c0a8e25bcbedc4aba9461bb3a471733b8c80db',
|
||||
include: ['build'],
|
||||
cleanArtifacts: true,
|
||||
setCommits: {
|
||||
auto: true,
|
||||
ignoreMissing: true,
|
||||
ignoreEmpty: true
|
||||
},
|
||||
telemetry: false,
|
||||
uploadSourceMaps: process.env.SENTRY_RELEASE !== undefined
|
||||
}
|
||||
}),
|
||||
sveltekit()
|
||||
],
|
||||
|
||||
// Vite options tailored for Tauri development and only applied in `tauri dev` or `tauri build`
|
||||
// prevent vite from obscuring rust errors
|