basic working history (#104)
This commit is contained in:
parent
d6684ccb21
commit
7638f6b827
17 changed files with 1427 additions and 15 deletions
BIN
bun.lockb
BIN
bun.lockb
Binary file not shown.
|
|
@ -19,6 +19,7 @@
|
|||
"@tauri-apps/plugin-opener": "^2",
|
||||
"@tauri-apps/plugin-os": "~2",
|
||||
"@tauri-apps/plugin-process": "~2",
|
||||
"@tauri-apps/plugin-sql": "~2",
|
||||
"@tauri-apps/plugin-store": "~2",
|
||||
"@tauri-apps/plugin-stronghold": "~2",
|
||||
"@tauri-apps/plugin-updater": "~2",
|
||||
|
|
|
|||
654
src-tauri/Cargo.lock
generated
654
src-tauri/Cargo.lock
generated
|
|
@ -17,6 +17,18 @@ version = "2.0.0"
|
|||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "512761e0bb2578dd7380c6baaa0f4ce03e84f95e960231d1dec8bf4d7d6e2627"
|
||||
|
||||
[[package]]
|
||||
name = "ahash"
|
||||
version = "0.8.12"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "5a15f179cd60c4584b8a8c596927aadc462e27f2ca70c04e0071964a73ba7a75"
|
||||
dependencies = [
|
||||
"cfg-if",
|
||||
"once_cell",
|
||||
"version_check",
|
||||
"zerocopy 0.8.26",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "aho-corasick"
|
||||
version = "1.1.3"
|
||||
|
|
@ -41,6 +53,12 @@ dependencies = [
|
|||
"alloc-no-stdlib",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "allocator-api2"
|
||||
version = "0.2.21"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "683d7910e743518b0e34f1186f92494becacb047c7b6bf616c96772180fef923"
|
||||
|
||||
[[package]]
|
||||
name = "alsa"
|
||||
version = "0.9.1"
|
||||
|
|
@ -322,6 +340,15 @@ dependencies = [
|
|||
"system-deps",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "atoi"
|
||||
version = "2.0.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "f28d99ec8bfea296261ca1af174f24225171fea9664ba9003cbebee704810528"
|
||||
dependencies = [
|
||||
"num-traits",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "atomic-waker"
|
||||
version = "1.1.2"
|
||||
|
|
@ -372,6 +399,12 @@ version = "0.22.1"
|
|||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "72b3254f16251a8381aa12e40e3c4d2f0199f8c6508fbecb9d91f575e0fbb8c6"
|
||||
|
||||
[[package]]
|
||||
name = "base64ct"
|
||||
version = "1.8.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "55248b47b0caf0546f7988906588779981c43bb1bc9d0c44087278f80cdb44ba"
|
||||
|
||||
[[package]]
|
||||
name = "bindgen"
|
||||
version = "0.69.5"
|
||||
|
|
@ -642,8 +675,10 @@ checksum = "7e36cc9d416881d2e24f9a963be5fb1cd90966419ac844274161d10488b3e825"
|
|||
dependencies = [
|
||||
"android-tzdata",
|
||||
"iana-time-zone",
|
||||
"js-sys",
|
||||
"num-traits",
|
||||
"serde",
|
||||
"wasm-bindgen",
|
||||
"windows-targets 0.52.6",
|
||||
]
|
||||
|
||||
|
|
@ -731,6 +766,12 @@ dependencies = [
|
|||
"crossbeam-utils",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "const-oid"
|
||||
version = "0.9.6"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "c2459377285ad874054d797f3ccebf984978aa39129f6eafde5cdc8315b612f8"
|
||||
|
||||
[[package]]
|
||||
name = "convert_case"
|
||||
version = "0.4.0"
|
||||
|
|
@ -896,6 +937,21 @@ dependencies = [
|
|||
"libc",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "crc"
|
||||
version = "3.3.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "9710d3b3739c2e349eb44fe848ad0b7c8cb1e42bd87ee49371df2f7acaf3e675"
|
||||
dependencies = [
|
||||
"crc-catalog",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "crc-catalog"
|
||||
version = "2.4.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "19d374276b40fb8bbdee95aef7c7fa6b5316ec764510eb64b8dd0e2ed0d7e7f5"
|
||||
|
||||
[[package]]
|
||||
name = "crc32fast"
|
||||
version = "1.4.2"
|
||||
|
|
@ -914,6 +970,15 @@ dependencies = [
|
|||
"crossbeam-utils",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "crossbeam-queue"
|
||||
version = "0.3.12"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "0f58bbc28f91df819d0aa2a2c00cd19754769c2fad90579b3592b1c9ba7a3115"
|
||||
dependencies = [
|
||||
"crossbeam-utils",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "crossbeam-utils"
|
||||
version = "0.8.21"
|
||||
|
|
@ -1023,6 +1088,17 @@ version = "0.11.0"
|
|||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "0c87e182de0887fd5361989c677c4e8f5000cd9491d6d563161a8f3a5519fc7f"
|
||||
|
||||
[[package]]
|
||||
name = "der"
|
||||
version = "0.7.10"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "e7c1832837b905bbfb5101e07cc24c8deddf52f93225eee6ead5f4d63d53ddcb"
|
||||
dependencies = [
|
||||
"const-oid",
|
||||
"pem-rfc7468",
|
||||
"zeroize",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "deranged"
|
||||
version = "0.3.11"
|
||||
|
|
@ -1064,7 +1140,9 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
|
|||
checksum = "9ed9a281f7bc9b7576e61468ba615a66a5c8cfdff42420a70aa82701a3b1e292"
|
||||
dependencies = [
|
||||
"block-buffer",
|
||||
"const-oid",
|
||||
"crypto-common",
|
||||
"subtle",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
|
|
@ -1158,6 +1236,12 @@ dependencies = [
|
|||
"syn 2.0.96",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "dotenvy"
|
||||
version = "0.15.7"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "1aaf95b3e5c8f23aa320147307562d361db0ae0d51242340f558153b4eb2439b"
|
||||
|
||||
[[package]]
|
||||
name = "dpi"
|
||||
version = "0.1.1"
|
||||
|
|
@ -1211,6 +1295,9 @@ name = "either"
|
|||
version = "1.13.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "60b1af1c220855b6ceac025d3f6ecdd2b7c4894bfe9cd9bda4fbb4bc7c0d4cf0"
|
||||
dependencies = [
|
||||
"serde",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "embed-resource"
|
||||
|
|
@ -1374,6 +1461,17 @@ version = "3.3.1"
|
|||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "a5d9305ccc6942a704f4335694ecd3de2ea531b114ac2d51f5f843750787a92f"
|
||||
|
||||
[[package]]
|
||||
name = "etcetera"
|
||||
version = "0.8.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "136d1b5283a1ab77bd9257427ffd09d8667ced0570b6f938942bc7568ed5b943"
|
||||
dependencies = [
|
||||
"cfg-if",
|
||||
"home",
|
||||
"windows-sys 0.48.0",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "event-listener"
|
||||
version = "5.4.0"
|
||||
|
|
@ -1411,6 +1509,18 @@ dependencies = [
|
|||
"once_cell",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "fallible-iterator"
|
||||
version = "0.3.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "2acce4a10f12dc2fb14a218589d4f1f62ef011b2d0cc4b3cb1bba8e94da14649"
|
||||
|
||||
[[package]]
|
||||
name = "fallible-streaming-iterator"
|
||||
version = "0.1.9"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "7360491ce676a36bf9bb3c56c1aa791658183a54d2744120f27285738d90465a"
|
||||
|
||||
[[package]]
|
||||
name = "fastrand"
|
||||
version = "2.3.0"
|
||||
|
|
@ -1458,12 +1568,29 @@ dependencies = [
|
|||
"miniz_oxide",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "flume"
|
||||
version = "0.11.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "da0e4dd2a88388a1f4ccc7c9ce104604dab68d9f408dc34cd45823d5a9069095"
|
||||
dependencies = [
|
||||
"futures-core",
|
||||
"futures-sink",
|
||||
"spin",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "fnv"
|
||||
version = "1.0.7"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "3f9eec918d3f24069decb9af1554cad7c880e2da24a9afd88aca000531ab82c1"
|
||||
|
||||
[[package]]
|
||||
name = "foldhash"
|
||||
version = "0.1.5"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "d9c4f5dac5e15c24eb999c26181a6ca40b39fe946cbe4c263c7209467bc83af2"
|
||||
|
||||
[[package]]
|
||||
name = "foreign-types"
|
||||
version = "0.3.2"
|
||||
|
|
@ -1538,6 +1665,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
|
|||
checksum = "2dff15bf788c671c1934e366d07e30c1814a8ef514e1af724a602e8a2fbe1b10"
|
||||
dependencies = [
|
||||
"futures-core",
|
||||
"futures-sink",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
|
|
@ -1557,6 +1685,17 @@ dependencies = [
|
|||
"futures-util",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "futures-intrusive"
|
||||
version = "0.5.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "1d930c203dd0b6ff06e0201a4a2fe9149b43c684fd4420555b26d21b1a02956f"
|
||||
dependencies = [
|
||||
"futures-core",
|
||||
"lock_api",
|
||||
"parking_lot",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "futures-io"
|
||||
version = "0.3.31"
|
||||
|
|
@ -1995,6 +2134,7 @@ name = "handy"
|
|||
version = "0.3.9"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"chrono",
|
||||
"cpal",
|
||||
"enigo",
|
||||
"env_logger",
|
||||
|
|
@ -2008,6 +2148,7 @@ dependencies = [
|
|||
"reqwest 0.11.27",
|
||||
"rodio",
|
||||
"rubato",
|
||||
"rusqlite",
|
||||
"rustfft",
|
||||
"serde",
|
||||
"serde_json",
|
||||
|
|
@ -2023,6 +2164,7 @@ dependencies = [
|
|||
"tauri-plugin-os",
|
||||
"tauri-plugin-process",
|
||||
"tauri-plugin-single-instance",
|
||||
"tauri-plugin-sql",
|
||||
"tauri-plugin-store",
|
||||
"tauri-plugin-updater",
|
||||
"tokio",
|
||||
|
|
@ -2036,11 +2178,43 @@ version = "0.12.3"
|
|||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "8a9ee70c43aaf417c914396645a0fa852624801b24ebb7ae78fe8272889ac888"
|
||||
|
||||
[[package]]
|
||||
name = "hashbrown"
|
||||
version = "0.14.5"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "e5274423e17b7c9fc20b6e7e208532f9b19825d82dfd615708b70edd83df41f1"
|
||||
dependencies = [
|
||||
"ahash",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "hashbrown"
|
||||
version = "0.15.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "bf151400ff0baff5465007dd2f3e717f3fe502074ca563069ce3a6629d07b289"
|
||||
dependencies = [
|
||||
"allocator-api2",
|
||||
"equivalent",
|
||||
"foldhash",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "hashlink"
|
||||
version = "0.9.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "6ba4ff7128dee98c7dc9794b6a411377e1404dba1c97deb8d1a55297bd25d8af"
|
||||
dependencies = [
|
||||
"hashbrown 0.14.5",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "hashlink"
|
||||
version = "0.10.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "7382cf6263419f2d8df38c55d7da83da5c18aef87fc7a7fc1fb1e344edfe14c1"
|
||||
dependencies = [
|
||||
"hashbrown 0.15.2",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "heck"
|
||||
|
|
@ -2066,6 +2240,24 @@ version = "0.4.3"
|
|||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "7f24254aa9a54b5c858eaee2f5bccdb46aaf0e486a595ed5fd8f86ba55232a70"
|
||||
|
||||
[[package]]
|
||||
name = "hkdf"
|
||||
version = "0.12.4"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "7b5f8eb2ad728638ea2c7d47a21db23b7b58a72ed6a38256b8a1849f15fbbdf7"
|
||||
dependencies = [
|
||||
"hmac",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "hmac"
|
||||
version = "0.12.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "6c49c37c09c17a53d937dfbb742eb3a961d65a994e6bcdcf37e7399d0cc8ab5e"
|
||||
dependencies = [
|
||||
"digest",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "home"
|
||||
version = "0.5.11"
|
||||
|
|
@ -2149,6 +2341,12 @@ dependencies = [
|
|||
"pin-project-lite",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "http-range"
|
||||
version = "0.1.5"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "21dec9db110f5f872ed9699c3ecf50cf16f423502706ba5c72462e28d3157573"
|
||||
|
||||
[[package]]
|
||||
name = "httparse"
|
||||
version = "1.10.0"
|
||||
|
|
@ -2665,6 +2863,9 @@ name = "lazy_static"
|
|||
version = "1.5.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "bbd2bcb4c963f2ddae06a2efc7e9f3591312473c50c6685e1f298068316e66fe"
|
||||
dependencies = [
|
||||
"spin",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "lazycell"
|
||||
|
|
@ -2722,6 +2923,12 @@ dependencies = [
|
|||
"windows-targets 0.52.6",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "libm"
|
||||
version = "0.2.15"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "f9fbbcab51052fe104eb5e5d351cf728d30a5be1fe14d9be8a3b097481fb97de"
|
||||
|
||||
[[package]]
|
||||
name = "libredox"
|
||||
version = "0.1.3"
|
||||
|
|
@ -2742,6 +2949,17 @@ dependencies = [
|
|||
"cmake",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "libsqlite3-sys"
|
||||
version = "0.30.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "2e99fb7a497b1e3339bc746195567ed8d3e24945ecd636e3619d20b9de9e9149"
|
||||
dependencies = [
|
||||
"cc",
|
||||
"pkg-config",
|
||||
"vcpkg",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "linux-raw-sys"
|
||||
version = "0.4.15"
|
||||
|
|
@ -2851,6 +3069,16 @@ dependencies = [
|
|||
"rawpointer",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "md-5"
|
||||
version = "0.10.6"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "d89e7ee0cfbedfc4da3340218492196241d89eefb6dab27de5df917a6d2e78cf"
|
||||
dependencies = [
|
||||
"cfg-if",
|
||||
"digest",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "memchr"
|
||||
version = "2.7.4"
|
||||
|
|
@ -3072,6 +3300,23 @@ dependencies = [
|
|||
"num-traits",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "num-bigint-dig"
|
||||
version = "0.8.4"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "dc84195820f291c7697304f3cbdadd1cb7199c0efc917ff5eafd71225c136151"
|
||||
dependencies = [
|
||||
"byteorder",
|
||||
"lazy_static",
|
||||
"libm",
|
||||
"num-integer",
|
||||
"num-iter",
|
||||
"num-traits",
|
||||
"rand 0.8.5",
|
||||
"smallvec",
|
||||
"zeroize",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "num-complex"
|
||||
version = "0.4.6"
|
||||
|
|
@ -3107,6 +3352,17 @@ dependencies = [
|
|||
"num-traits",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "num-iter"
|
||||
version = "0.1.45"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "1429034a0490724d0075ebb2bc9e875d6503c3cf69e235a8941aa757d83ef5bf"
|
||||
dependencies = [
|
||||
"autocfg",
|
||||
"num-integer",
|
||||
"num-traits",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "num-rational"
|
||||
version = "0.4.2"
|
||||
|
|
@ -3125,6 +3381,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
|
|||
checksum = "071dfc062690e90b734c0b2273ce72ad0ffa95f0c74596bc250dcfd960262841"
|
||||
dependencies = [
|
||||
"autocfg",
|
||||
"libm",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
|
|
@ -3657,6 +3914,15 @@ version = "0.2.3"
|
|||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "df94ce210e5bc13cb6651479fa48d14f601d9858cfe0467f43ae157023b938d3"
|
||||
|
||||
[[package]]
|
||||
name = "pem-rfc7468"
|
||||
version = "0.7.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "88b39c9bfcfc231068454382784bb460aae594343fb030d46e9f50a645418412"
|
||||
dependencies = [
|
||||
"base64ct",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "percent-encoding"
|
||||
version = "2.3.1"
|
||||
|
|
@ -3820,6 +4086,27 @@ dependencies = [
|
|||
"futures-io",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "pkcs1"
|
||||
version = "0.7.5"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "c8ffb9f10fa047879315e6625af03c164b16962a5368d724ed16323b68ace47f"
|
||||
dependencies = [
|
||||
"der",
|
||||
"pkcs8",
|
||||
"spki",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "pkcs8"
|
||||
version = "0.10.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "f950b2377845cebe5cf8b5165cb3cc1a5e0fa5cfa3e1f7f55707d8fd82e0a7b7"
|
||||
dependencies = [
|
||||
"der",
|
||||
"spki",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "pkg-config"
|
||||
version = "0.3.31"
|
||||
|
|
@ -3894,7 +4181,7 @@ version = "0.2.20"
|
|||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "77957b295656769bb8ad2b6a6b09d897d94f05c41b069aede1fcdaa675eaea04"
|
||||
dependencies = [
|
||||
"zerocopy",
|
||||
"zerocopy 0.7.35",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
|
|
@ -4364,6 +4651,26 @@ dependencies = [
|
|||
"symphonia",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "rsa"
|
||||
version = "0.9.8"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "78928ac1ed176a5ca1d17e578a1825f3d81ca54cf41053a592584b020cfd691b"
|
||||
dependencies = [
|
||||
"const-oid",
|
||||
"digest",
|
||||
"num-bigint-dig",
|
||||
"num-integer",
|
||||
"num-traits",
|
||||
"pkcs1",
|
||||
"pkcs8",
|
||||
"rand_core 0.6.4",
|
||||
"signature",
|
||||
"spki",
|
||||
"subtle",
|
||||
"zeroize",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "rubato"
|
||||
version = "0.16.2"
|
||||
|
|
@ -4376,6 +4683,20 @@ dependencies = [
|
|||
"realfft",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "rusqlite"
|
||||
version = "0.32.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "7753b721174eb8ff87a9a0e799e2d7bc3749323e773db92e0984debb00019d6e"
|
||||
dependencies = [
|
||||
"bitflags 2.8.0",
|
||||
"fallible-iterator",
|
||||
"fallible-streaming-iterator",
|
||||
"hashlink 0.9.1",
|
||||
"libsqlite3-sys",
|
||||
"smallvec",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "rust-stemmers"
|
||||
version = "1.2.0"
|
||||
|
|
@ -4776,6 +5097,17 @@ dependencies = [
|
|||
"stable_deref_trait",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "sha1"
|
||||
version = "0.10.6"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "e3bf829a2d51ab4a5ddf1352d8470c140cadc8301b2ae1789db023f01cedd6ba"
|
||||
dependencies = [
|
||||
"cfg-if",
|
||||
"cpufeatures",
|
||||
"digest",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "sha2"
|
||||
version = "0.10.8"
|
||||
|
|
@ -4802,6 +5134,16 @@ dependencies = [
|
|||
"libc",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "signature"
|
||||
version = "2.2.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "77549399552de45a898a580c1b41d445bf730df867cc44e6c0233bbc4b8329de"
|
||||
dependencies = [
|
||||
"digest",
|
||||
"rand_core 0.6.4",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "simd-adler32"
|
||||
version = "0.3.7"
|
||||
|
|
@ -4834,6 +5176,9 @@ name = "smallvec"
|
|||
version = "1.13.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "3c5e1a9a646d36c3599cd173a41282daf47c44583ad367b8e6837255952e5c67"
|
||||
dependencies = [
|
||||
"serde",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "socket2"
|
||||
|
|
@ -4909,6 +5254,211 @@ name = "spin"
|
|||
version = "0.9.8"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "6980e8d7511241f8acf4aebddbb1ff938df5eebe98691418c4468d0b72a96a67"
|
||||
dependencies = [
|
||||
"lock_api",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "spki"
|
||||
version = "0.7.3"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "d91ed6c858b01f942cd56b37a94b3e0a1798290327d1236e4d9cf4eaca44d29d"
|
||||
dependencies = [
|
||||
"base64ct",
|
||||
"der",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "sqlx"
|
||||
version = "0.8.6"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "1fefb893899429669dcdd979aff487bd78f4064e5e7907e4269081e0ef7d97dc"
|
||||
dependencies = [
|
||||
"sqlx-core",
|
||||
"sqlx-macros",
|
||||
"sqlx-mysql",
|
||||
"sqlx-postgres",
|
||||
"sqlx-sqlite",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "sqlx-core"
|
||||
version = "0.8.6"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "ee6798b1838b6a0f69c007c133b8df5866302197e404e8b6ee8ed3e3a5e68dc6"
|
||||
dependencies = [
|
||||
"base64 0.22.1",
|
||||
"bytes",
|
||||
"crc",
|
||||
"crossbeam-queue",
|
||||
"either",
|
||||
"event-listener",
|
||||
"futures-core",
|
||||
"futures-intrusive",
|
||||
"futures-io",
|
||||
"futures-util",
|
||||
"hashbrown 0.15.2",
|
||||
"hashlink 0.10.0",
|
||||
"indexmap 2.7.1",
|
||||
"log",
|
||||
"memchr",
|
||||
"once_cell",
|
||||
"percent-encoding",
|
||||
"serde",
|
||||
"serde_json",
|
||||
"sha2",
|
||||
"smallvec",
|
||||
"thiserror 2.0.11",
|
||||
"time",
|
||||
"tokio",
|
||||
"tokio-stream",
|
||||
"tracing",
|
||||
"url",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "sqlx-macros"
|
||||
version = "0.8.6"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "a2d452988ccaacfbf5e0bdbc348fb91d7c8af5bee192173ac3636b5fb6e6715d"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"sqlx-core",
|
||||
"sqlx-macros-core",
|
||||
"syn 2.0.96",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "sqlx-macros-core"
|
||||
version = "0.8.6"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "19a9c1841124ac5a61741f96e1d9e2ec77424bf323962dd894bdb93f37d5219b"
|
||||
dependencies = [
|
||||
"dotenvy",
|
||||
"either",
|
||||
"heck 0.5.0",
|
||||
"hex",
|
||||
"once_cell",
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"serde",
|
||||
"serde_json",
|
||||
"sha2",
|
||||
"sqlx-core",
|
||||
"sqlx-mysql",
|
||||
"sqlx-postgres",
|
||||
"sqlx-sqlite",
|
||||
"syn 2.0.96",
|
||||
"tokio",
|
||||
"url",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "sqlx-mysql"
|
||||
version = "0.8.6"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "aa003f0038df784eb8fecbbac13affe3da23b45194bd57dba231c8f48199c526"
|
||||
dependencies = [
|
||||
"atoi",
|
||||
"base64 0.22.1",
|
||||
"bitflags 2.8.0",
|
||||
"byteorder",
|
||||
"bytes",
|
||||
"crc",
|
||||
"digest",
|
||||
"dotenvy",
|
||||
"either",
|
||||
"futures-channel",
|
||||
"futures-core",
|
||||
"futures-io",
|
||||
"futures-util",
|
||||
"generic-array",
|
||||
"hex",
|
||||
"hkdf",
|
||||
"hmac",
|
||||
"itoa",
|
||||
"log",
|
||||
"md-5",
|
||||
"memchr",
|
||||
"once_cell",
|
||||
"percent-encoding",
|
||||
"rand 0.8.5",
|
||||
"rsa",
|
||||
"serde",
|
||||
"sha1",
|
||||
"sha2",
|
||||
"smallvec",
|
||||
"sqlx-core",
|
||||
"stringprep",
|
||||
"thiserror 2.0.11",
|
||||
"time",
|
||||
"tracing",
|
||||
"whoami",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "sqlx-postgres"
|
||||
version = "0.8.6"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "db58fcd5a53cf07c184b154801ff91347e4c30d17a3562a635ff028ad5deda46"
|
||||
dependencies = [
|
||||
"atoi",
|
||||
"base64 0.22.1",
|
||||
"bitflags 2.8.0",
|
||||
"byteorder",
|
||||
"crc",
|
||||
"dotenvy",
|
||||
"etcetera",
|
||||
"futures-channel",
|
||||
"futures-core",
|
||||
"futures-util",
|
||||
"hex",
|
||||
"hkdf",
|
||||
"hmac",
|
||||
"home",
|
||||
"itoa",
|
||||
"log",
|
||||
"md-5",
|
||||
"memchr",
|
||||
"once_cell",
|
||||
"rand 0.8.5",
|
||||
"serde",
|
||||
"serde_json",
|
||||
"sha2",
|
||||
"smallvec",
|
||||
"sqlx-core",
|
||||
"stringprep",
|
||||
"thiserror 2.0.11",
|
||||
"time",
|
||||
"tracing",
|
||||
"whoami",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "sqlx-sqlite"
|
||||
version = "0.8.6"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "c2d12fe70b2c1b4401038055f90f151b78208de1f9f89a7dbfd41587a10c3eea"
|
||||
dependencies = [
|
||||
"atoi",
|
||||
"flume",
|
||||
"futures-channel",
|
||||
"futures-core",
|
||||
"futures-executor",
|
||||
"futures-intrusive",
|
||||
"futures-util",
|
||||
"libsqlite3-sys",
|
||||
"log",
|
||||
"percent-encoding",
|
||||
"serde",
|
||||
"serde_urlencoded",
|
||||
"sqlx-core",
|
||||
"thiserror 2.0.11",
|
||||
"time",
|
||||
"tracing",
|
||||
"url",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "stable_deref_trait"
|
||||
|
|
@ -4954,6 +5504,17 @@ dependencies = [
|
|||
"quote",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "stringprep"
|
||||
version = "0.1.5"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "7b4df3d392d81bd458a8a621b8bffbd2302a12ffe288a9d931670948749463b1"
|
||||
dependencies = [
|
||||
"unicode-bidi",
|
||||
"unicode-normalization",
|
||||
"unicode-properties",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "strsim"
|
||||
version = "0.11.1"
|
||||
|
|
@ -5317,6 +5878,7 @@ dependencies = [
|
|||
"gtk",
|
||||
"heck 0.5.0",
|
||||
"http 1.2.0",
|
||||
"http-range",
|
||||
"image",
|
||||
"jni",
|
||||
"libc",
|
||||
|
|
@ -5576,6 +6138,25 @@ dependencies = [
|
|||
"zbus",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "tauri-plugin-sql"
|
||||
version = "2.3.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "df059378695202fef1e274b8e7916fc3dffc44716ae4baf8c0226089b2f390ae"
|
||||
dependencies = [
|
||||
"futures-core",
|
||||
"indexmap 2.7.1",
|
||||
"log",
|
||||
"serde",
|
||||
"serde_json",
|
||||
"sqlx",
|
||||
"tauri",
|
||||
"tauri-plugin",
|
||||
"thiserror 2.0.11",
|
||||
"time",
|
||||
"tokio",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "tauri-plugin-store"
|
||||
version = "2.2.0"
|
||||
|
|
@ -5901,6 +6482,17 @@ dependencies = [
|
|||
"tokio",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "tokio-stream"
|
||||
version = "0.1.17"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "eca58d7bba4a75707817a2c44174253f9236b2d5fbd055602e9d5c07c139a047"
|
||||
dependencies = [
|
||||
"futures-core",
|
||||
"pin-project-lite",
|
||||
"tokio",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "tokio-util"
|
||||
version = "0.7.13"
|
||||
|
|
@ -6003,6 +6595,7 @@ version = "0.1.41"
|
|||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "784e0ac535deb450455cbfa28a6f0df145ea1bb7ae51b821cf5e7927fdcfbdd0"
|
||||
dependencies = [
|
||||
"log",
|
||||
"pin-project-lite",
|
||||
"tracing-attributes",
|
||||
"tracing-core",
|
||||
|
|
@ -6130,12 +6723,33 @@ dependencies = [
|
|||
"unic-common",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "unicode-bidi"
|
||||
version = "0.3.18"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "5c1cb5db39152898a79168971543b1cb5020dff7fe43c8dc468b0885f5e29df5"
|
||||
|
||||
[[package]]
|
||||
name = "unicode-ident"
|
||||
version = "1.0.16"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "a210d160f08b701c8721ba1c726c11662f877ea6b7094007e1ca9a1041945034"
|
||||
|
||||
[[package]]
|
||||
name = "unicode-normalization"
|
||||
version = "0.1.24"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "5033c97c4262335cded6d6fc3e5c18ab755e1a3dc96376350f3d8e9f009ad956"
|
||||
dependencies = [
|
||||
"tinyvec",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "unicode-properties"
|
||||
version = "0.1.3"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "e70f2a8b45122e719eb623c01822704c4e0907e7e426a05927e1a1cfff5b75d0"
|
||||
|
||||
[[package]]
|
||||
name = "unicode-segmentation"
|
||||
version = "1.12.0"
|
||||
|
|
@ -6314,6 +6928,12 @@ dependencies = [
|
|||
"wit-bindgen-rt",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "wasite"
|
||||
version = "0.1.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "b8dad83b4f25e74f184f64c43b150b91efe7647395b42289f38e50566d82855b"
|
||||
|
||||
[[package]]
|
||||
name = "wasm-bindgen"
|
||||
version = "0.2.100"
|
||||
|
|
@ -6546,6 +7166,16 @@ dependencies = [
|
|||
"fs_extra",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "whoami"
|
||||
version = "1.6.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "5d4a4db5077702ca3015d3d02d74974948aba2ad9e12ab7df718ee64ccd7e97d"
|
||||
dependencies = [
|
||||
"libredox",
|
||||
"wasite",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "widestring"
|
||||
version = "1.1.0"
|
||||
|
|
@ -7388,7 +8018,16 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
|
|||
checksum = "1b9b4fd18abc82b8136838da5d50bae7bdea537c574d8dc1a34ed098d6c166f0"
|
||||
dependencies = [
|
||||
"byteorder",
|
||||
"zerocopy-derive",
|
||||
"zerocopy-derive 0.7.35",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "zerocopy"
|
||||
version = "0.8.26"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "1039dd0d3c310cf05de012d8a39ff557cb0d23087fd44cad61df08fc31907a2f"
|
||||
dependencies = [
|
||||
"zerocopy-derive 0.8.26",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
|
|
@ -7402,6 +8041,17 @@ dependencies = [
|
|||
"syn 2.0.96",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "zerocopy-derive"
|
||||
version = "0.8.26"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "9ecf5b4cc5364572d7f4c329661bcc82724222973f2cab6f050a4e5c22f75181"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn 2.0.96",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "zerofrom"
|
||||
version = "0.1.5"
|
||||
|
|
|
|||
|
|
@ -28,7 +28,7 @@ tauri-build = { version = "2", features = [] }
|
|||
|
||||
[dependencies]
|
||||
once_cell = "1"
|
||||
tauri = { version = "2", features = [
|
||||
tauri = { version = "2", features = [ "protocol-asset",
|
||||
"macos-private-api",
|
||||
"tray-icon",
|
||||
'image-png',
|
||||
|
|
@ -58,6 +58,9 @@ tauri-plugin-fs = "2"
|
|||
rustfft = "6.4.0"
|
||||
strsim = "0.11.0"
|
||||
natural = "0.5.0"
|
||||
chrono = "0.4"
|
||||
rusqlite = { version = "0.32", features = ["bundled"] }
|
||||
tauri-plugin-sql = { version = "2", features = ["sqlite"] }
|
||||
|
||||
[dependencies.ort-sys]
|
||||
version = "=2.0.0-rc.9"
|
||||
|
|
|
|||
|
|
@ -1,11 +1,12 @@
|
|||
use crate::audio_feedback::{play_recording_start_sound, play_recording_stop_sound};
|
||||
use crate::managers::audio::AudioRecordingManager;
|
||||
use crate::managers::history::HistoryManager;
|
||||
use crate::managers::transcription::TranscriptionManager;
|
||||
use crate::overlay::{show_recording_overlay, show_transcribing_overlay};
|
||||
use crate::settings::get_settings;
|
||||
use crate::tray::{change_tray_icon, TrayIconState};
|
||||
use crate::utils;
|
||||
use log::debug;
|
||||
use log::{debug, error};
|
||||
use once_cell::sync::Lazy;
|
||||
use std::collections::HashMap;
|
||||
use std::sync::Arc;
|
||||
|
|
@ -76,6 +77,7 @@ impl ShortcutAction for TranscribeAction {
|
|||
let ah = app.clone();
|
||||
let rm = Arc::clone(&app.state::<Arc<AudioRecordingManager>>());
|
||||
let tm = Arc::clone(&app.state::<Arc<TranscriptionManager>>());
|
||||
let hm = Arc::clone(&app.state::<Arc<HistoryManager>>());
|
||||
|
||||
change_tray_icon(app, TrayIconState::Transcribing);
|
||||
show_transcribing_overlay(app);
|
||||
|
|
@ -101,6 +103,7 @@ impl ShortcutAction for TranscribeAction {
|
|||
);
|
||||
|
||||
let transcription_time = Instant::now();
|
||||
let samples_clone = samples.clone(); // Clone for history saving
|
||||
match tm.transcribe(samples) {
|
||||
Ok(transcription) => {
|
||||
debug!(
|
||||
|
|
@ -109,6 +112,17 @@ impl ShortcutAction for TranscribeAction {
|
|||
transcription
|
||||
);
|
||||
if !transcription.is_empty() {
|
||||
// Save to history
|
||||
let hm_clone = Arc::clone(&hm);
|
||||
let transcription_for_history = transcription.clone();
|
||||
tauri::async_runtime::spawn(async move {
|
||||
if let Err(e) = hm_clone
|
||||
.save_transcription(samples_clone, transcription_for_history)
|
||||
.await
|
||||
{
|
||||
error!("Failed to save transcription to history: {}", e);
|
||||
}
|
||||
});
|
||||
let transcription_clone = transcription.clone();
|
||||
let ah_clone = ah.clone();
|
||||
let paste_time = Instant::now();
|
||||
|
|
|
|||
|
|
@ -2,9 +2,11 @@
|
|||
mod device;
|
||||
mod recorder;
|
||||
mod resampler;
|
||||
mod utils;
|
||||
mod visualizer;
|
||||
|
||||
pub use device::{list_input_devices, list_output_devices, CpalDeviceInfo};
|
||||
pub use recorder::AudioRecorder;
|
||||
pub use resampler::FrameResampler;
|
||||
pub use utils::save_wav_file;
|
||||
pub use visualizer::AudioVisualiser;
|
||||
|
|
|
|||
26
src-tauri/src/audio_toolkit/audio/utils.rs
Normal file
26
src-tauri/src/audio_toolkit/audio/utils.rs
Normal file
|
|
@ -0,0 +1,26 @@
|
|||
use anyhow::Result;
|
||||
use hound::{WavSpec, WavWriter};
|
||||
use log::debug;
|
||||
use std::path::Path;
|
||||
|
||||
/// Save audio samples as a WAV file
|
||||
pub async fn save_wav_file<P: AsRef<Path>>(file_path: P, samples: &[f32]) -> Result<()> {
|
||||
let spec = WavSpec {
|
||||
channels: 1,
|
||||
sample_rate: 16000,
|
||||
bits_per_sample: 16,
|
||||
sample_format: hound::SampleFormat::Int,
|
||||
};
|
||||
|
||||
let mut writer = WavWriter::create(file_path.as_ref(), spec)?;
|
||||
|
||||
// Convert f32 samples to i16 for WAV
|
||||
for sample in samples {
|
||||
let sample_i16 = (sample * i16::MAX as f32) as i16;
|
||||
writer.write_sample(sample_i16)?;
|
||||
}
|
||||
|
||||
writer.finalize()?;
|
||||
debug!("Saved WAV file: {:?}", file_path.as_ref());
|
||||
Ok(())
|
||||
}
|
||||
|
|
@ -2,5 +2,7 @@ pub mod audio;
|
|||
pub mod constants;
|
||||
pub mod vad;
|
||||
|
||||
pub use audio::{list_input_devices, list_output_devices, AudioRecorder, CpalDeviceInfo};
|
||||
pub use audio::{
|
||||
list_input_devices, list_output_devices, save_wav_file, AudioRecorder, CpalDeviceInfo,
|
||||
};
|
||||
pub use vad::{SileroVad, VoiceActivityDetector};
|
||||
|
|
|
|||
38
src-tauri/src/commands/history.rs
Normal file
38
src-tauri/src/commands/history.rs
Normal file
|
|
@ -0,0 +1,38 @@
|
|||
use crate::managers::history::{HistoryEntry, HistoryManager};
|
||||
use std::sync::Arc;
|
||||
use tauri::{AppHandle, State};
|
||||
|
||||
#[tauri::command]
|
||||
pub async fn get_history_entries(
|
||||
_app: AppHandle,
|
||||
history_manager: State<'_, Arc<HistoryManager>>,
|
||||
) -> Result<Vec<HistoryEntry>, String> {
|
||||
history_manager
|
||||
.get_history_entries()
|
||||
.await
|
||||
.map_err(|e| e.to_string())
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
pub async fn toggle_history_entry_saved(
|
||||
_app: AppHandle,
|
||||
history_manager: State<'_, Arc<HistoryManager>>,
|
||||
id: i64,
|
||||
) -> Result<(), String> {
|
||||
history_manager
|
||||
.toggle_saved_status(id)
|
||||
.await
|
||||
.map_err(|e| e.to_string())
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
pub async fn get_audio_file_path(
|
||||
_app: AppHandle,
|
||||
history_manager: State<'_, Arc<HistoryManager>>,
|
||||
file_name: String,
|
||||
) -> Result<String, String> {
|
||||
let path = history_manager.get_audio_file_path(&file_name);
|
||||
path.to_str()
|
||||
.ok_or_else(|| "Invalid file path".to_string())
|
||||
.map(|s| s.to_string())
|
||||
}
|
||||
|
|
@ -1,4 +1,5 @@
|
|||
pub mod audio;
|
||||
pub mod history;
|
||||
pub mod models;
|
||||
pub mod transcription;
|
||||
|
||||
|
|
|
|||
|
|
@ -11,6 +11,7 @@ mod tray;
|
|||
mod utils;
|
||||
|
||||
use managers::audio::AudioRecordingManager;
|
||||
use managers::history::HistoryManager;
|
||||
use managers::model::ModelManager;
|
||||
use managers::transcription::TranscriptionManager;
|
||||
use std::collections::HashMap;
|
||||
|
|
@ -75,6 +76,14 @@ pub fn run() {
|
|||
.plugin(tauri_plugin_macos_permissions::init())
|
||||
.plugin(tauri_plugin_opener::init())
|
||||
.plugin(tauri_plugin_store::Builder::default().build())
|
||||
.plugin(
|
||||
tauri_plugin_sql::Builder::default()
|
||||
.add_migrations(
|
||||
"sqlite:history.db",
|
||||
managers::history::HistoryManager::get_migrations(),
|
||||
)
|
||||
.build(),
|
||||
)
|
||||
.plugin(tauri_plugin_global_shortcut::Builder::new().build())
|
||||
.plugin(tauri_plugin_autostart::init(
|
||||
MacosLauncher::LaunchAgent,
|
||||
|
|
@ -134,11 +143,14 @@ pub fn run() {
|
|||
TranscriptionManager::new(&app, model_manager.clone())
|
||||
.expect("Failed to initialize transcription manager"),
|
||||
);
|
||||
let history_manager =
|
||||
Arc::new(HistoryManager::new(&app).expect("Failed to initialize history manager"));
|
||||
|
||||
// Add managers to Tauri's managed state
|
||||
app.manage(recording_manager.clone());
|
||||
app.manage(model_manager.clone());
|
||||
app.manage(transcription_manager.clone());
|
||||
app.manage(history_manager.clone());
|
||||
|
||||
// Create the recording overlay window (hidden by default)
|
||||
utils::create_recording_overlay(&app.handle());
|
||||
|
|
@ -206,7 +218,10 @@ pub fn run() {
|
|||
commands::audio::get_selected_output_device,
|
||||
commands::transcription::set_model_unload_timeout,
|
||||
commands::transcription::get_model_load_status,
|
||||
commands::transcription::unload_model_manually
|
||||
commands::transcription::unload_model_manually,
|
||||
commands::history::get_history_entries,
|
||||
commands::history::toggle_history_entry_saved,
|
||||
commands::history::get_audio_file_path
|
||||
])
|
||||
.run(tauri::generate_context!())
|
||||
.expect("error while running tauri application");
|
||||
|
|
|
|||
250
src-tauri/src/managers/history.rs
Normal file
250
src-tauri/src/managers/history.rs
Normal file
|
|
@ -0,0 +1,250 @@
|
|||
use anyhow::Result;
|
||||
use chrono::{DateTime, Local, Utc};
|
||||
use log::{debug, error};
|
||||
use rusqlite::{params, Connection};
|
||||
use serde::{Deserialize, Serialize};
|
||||
use std::fs;
|
||||
use std::path::PathBuf;
|
||||
use tauri::{App, AppHandle, Emitter, Manager};
|
||||
use tauri_plugin_sql::{Migration, MigrationKind};
|
||||
|
||||
use crate::audio_toolkit::save_wav_file;
|
||||
|
||||
const HISTORY_LIMIT: usize = 5;
|
||||
|
||||
#[derive(Clone, Debug, Serialize, Deserialize)]
|
||||
pub struct HistoryEntry {
|
||||
pub id: i64,
|
||||
pub file_name: String,
|
||||
pub timestamp: i64,
|
||||
pub saved: bool,
|
||||
pub title: String,
|
||||
pub transcription_text: String,
|
||||
}
|
||||
|
||||
#[derive(Clone)]
|
||||
pub struct HistoryManager {
|
||||
app_handle: AppHandle,
|
||||
recordings_dir: PathBuf,
|
||||
db_path: PathBuf,
|
||||
}
|
||||
|
||||
impl HistoryManager {
|
||||
pub fn new(app: &App) -> Result<Self> {
|
||||
let app_handle = app.app_handle().clone();
|
||||
|
||||
// Create recordings directory in app data dir
|
||||
let app_data_dir = app.path().app_data_dir()?;
|
||||
let recordings_dir = app_data_dir.join("recordings");
|
||||
let db_path = app_data_dir.join("history.db");
|
||||
|
||||
// Ensure recordings directory exists
|
||||
if !recordings_dir.exists() {
|
||||
fs::create_dir_all(&recordings_dir)?;
|
||||
debug!("Created recordings directory: {:?}", recordings_dir);
|
||||
}
|
||||
|
||||
let manager = Self {
|
||||
app_handle,
|
||||
recordings_dir,
|
||||
db_path,
|
||||
};
|
||||
|
||||
// Initialize database
|
||||
manager.init_database()?;
|
||||
|
||||
Ok(manager)
|
||||
}
|
||||
|
||||
pub fn get_migrations() -> Vec<Migration> {
|
||||
vec![Migration {
|
||||
version: 1,
|
||||
description: "create_transcription_history_table",
|
||||
sql: "CREATE TABLE IF NOT EXISTS transcription_history (
|
||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||
file_name TEXT NOT NULL,
|
||||
timestamp INTEGER NOT NULL,
|
||||
saved BOOLEAN NOT NULL DEFAULT 0,
|
||||
title TEXT NOT NULL,
|
||||
transcription_text TEXT NOT NULL
|
||||
);",
|
||||
kind: MigrationKind::Up,
|
||||
}]
|
||||
}
|
||||
|
||||
fn init_database(&self) -> Result<()> {
|
||||
let conn = Connection::open(&self.db_path)?;
|
||||
conn.execute(
|
||||
"CREATE TABLE IF NOT EXISTS transcription_history (
|
||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||
file_name TEXT NOT NULL,
|
||||
timestamp INTEGER NOT NULL,
|
||||
saved BOOLEAN NOT NULL DEFAULT 0,
|
||||
title TEXT NOT NULL,
|
||||
transcription_text TEXT NOT NULL
|
||||
)",
|
||||
[],
|
||||
)?;
|
||||
debug!("Database initialized at: {:?}", self.db_path);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn get_connection(&self) -> Result<Connection> {
|
||||
Ok(Connection::open(&self.db_path)?)
|
||||
}
|
||||
|
||||
/// Save a transcription to history (both database and WAV file)
|
||||
pub async fn save_transcription(
|
||||
&self,
|
||||
audio_samples: Vec<f32>,
|
||||
transcription_text: String,
|
||||
) -> Result<()> {
|
||||
let timestamp = Utc::now().timestamp();
|
||||
let file_name = format!("handy-{}.wav", timestamp);
|
||||
let title = self.format_timestamp_title(timestamp);
|
||||
|
||||
// Save WAV file
|
||||
let file_path = self.recordings_dir.join(&file_name);
|
||||
save_wav_file(file_path, &audio_samples).await?;
|
||||
|
||||
// Save to database
|
||||
self.save_to_database(file_name, timestamp, title, transcription_text)?;
|
||||
|
||||
// Clean up old entries
|
||||
self.cleanup_old_entries()?;
|
||||
|
||||
// Emit history updated event
|
||||
if let Err(e) = self.app_handle.emit("history-updated", ()) {
|
||||
error!("Failed to emit history-updated event: {}", e);
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn save_to_database(
|
||||
&self,
|
||||
file_name: String,
|
||||
timestamp: i64,
|
||||
title: String,
|
||||
transcription_text: String,
|
||||
) -> Result<()> {
|
||||
let conn = self.get_connection()?;
|
||||
conn.execute(
|
||||
"INSERT INTO transcription_history (file_name, timestamp, saved, title, transcription_text) VALUES (?1, ?2, ?3, ?4, ?5)",
|
||||
params![file_name, timestamp, false, title, transcription_text],
|
||||
)?;
|
||||
|
||||
debug!("Saved transcription to database");
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn cleanup_old_entries(&self) -> Result<()> {
|
||||
let conn = self.get_connection()?;
|
||||
|
||||
// Get all entries that are not saved, ordered by timestamp desc
|
||||
let mut stmt = conn.prepare(
|
||||
"SELECT id, file_name FROM transcription_history WHERE saved = 0 ORDER BY timestamp DESC"
|
||||
)?;
|
||||
|
||||
let rows = stmt.query_map([], |row| {
|
||||
Ok((row.get::<_, i64>("id")?, row.get::<_, String>("file_name")?))
|
||||
})?;
|
||||
|
||||
let mut entries: Vec<(i64, String)> = Vec::new();
|
||||
for row in rows {
|
||||
entries.push(row?);
|
||||
}
|
||||
|
||||
if entries.len() > HISTORY_LIMIT {
|
||||
let entries_to_delete = &entries[HISTORY_LIMIT..];
|
||||
|
||||
for (id, file_name) in entries_to_delete {
|
||||
// Delete database entry
|
||||
conn.execute(
|
||||
"DELETE FROM transcription_history WHERE id = ?1",
|
||||
params![id],
|
||||
)?;
|
||||
|
||||
// Delete WAV file
|
||||
let file_path = self.recordings_dir.join(file_name);
|
||||
if file_path.exists() {
|
||||
if let Err(e) = fs::remove_file(&file_path) {
|
||||
error!("Failed to delete WAV file {}: {}", file_name, e);
|
||||
} else {
|
||||
debug!("Deleted old WAV file: {}", file_name);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
debug!("Cleaned up {} old history entries", entries_to_delete.len());
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub async fn get_history_entries(&self) -> Result<Vec<HistoryEntry>> {
|
||||
let conn = self.get_connection()?;
|
||||
let mut stmt = conn.prepare(
|
||||
"SELECT id, file_name, timestamp, saved, title, transcription_text FROM transcription_history ORDER BY timestamp DESC"
|
||||
)?;
|
||||
|
||||
let rows = stmt.query_map([], |row| {
|
||||
Ok(HistoryEntry {
|
||||
id: row.get("id")?,
|
||||
file_name: row.get("file_name")?,
|
||||
timestamp: row.get("timestamp")?,
|
||||
saved: row.get("saved")?,
|
||||
title: row.get("title")?,
|
||||
transcription_text: row.get("transcription_text")?,
|
||||
})
|
||||
})?;
|
||||
|
||||
let mut entries = Vec::new();
|
||||
for row in rows {
|
||||
entries.push(row?);
|
||||
}
|
||||
|
||||
Ok(entries)
|
||||
}
|
||||
|
||||
pub async fn toggle_saved_status(&self, id: i64) -> Result<()> {
|
||||
let conn = self.get_connection()?;
|
||||
|
||||
// Get current saved status
|
||||
let current_saved: bool = conn.query_row(
|
||||
"SELECT saved FROM transcription_history WHERE id = ?1",
|
||||
params![id],
|
||||
|row| row.get("saved"),
|
||||
)?;
|
||||
|
||||
let new_saved = !current_saved;
|
||||
|
||||
conn.execute(
|
||||
"UPDATE transcription_history SET saved = ?1 WHERE id = ?2",
|
||||
params![new_saved, id],
|
||||
)?;
|
||||
|
||||
debug!("Toggled saved status for entry {}: {}", id, new_saved);
|
||||
|
||||
// Emit history updated event
|
||||
if let Err(e) = self.app_handle.emit("history-updated", ()) {
|
||||
error!("Failed to emit history-updated event: {}", e);
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub fn get_audio_file_path(&self, file_name: &str) -> PathBuf {
|
||||
self.recordings_dir.join(file_name)
|
||||
}
|
||||
|
||||
fn format_timestamp_title(&self, timestamp: i64) -> String {
|
||||
if let Some(utc_datetime) = DateTime::from_timestamp(timestamp, 0) {
|
||||
// Convert UTC to local timezone
|
||||
let local_datetime = utc_datetime.with_timezone(&Local);
|
||||
local_datetime.format("%B %e, %Y - %l:%M%p").to_string()
|
||||
} else {
|
||||
format!("Recording {}", timestamp)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -1,3 +1,4 @@
|
|||
pub mod audio;
|
||||
pub mod history;
|
||||
pub mod model;
|
||||
pub mod transcription;
|
||||
|
|
|
|||
|
|
@ -23,7 +23,11 @@
|
|||
}
|
||||
],
|
||||
"security": {
|
||||
"csp": null
|
||||
"csp": null,
|
||||
"assetProtocol": {
|
||||
"enable": true,
|
||||
"scope": ["**"]
|
||||
}
|
||||
}
|
||||
},
|
||||
"bundle": {
|
||||
|
|
|
|||
|
|
@ -41,12 +41,12 @@ export const SECTIONS_CONFIG = {
|
|||
component: AdvancedSettings,
|
||||
enabled: () => true,
|
||||
},
|
||||
// history: {
|
||||
// label: "History",
|
||||
// icon: History,
|
||||
// component: HistorySettings,
|
||||
// enabled: () => true,
|
||||
// },
|
||||
history: {
|
||||
label: "History",
|
||||
icon: History,
|
||||
component: HistorySettings,
|
||||
enabled: () => true,
|
||||
},
|
||||
debug: {
|
||||
label: "Debug",
|
||||
icon: FlaskConical,
|
||||
|
|
|
|||
|
|
@ -1,12 +1,197 @@
|
|||
import React from "react";
|
||||
import React, { useState, useEffect, useCallback } from "react";
|
||||
import { SettingsGroup } from "../ui/SettingsGroup";
|
||||
import { AudioPlayer } from "../ui/AudioPlayer";
|
||||
import { ClipboardCopy, Star, Check } from "lucide-react";
|
||||
import { convertFileSrc, invoke } from "@tauri-apps/api/core";
|
||||
import { listen } from "@tauri-apps/api/event";
|
||||
|
||||
interface HistoryEntry {
|
||||
id: number;
|
||||
file_name: string;
|
||||
timestamp: number;
|
||||
saved: boolean;
|
||||
title: string;
|
||||
transcription_text: string;
|
||||
}
|
||||
|
||||
export const HistorySettings: React.FC = () => {
|
||||
const [historyEntries, setHistoryEntries] = useState<HistoryEntry[]>([]);
|
||||
const [loading, setLoading] = useState(true);
|
||||
|
||||
const loadHistoryEntries = useCallback(async () => {
|
||||
try {
|
||||
const entries = await invoke<HistoryEntry[]>("get_history_entries");
|
||||
setHistoryEntries(entries);
|
||||
} catch (error) {
|
||||
console.error("Failed to load history entries:", error);
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
loadHistoryEntries();
|
||||
|
||||
// Listen for history update events
|
||||
const setupListener = async () => {
|
||||
const unlisten = await listen("history-updated", () => {
|
||||
console.log("History updated, reloading entries...");
|
||||
loadHistoryEntries();
|
||||
});
|
||||
|
||||
// Return cleanup function
|
||||
return unlisten;
|
||||
};
|
||||
|
||||
let unlistenPromise = setupListener();
|
||||
|
||||
return () => {
|
||||
unlistenPromise.then((unlisten) => {
|
||||
if (unlisten) {
|
||||
unlisten();
|
||||
}
|
||||
});
|
||||
};
|
||||
}, [loadHistoryEntries]);
|
||||
|
||||
const toggleSaved = async (id: number) => {
|
||||
try {
|
||||
await invoke("toggle_history_entry_saved", { id });
|
||||
// No need to reload here - the event listener will handle it
|
||||
} catch (error) {
|
||||
console.error("Failed to toggle saved status:", error);
|
||||
}
|
||||
};
|
||||
|
||||
const copyToClipboard = async (text: string) => {
|
||||
try {
|
||||
await navigator.clipboard.writeText(text);
|
||||
} catch (error) {
|
||||
console.error("Failed to copy to clipboard:", error);
|
||||
}
|
||||
};
|
||||
|
||||
const getAudioUrl = async (fileName: string) => {
|
||||
try {
|
||||
const filePath = await invoke<string>("get_audio_file_path", {
|
||||
fileName,
|
||||
});
|
||||
|
||||
return convertFileSrc(`${filePath}`, "asset");
|
||||
} catch (error) {
|
||||
console.error("Failed to get audio file path:", error);
|
||||
return null;
|
||||
}
|
||||
};
|
||||
|
||||
if (loading) {
|
||||
return (
|
||||
<div className="max-w-3xl w-full mx-auto space-y-6">
|
||||
<SettingsGroup title="History">
|
||||
<div className="px-4 py-3 text-center text-text/60">
|
||||
Loading history...
|
||||
</div>
|
||||
</SettingsGroup>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
if (historyEntries.length === 0) {
|
||||
return (
|
||||
<div className="max-w-3xl w-full mx-auto space-y-6">
|
||||
<SettingsGroup title="History">
|
||||
<div className="px-4 py-3 text-center text-text/60">
|
||||
No transcriptions yet. Start recording to build your history!
|
||||
</div>
|
||||
</SettingsGroup>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="max-w-3xl w-full mx-auto space-y-6">
|
||||
<SettingsGroup title="History">
|
||||
<></>
|
||||
{historyEntries.map((entry) => (
|
||||
<HistoryEntryComponent
|
||||
key={entry.id}
|
||||
entry={entry}
|
||||
onToggleSaved={() => toggleSaved(entry.id)}
|
||||
onCopyText={() => copyToClipboard(entry.transcription_text)}
|
||||
getAudioUrl={getAudioUrl}
|
||||
/>
|
||||
))}
|
||||
</SettingsGroup>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
interface HistoryEntryProps {
|
||||
entry: HistoryEntry;
|
||||
onToggleSaved: () => void;
|
||||
onCopyText: () => void;
|
||||
getAudioUrl: (fileName: string) => Promise<string | null>;
|
||||
}
|
||||
|
||||
const HistoryEntryComponent: React.FC<HistoryEntryProps> = ({
|
||||
entry,
|
||||
onToggleSaved,
|
||||
onCopyText,
|
||||
getAudioUrl,
|
||||
}) => {
|
||||
const [audioUrl, setAudioUrl] = useState<string | null>(null);
|
||||
const [showCopied, setShowCopied] = useState(false);
|
||||
|
||||
useEffect(() => {
|
||||
const loadAudio = async () => {
|
||||
const url = await getAudioUrl(entry.file_name);
|
||||
setAudioUrl(url);
|
||||
};
|
||||
loadAudio();
|
||||
}, [entry.file_name, getAudioUrl]);
|
||||
|
||||
const handleCopyText = () => {
|
||||
onCopyText();
|
||||
setShowCopied(true);
|
||||
setTimeout(() => setShowCopied(false), 2000);
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="px-4 py-2 pb-5 flex flex-col gap-3">
|
||||
<div className="flex justify-between items-center">
|
||||
<p className="text-sm font-medium">{entry.title}</p>
|
||||
<div className="flex items-center gap-1">
|
||||
<button
|
||||
onClick={handleCopyText}
|
||||
className="text-text/50 hover:text-logo-primary hover:border-logo-primary transition-colors cursor-pointer"
|
||||
title="Copy transcription to clipboard"
|
||||
>
|
||||
{showCopied ? (
|
||||
<Check width={16} height={16} />
|
||||
) : (
|
||||
<ClipboardCopy width={16} height={16} />
|
||||
)}
|
||||
</button>
|
||||
<button
|
||||
onClick={onToggleSaved}
|
||||
className={`p-2 rounded transition-colors cursor-pointer ${
|
||||
entry.saved
|
||||
? "text-logo-primary hover:text-logo-primary/80"
|
||||
: "text-text/50 hover:text-logo-primary"
|
||||
}`}
|
||||
title={entry.saved ? "Remove from saved" : "Save transcription"}
|
||||
>
|
||||
<Star
|
||||
width={16}
|
||||
height={16}
|
||||
fill={entry.saved ? "currentColor" : "none"}
|
||||
/>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
<p className="italic text-text/90 text-sm pb-2">
|
||||
{entry.transcription_text}
|
||||
</p>
|
||||
{audioUrl && <AudioPlayer src={audioUrl} className="w-full" />}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
|
|
|||
220
src/components/ui/AudioPlayer.tsx
Normal file
220
src/components/ui/AudioPlayer.tsx
Normal file
|
|
@ -0,0 +1,220 @@
|
|||
import React, { useState, useRef, useEffect, useCallback } from "react";
|
||||
import { Play, Pause } from "lucide-react";
|
||||
|
||||
interface AudioPlayerProps {
|
||||
src: string;
|
||||
className?: string;
|
||||
}
|
||||
|
||||
export const AudioPlayer: React.FC<AudioPlayerProps> = ({
|
||||
src,
|
||||
className = "",
|
||||
}) => {
|
||||
const [isPlaying, setIsPlaying] = useState(false);
|
||||
const [duration, setDuration] = useState(0);
|
||||
const [currentTime, setCurrentTime] = useState(0);
|
||||
const [isDragging, setIsDragging] = useState(false);
|
||||
|
||||
const audioRef = useRef<HTMLAudioElement>(null);
|
||||
const animationRef = useRef<number>();
|
||||
const dragTimeRef = useRef<number>(0);
|
||||
|
||||
// Use refs to avoid stale closures in animation loop
|
||||
const isPlayingRef = useRef(false);
|
||||
const isDraggingRef = useRef(false);
|
||||
|
||||
// Keep refs in sync with state
|
||||
useEffect(() => {
|
||||
isPlayingRef.current = isPlaying;
|
||||
}, [isPlaying]);
|
||||
|
||||
useEffect(() => {
|
||||
isDraggingRef.current = isDragging;
|
||||
}, [isDragging]);
|
||||
|
||||
// Stable animation loop with no dependencies
|
||||
const tick = useCallback(() => {
|
||||
if (audioRef.current && !isDraggingRef.current) {
|
||||
const time = audioRef.current.currentTime;
|
||||
setCurrentTime(time);
|
||||
}
|
||||
|
||||
if (isPlayingRef.current) {
|
||||
animationRef.current = requestAnimationFrame(tick);
|
||||
}
|
||||
}, []); // Empty dependency array is key!
|
||||
|
||||
// Manage animation loop lifecycle
|
||||
useEffect(() => {
|
||||
if (isPlaying && !isDragging) {
|
||||
// Only start if not already running
|
||||
if (!animationRef.current) {
|
||||
animationRef.current = requestAnimationFrame(tick);
|
||||
}
|
||||
} else {
|
||||
// Stop animation loop
|
||||
if (animationRef.current) {
|
||||
cancelAnimationFrame(animationRef.current);
|
||||
animationRef.current = undefined;
|
||||
}
|
||||
}
|
||||
|
||||
return () => {
|
||||
if (animationRef.current) {
|
||||
cancelAnimationFrame(animationRef.current);
|
||||
animationRef.current = undefined;
|
||||
}
|
||||
};
|
||||
}, [isPlaying, isDragging, tick]);
|
||||
|
||||
// Audio event handlers
|
||||
useEffect(() => {
|
||||
const audio = audioRef.current;
|
||||
if (!audio) return;
|
||||
|
||||
const handleLoadedMetadata = () => {
|
||||
setDuration(audio.duration || 0);
|
||||
setCurrentTime(0);
|
||||
};
|
||||
|
||||
const handleEnded = () => {
|
||||
setIsPlaying(false);
|
||||
setCurrentTime(audio.duration || 0);
|
||||
};
|
||||
|
||||
const handlePlay = () => setIsPlaying(true);
|
||||
const handlePause = () => setIsPlaying(false);
|
||||
|
||||
audio.addEventListener("loadedmetadata", handleLoadedMetadata);
|
||||
audio.addEventListener("ended", handleEnded);
|
||||
audio.addEventListener("play", handlePlay);
|
||||
audio.addEventListener("pause", handlePause);
|
||||
|
||||
return () => {
|
||||
audio.removeEventListener("loadedmetadata", handleLoadedMetadata);
|
||||
audio.removeEventListener("ended", handleEnded);
|
||||
audio.removeEventListener("play", handlePlay);
|
||||
audio.removeEventListener("pause", handlePause);
|
||||
};
|
||||
}, []);
|
||||
|
||||
// Global drag handlers
|
||||
const handleMouseUp = useCallback(() => {
|
||||
if (isDragging) {
|
||||
setIsDragging(false);
|
||||
if (audioRef.current) {
|
||||
audioRef.current.currentTime = dragTimeRef.current;
|
||||
setCurrentTime(dragTimeRef.current);
|
||||
}
|
||||
}
|
||||
}, [isDragging]);
|
||||
|
||||
useEffect(() => {
|
||||
if (isDragging) {
|
||||
document.addEventListener("mouseup", handleMouseUp);
|
||||
document.addEventListener("touchend", handleMouseUp);
|
||||
|
||||
return () => {
|
||||
document.removeEventListener("mouseup", handleMouseUp);
|
||||
document.removeEventListener("touchend", handleMouseUp);
|
||||
};
|
||||
}
|
||||
}, [isDragging, handleMouseUp]);
|
||||
|
||||
const togglePlay = async () => {
|
||||
const audio = audioRef.current;
|
||||
if (!audio) return;
|
||||
|
||||
try {
|
||||
if (isPlaying) {
|
||||
audio.pause();
|
||||
} else {
|
||||
await audio.play();
|
||||
}
|
||||
} catch (error) {
|
||||
console.error("Playback failed:", error);
|
||||
}
|
||||
};
|
||||
|
||||
const handleSeek = (e: React.ChangeEvent<HTMLInputElement>) => {
|
||||
const newTime = parseFloat(e.target.value);
|
||||
dragTimeRef.current = newTime;
|
||||
setCurrentTime(newTime);
|
||||
|
||||
if (!isDragging && audioRef.current) {
|
||||
audioRef.current.currentTime = newTime;
|
||||
}
|
||||
};
|
||||
|
||||
const handleSliderMouseDown = () => {
|
||||
setIsDragging(true);
|
||||
};
|
||||
|
||||
const handleSliderTouchStart = () => {
|
||||
setIsDragging(true);
|
||||
};
|
||||
|
||||
const formatTime = (time: number): string => {
|
||||
if (!isFinite(time)) return "0:00";
|
||||
|
||||
const minutes = Math.floor(time / 60);
|
||||
const seconds = Math.floor(time % 60);
|
||||
return `${minutes}:${seconds.toString().padStart(2, "0")}`;
|
||||
};
|
||||
|
||||
// Fix playhead positioning with better edge case handling
|
||||
const getProgressPercent = (): number => {
|
||||
if (duration <= 0) return 0;
|
||||
|
||||
// Handle the end case - if we're within 0.1 seconds of the end, show 100%
|
||||
if (duration - currentTime < 0.1) return 100;
|
||||
|
||||
const percent = (currentTime / duration) * 100;
|
||||
return Math.min(100, Math.max(0, percent));
|
||||
};
|
||||
|
||||
const progressPercent = getProgressPercent();
|
||||
|
||||
return (
|
||||
<div className={`flex items-center gap-3 ${className}`}>
|
||||
<audio ref={audioRef} src={src} preload="metadata" />
|
||||
|
||||
<button
|
||||
onClick={togglePlay}
|
||||
className="transition-colors cursor-pointer text-text hover:text-logo-primary"
|
||||
aria-label={isPlaying ? "Pause" : "Play"}
|
||||
>
|
||||
{isPlaying ? (
|
||||
<Pause width={20} height={20} fill="currentColor" />
|
||||
) : (
|
||||
<Play width={20} height={20} fill="currentColor" />
|
||||
)}
|
||||
</button>
|
||||
|
||||
<div className="flex-1 flex items-center gap-2">
|
||||
<span className="text-xs text-text/60 min-w-[30px] tabular-nums">
|
||||
{formatTime(currentTime)}
|
||||
</span>
|
||||
|
||||
<input
|
||||
type="range"
|
||||
min="0"
|
||||
max={duration || 0}
|
||||
step="0.01"
|
||||
value={currentTime}
|
||||
onChange={handleSeek}
|
||||
onMouseDown={handleSliderMouseDown}
|
||||
onTouchStart={handleSliderTouchStart}
|
||||
className={`flex-1 h-1 rounded-lg appearance-none cursor-pointer focus:outline-none focus:ring-1 focus:ring-logo-primary ${progressPercent >= 99.5 ? "[&::-webkit-slider-thumb]:translate-x-0.5 [&::-moz-range-thumb]:translate-x-0.5" : ""}`}
|
||||
style={{
|
||||
background: `linear-gradient(to right, #FAA2CA 0%, #FAA2CA ${progressPercent}%, rgba(128, 128, 128, 0.2) ${progressPercent}%, rgba(128, 128, 128, 0.2) 100%)`,
|
||||
}}
|
||||
/>
|
||||
|
||||
<span className="text-xs text-text/60 min-w-[30px] tabular-nums">
|
||||
{formatTime(duration)}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
Loading…
Reference in a new issue