diff options
| author | Malte Voos <git@mal.tc> | 2025-12-05 15:35:38 +0100 |
|---|---|---|
| committer | Malte Voos <git@mal.tc> | 2025-12-05 15:43:58 +0100 |
| commit | c347b6133365dcf1b7da4e77890b20d04d6cfba4 (patch) | |
| tree | c83aac6f7d1e6edc57e607f01e5d3eeee8da4a0e | |
| parent | 652b1c2a0ce7db4885ebc51f7f09133a43401442 (diff) | |
| download | lleap-main.tar.gz lleap-main.zip | |
| -rw-r--r-- | Cargo.lock | 1467 | ||||
| -rw-r--r-- | Cargo.toml | 2 | ||||
| -rw-r--r-- | data/style.css (renamed from resources/style.css) | 4 | ||||
| -rw-r--r-- | flake.nix | 1 | ||||
| -rw-r--r-- | src/app.rs | 335 | ||||
| -rw-r--r-- | src/cue_view.rs | 81 | ||||
| -rw-r--r-- | src/main.rs | 9 | ||||
| -rw-r--r-- | src/open_dialog.rs | 58 | ||||
| -rw-r--r-- | src/preferences_dialog.rs (renamed from src/preferences.rs) | 29 | ||||
| -rw-r--r-- | src/settings.rs | 5 | ||||
| -rw-r--r-- | src/subtitle_selection_dialog.rs | 148 | ||||
| -rw-r--r-- | src/subtitle_view.rs | 57 | ||||
| -rw-r--r-- | src/subtitles/extraction/embedded.rs (renamed from src/subtitle_extraction/embedded.rs) | 18 | ||||
| -rw-r--r-- | src/subtitles/extraction/mod.rs (renamed from src/subtitle_extraction/mod.rs) | 12 | ||||
| -rw-r--r-- | src/subtitles/extraction/whisper.rs (renamed from src/subtitle_extraction/whisper.rs) | 16 | ||||
| -rw-r--r-- | src/subtitles/mod.rs | 86 | ||||
| -rw-r--r-- | src/subtitles/state.rs | 63 | ||||
| -rw-r--r-- | src/track_selector.rs | 26 | ||||
| -rw-r--r-- | src/tracks.rs | 38 | ||||
| -rw-r--r-- | src/transcript.rs | 8 | ||||
| -rw-r--r-- | src/translation/deepl.rs | 106 | ||||
| -rw-r--r-- | src/translation/mod.rs | 11 | ||||
| -rw-r--r-- | src/util/tracker.rs | 18 | ||||
| -rw-r--r-- | subs.srt | 644 |
24 files changed, 2172 insertions, 1070 deletions
diff --git a/Cargo.lock b/Cargo.lock index f3b1d1c..6a00a96 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -62,7 +62,7 @@ version = "1.1.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "40c48f72fd53cd289104fc64099abca73db4166ad86ea0b4341abe65af83dadc" dependencies = [ - "windows-sys", + "windows-sys 0.61.2", ] [[package]] @@ -73,7 +73,7 @@ checksum = "291e6a250ff86cd4a820112fb8898808a366d8f9f58ce16d1f538353ad55747d" dependencies = [ "anstyle", "once_cell_polyfill", - "windows-sys", + "windows-sys 0.61.2", ] [[package]] @@ -83,6 +83,12 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a23eb6b1614318a8071c9b2521f36b424b2c83db5eb3a0fead4a6c0809af6e61" [[package]] +name = "arrayvec" +version = "0.7.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7c02d123df017efcdfbd739ef81735b36c5ba83ec3c59c80a9d7ecc718f92e50" + +[[package]] name = "async-channel" version = "2.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" @@ -95,6 +101,12 @@ dependencies = [ ] [[package]] +name = "atomic-waker" +version = "1.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1505bd5d3d116872e7271a6d4e16d81d0c8570876c8de68093a09ac269d8aac0" + +[[package]] name = "atomic_refcell" version = "0.1.13" source = "registry+https://github.com/rust-lang/crates.io-index" @@ -107,6 +119,12 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c08606f8c3cbf4ce6ec8e28fb0014a2c086708fe954eaa885384a6165172e7e8" [[package]] +name = "base64" +version = "0.22.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "72b3254f16251a8381aa12e40e3c4d2f0199f8c6508fbecb9d91f575e0fbb8c6" + +[[package]] name = "bindgen" version = "0.72.1" source = "registry+https://github.com/rust-lang/crates.io-index" @@ -121,7 +139,7 @@ dependencies = [ "regex", "rustc-hash", "shlex", - "syn", + "syn 2.0.110", ] [[package]] @@ -143,6 +161,12 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "46c5e41b57b8bba42a04676d81cb89e9ee8e859a1a66f80a5a72e1cb76b34d43" [[package]] +name = "bytes" +version = "1.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b35204fbdc0b3f4446b89fc1ac2cf84a8a68971995d0bf2e925ec7cd960f9cb3" + +[[package]] name = "cairo-rs" version = "0.21.2" source = "registry+https://github.com/rust-lang/crates.io-index" @@ -231,9 +255,9 @@ dependencies = [ "bitflags", "block", "cocoa-foundation", - "core-foundation", + "core-foundation 0.10.1", "core-graphics", - "foreign-types", + "foreign-types 0.5.0", "libc", "objc", ] @@ -246,7 +270,7 @@ checksum = "81411967c50ee9a1fc11365f8c585f863a22a9697c89239c452292c40ba79b0d" dependencies = [ "bitflags", "block", - "core-foundation", + "core-foundation 0.10.1", "core-graphics-types", "objc", ] @@ -268,6 +292,16 @@ dependencies = [ [[package]] name = "core-foundation" +version = "0.9.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "91e195e091a93c46f7102ec7818a2aa394e1e1771c3ab4825963fa03e45afb8f" +dependencies = [ + "core-foundation-sys", + "libc", +] + +[[package]] +name = "core-foundation" version = "0.10.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b2a6cd9ae233e7f62ba4e9353e81a88df7fc8a5987b8d445b4d90c879bd156f6" @@ -289,9 +323,9 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "fa95a34622365fa5bbf40b20b75dba8dfa8c94c734aea8ac9a5ca38af14316f1" dependencies = [ "bitflags", - "core-foundation", + "core-foundation 0.10.1", "core-graphics-types", - "foreign-types", + "foreign-types 0.5.0", "libc", ] @@ -302,7 +336,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3d44a101f213f6c4cdc1853d4b78aef6db6bdfa3468798cc1d9912f4735013eb" dependencies = [ "bitflags", - "core-foundation", + "core-foundation 0.10.1", "libc", ] @@ -322,12 +356,89 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d0a5c400df2834b80a4c3327b3aad3a4c4cd4de0629063962b03235697506a28" [[package]] +name = "deepl" +version = "0.7.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e315350d74306624c6519aea28860100be984dfbec7939a36d6b190fc0b77fb1" +dependencies = [ + "paste", + "reqwest", + "serde", + "serde_json", + "thiserror", + "tokio", + "tokio-stream", + "typed-builder", +] + +[[package]] +name = "deluxe" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8ed332aaf752b459088acf3dd4eca323e3ef4b83c70a84ca48fb0ec5305f1488" +dependencies = [ + "deluxe-core", + "deluxe-macros", + "once_cell", + "proc-macro2", + "syn 2.0.110", +] + +[[package]] +name = "deluxe-core" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "eddada51c8576df9d6a8450c351ff63042b092c9458b8ac7d20f89cbd0ffd313" +dependencies = [ + "arrayvec", + "proc-macro2", + "quote", + "strsim", + "syn 2.0.110", +] + +[[package]] +name = "deluxe-macros" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f87546d9c837f0b7557e47b8bd6eae52c3c223141b76aa233c345c9ab41d9117" +dependencies = [ + "deluxe-core", + "heck 0.4.1", + "if_chain", + "proc-macro-crate 1.3.1", + "proc-macro2", + "quote", + "syn 2.0.110", +] + +[[package]] +name = "displaydoc" +version = "0.2.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "97369cbbc041bc366949bc74d34658d6cda5621039731c6310521892a3a20ae0" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.110", +] + +[[package]] name = "either" version = "1.15.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "48c757948c5ede0e46177b7add2e67155f70e33c07fea8284df6576da70b3719" [[package]] +name = "encoding_rs" +version = "0.8.35" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "75030f3c4f45dafd7586dd6780965a8c7e8e285a5ecb86713e63a79c5b2766f3" +dependencies = [ + "cfg-if", +] + +[[package]] name = "endi" version = "1.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" @@ -363,6 +474,16 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "877a4ace8713b0bcf2a4e7eec82529c029f1d0619886d18145fea96c3ffe5c0f" [[package]] +name = "errno" +version = "0.3.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "39cab71617ae0d63f51a36d69f866391735b51691dbda63cf6f96d042b63efeb" +dependencies = [ + "libc", + "windows-sys 0.61.2", +] + +[[package]] name = "event-listener" version = "5.4.1" source = "registry+https://github.com/rust-lang/crates.io-index" @@ -384,6 +505,12 @@ dependencies = [ ] [[package]] +name = "fastrand" +version = "2.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "37909eebbb50d72f9059c3b6d82c0463f2ff062c9e95845c43a6c9c0355411be" + +[[package]] name = "ffmpeg-next" version = "8.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" @@ -447,13 +574,28 @@ dependencies = [ ] [[package]] +name = "fnv" +version = "1.0.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3f9eec918d3f24069decb9af1554cad7c880e2da24a9afd88aca000531ab82c1" + +[[package]] +name = "foreign-types" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f6f339eb8adc052cd2ca78910fda869aefa38d22d5cb648e6485e4d3fc06f3b1" +dependencies = [ + "foreign-types-shared 0.1.1", +] + +[[package]] name = "foreign-types" version = "0.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d737d9aa519fb7b749cbc3b962edcf310a8dd1f4b67c91c4f83975dbdd17d965" dependencies = [ "foreign-types-macros", - "foreign-types-shared", + "foreign-types-shared 0.3.1", ] [[package]] @@ -464,16 +606,31 @@ checksum = "1a5c6c585bc94aaf2c7b51dd4c2ba22680844aba4c687be581871a6f518c5742" dependencies = [ "proc-macro2", "quote", - "syn", + "syn 2.0.110", ] [[package]] name = "foreign-types-shared" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "00b0228411908ca8685dba7fc2cdd70ec9990a6e753e89b6ac91a84c40fbaf4b" + +[[package]] +name = "foreign-types-shared" version = "0.3.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "aa9a19cbb55df58761df49b23516a86d432839add4af60fc256da840f66ed35b" [[package]] +name = "form_urlencoded" +version = "1.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cb4cb245038516f5f85277875cdaa4f7d2c9a0fa0468de06ed190163b1581fcf" +dependencies = [ + "percent-encoding", +] + +[[package]] name = "fragile" version = "2.0.1" source = "registry+https://github.com/rust-lang/crates.io-index" @@ -535,7 +692,7 @@ checksum = "162ee34ebcb7c64a8abebc059ce0fee27c2262618d7b60ed8faf72fef13c3650" dependencies = [ "proc-macro2", "quote", - "syn", + "syn 2.0.110", ] [[package]] @@ -664,6 +821,18 @@ dependencies = [ ] [[package]] +name = "getrandom" +version = "0.3.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "899def5c37c4fd7b2664648c28120ecec138e4d395b459e5ca34f9cce2dd77fd" +dependencies = [ + "cfg-if", + "libc", + "r-efi", + "wasip2", +] + +[[package]] name = "gio" version = "0.21.4" source = "registry+https://github.com/rust-lang/crates.io-index" @@ -690,7 +859,7 @@ dependencies = [ "gobject-sys", "libc", "system-deps", - "windows-sys", + "windows-sys 0.61.2", ] [[package]] @@ -720,11 +889,11 @@ version = "0.21.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "880e524e0085f3546cfb38532b2c202c0d64741d9977a6e4aa24704bfc9f19fb" dependencies = [ - "heck", - "proc-macro-crate", + "heck 0.5.0", + "proc-macro-crate 3.4.0", "proc-macro2", "quote", - "syn", + "syn 2.0.110", ] [[package]] @@ -778,6 +947,22 @@ dependencies = [ ] [[package]] +name = "gsettings-macro" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3f3b38a7ca9498c2a7ed01ec9cff1f07397d8bc8f4a345b46a58a80170947263" +dependencies = [ + "deluxe", + "heck 0.5.0", + "proc-macro-error", + "proc-macro2", + "quick-xml 0.37.5", + "quote", + "serde", + "syn 2.0.110", +] + +[[package]] name = "gsk4" version = "0.10.1" source = "registry+https://github.com/rust-lang/crates.io-index" @@ -822,7 +1007,7 @@ dependencies = [ "gstreamer-gl", "gstreamer-video", "gtk4", - "windows-sys", + "windows-sys 0.61.2", ] [[package]] @@ -832,7 +1017,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a68a894ef2d738054b950e1dbef5d9012b63fd968d4d32dbccd31bd8d8d4b219" dependencies = [ "chrono", - "toml_edit", + "toml_edit 0.23.7", ] [[package]] @@ -1013,10 +1198,10 @@ version = "0.10.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "821160b4f17e7e4ed748818c23682d0a46bed04c287dbaac54dd4869d2c5e06a" dependencies = [ - "proc-macro-crate", + "proc-macro-crate 3.4.0", "proc-macro2", "quote", - "syn", + "syn 2.0.110", ] [[package]] @@ -1045,7 +1230,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b9d94c8a63f94bbc35cf63e105791c5992bd60d4516d41fe5bf3db8d10b30b43" dependencies = [ "flate2", - "quick-xml", + "quick-xml 0.38.4", "serde", "serde_json", "walkdir", @@ -1054,6 +1239,25 @@ dependencies = [ ] [[package]] +name = "h2" +version = "0.4.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f3c0b69cfcb4e1b9f1bf2f53f95f766e4661169728ec61cd3fe5a0166f2d1386" +dependencies = [ + "atomic-waker", + "bytes", + "fnv", + "futures-core", + "futures-sink", + "http", + "indexmap", + "slab", + "tokio", + "tokio-util", + "tracing", +] + +[[package]] name = "hashbrown" version = "0.16.1" source = "registry+https://github.com/rust-lang/crates.io-index" @@ -1061,6 +1265,12 @@ checksum = "841d1cc9bed7f9236f321df977030373f4a4163ae1a7dbfe1a51a2c1a51d9100" [[package]] name = "heck" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "95505c38b4572b2d910cecb0281560f54b440a19336cbbcb27bf6ce6adc6f5a8" + +[[package]] +name = "heck" version = "0.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "2304e00983f87ffb38b55b444b5e3b60a884b5d30c0fca7d82fe33449bbe55ea" @@ -1072,6 +1282,125 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "fc0fef456e4baa96da950455cd02c081ca953b141298e41db3fc7e36b1da849c" [[package]] +name = "http" +version = "1.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e3ba2a386d7f85a81f119ad7498ebe444d2e22c2af0b86b069416ace48b3311a" +dependencies = [ + "bytes", + "itoa", +] + +[[package]] +name = "http-body" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1efedce1fb8e6913f23e0c92de8e62cd5b772a67e7b3946df930a62566c93184" +dependencies = [ + "bytes", + "http", +] + +[[package]] +name = "http-body-util" +version = "0.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b021d93e26becf5dc7e1b75b1bed1fd93124b374ceb73f43d4d4eafec896a64a" +dependencies = [ + "bytes", + "futures-core", + "http", + "http-body", + "pin-project-lite", +] + +[[package]] +name = "httparse" +version = "1.10.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6dbf3de79e51f3d586ab4cb9d5c3e2c14aa28ed23d180cf89b4df0454a69cc87" + +[[package]] +name = "hyper" +version = "1.8.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2ab2d4f250c3d7b1c9fcdff1cece94ea4e2dfbec68614f7b87cb205f24ca9d11" +dependencies = [ + "atomic-waker", + "bytes", + "futures-channel", + "futures-core", + "h2", + "http", + "http-body", + "httparse", + "itoa", + "pin-project-lite", + "pin-utils", + "smallvec", + "tokio", + "want", +] + +[[package]] +name = "hyper-rustls" +version = "0.27.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e3c93eb611681b207e1fe55d5a71ecf91572ec8a6705cdb6857f7d8d5242cf58" +dependencies = [ + "http", + "hyper", + "hyper-util", + "rustls", + "rustls-pki-types", + "tokio", + "tokio-rustls", + "tower-service", +] + +[[package]] +name = "hyper-tls" +version = "0.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "70206fc6890eaca9fde8a0bf71caa2ddfc9fe045ac9e5c70df101a7dbde866e0" +dependencies = [ + "bytes", + "http-body-util", + "hyper", + "hyper-util", + "native-tls", + "tokio", + "tokio-native-tls", + "tower-service", +] + +[[package]] +name = "hyper-util" +version = "0.1.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "52e9a2a24dc5c6821e71a7030e1e14b7b632acac55c40e9d2e082c621261bb56" +dependencies = [ + "base64", + "bytes", + "futures-channel", + "futures-core", + "futures-util", + "http", + "http-body", + "hyper", + "ipnet", + "libc", + "percent-encoding", + "pin-project-lite", + "socket2", + "system-configuration", + "tokio", + "tower-service", + "tracing", + "windows-registry", +] + +[[package]] name = "iana-time-zone" version = "0.1.64" source = "registry+https://github.com/rust-lang/crates.io-index" @@ -1096,6 +1425,114 @@ dependencies = [ ] [[package]] +name = "icu_collections" +version = "2.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4c6b649701667bbe825c3b7e6388cb521c23d88644678e83c0c4d0a621a34b43" +dependencies = [ + "displaydoc", + "potential_utf", + "yoke", + "zerofrom", + "zerovec", +] + +[[package]] +name = "icu_locale_core" +version = "2.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "edba7861004dd3714265b4db54a3c390e880ab658fec5f7db895fae2046b5bb6" +dependencies = [ + "displaydoc", + "litemap", + "tinystr", + "writeable", + "zerovec", +] + +[[package]] +name = "icu_normalizer" +version = "2.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5f6c8828b67bf8908d82127b2054ea1b4427ff0230ee9141c54251934ab1b599" +dependencies = [ + "icu_collections", + "icu_normalizer_data", + "icu_properties", + "icu_provider", + "smallvec", + "zerovec", +] + +[[package]] +name = "icu_normalizer_data" +version = "2.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7aedcccd01fc5fe81e6b489c15b247b8b0690feb23304303a9e560f37efc560a" + +[[package]] +name = "icu_properties" +version = "2.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e93fcd3157766c0c8da2f8cff6ce651a31f0810eaa1c51ec363ef790bbb5fb99" +dependencies = [ + "icu_collections", + "icu_locale_core", + "icu_properties_data", + "icu_provider", + "zerotrie", + "zerovec", +] + +[[package]] +name = "icu_properties_data" +version = "2.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "02845b3647bb045f1100ecd6480ff52f34c35f82d9880e029d329c21d1054899" + +[[package]] +name = "icu_provider" +version = "2.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "85962cf0ce02e1e0a629cc34e7ca3e373ce20dda4c4d7294bbd0bf1fdb59e614" +dependencies = [ + "displaydoc", + "icu_locale_core", + "writeable", + "yoke", + "zerofrom", + "zerotrie", + "zerovec", +] + +[[package]] +name = "idna" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3b0875f23caa03898994f6ddc501886a45c7d3d62d04d2d90788d47be1b1e4de" +dependencies = [ + "idna_adapter", + "smallvec", + "utf8_iter", +] + +[[package]] +name = "idna_adapter" +version = "1.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3acae9609540aa318d1bc588455225fb2085b9ed0c4f6bd0d9d5bcd86f1a0344" +dependencies = [ + "icu_normalizer", + "icu_properties", +] + +[[package]] +name = "if_chain" +version = "1.0.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cd62e6b5e86ea8eeeb8db1de02880a6abc01a397b2ebb64b5d74ac255318f5cb" + +[[package]] name = "indexmap" version = "2.12.1" source = "registry+https://github.com/rust-lang/crates.io-index" @@ -1106,6 +1543,22 @@ dependencies = [ ] [[package]] +name = "ipnet" +version = "2.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "469fb0b9cefa57e3ef31275ee7cacb78f2fdca44e4765491884a2b119d4eb130" + +[[package]] +name = "iri-string" +version = "0.7.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4f867b9d1d896b67beb18518eda36fdb77a32ea590de864f1325b294a6d14397" +dependencies = [ + "memchr", + "serde", +] + +[[package]] name = "is_terminal_polyfill" version = "1.70.2" source = "registry+https://github.com/rust-lang/crates.io-index" @@ -1164,7 +1617,7 @@ checksum = "980af8b43c3ad5d8d349ace167ec8170839f753a42d233ba19e08afe1850fa69" dependencies = [ "proc-macro2", "quote", - "syn", + "syn 2.0.110", ] [[package]] @@ -1234,14 +1687,28 @@ dependencies = [ ] [[package]] +name = "linux-raw-sys" +version = "0.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "df1d3c3b53da64cf5760482273a98e575c651a67eec7f77df96b5b642de8f039" + +[[package]] +name = "litemap" +version = "0.8.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6373607a59f0be73a39b6fe456b8192fcc3585f602af20751600e974dd455e77" + +[[package]] name = "lleap" version = "0.1.0" dependencies = [ "anyhow", "async-channel", "cocoa", + "deepl", "env_logger", "ffmpeg-next", + "gsettings-macro", "gst-plugin-gtk4", "gstreamer", "gstreamer-play", @@ -1302,6 +1769,22 @@ dependencies = [ ] [[package]] +name = "mime" +version = "0.3.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6877bb514081ee2a7ff5ef9de3281f14a4dd4bceac4c09388074a6b5df8a139a" + +[[package]] +name = "mime_guess" +version = "2.0.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f7c44f8e672c00fe5308fa235f821cb4198414e1c77935c1ab6948d3fd78550e" +dependencies = [ + "mime", + "unicase", +] + +[[package]] name = "minimal-lexical" version = "0.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" @@ -1318,6 +1801,17 @@ dependencies = [ ] [[package]] +name = "mio" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "69d83b0086dc8ecf3ce9ae2874b2d1290252e2a30720bea58a5c6639b0092873" +dependencies = [ + "libc", + "wasi", + "windows-sys 0.61.2", +] + +[[package]] name = "muldiv" version = "1.0.1" source = "registry+https://github.com/rust-lang/crates.io-index" @@ -1329,7 +1823,24 @@ version = "0.7.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "6a51313c5820b0b02bd422f4b44776fbf47961755c74ce64afc73bfad10226c3" dependencies = [ - "getrandom", + "getrandom 0.2.16", +] + +[[package]] +name = "native-tls" +version = "0.2.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "87de3442987e9dbec73158d5c715e7ad9072fda936bb03d19d7fa10e00520f0e" +dependencies = [ + "libc", + "log", + "openssl", + "openssl-probe", + "openssl-sys", + "schannel", + "security-framework", + "security-framework-sys", + "tempfile", ] [[package]] @@ -1402,6 +1913,50 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "384b8ab6d37215f3c5301a95a4accb5d64aa607f1fcb26a11b5303878451b4fe" [[package]] +name = "openssl" +version = "0.10.75" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "08838db121398ad17ab8531ce9de97b244589089e290a384c900cb9ff7434328" +dependencies = [ + "bitflags", + "cfg-if", + "foreign-types 0.3.2", + "libc", + "once_cell", + "openssl-macros", + "openssl-sys", +] + +[[package]] +name = "openssl-macros" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a948666b637a0f465e8564c73e89d4dde00d72d4d473cc972f390fc3dcee7d9c" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.110", +] + +[[package]] +name = "openssl-probe" +version = "0.1.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d05e27ee213611ffe7d6348b942e8f942b37114c00cc03cec254295a4a17852e" + +[[package]] +name = "openssl-sys" +version = "0.9.111" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "82cab2d520aa75e3c58898289429321eb788c3106963d0dc886ec7a5f4adc321" +dependencies = [ + "cc", + "libc", + "pkg-config", + "vcpkg", +] + +[[package]] name = "option-operations" version = "0.6.0" source = "registry+https://github.com/rust-lang/crates.io-index" @@ -1441,12 +1996,24 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f38d5652c16fde515bb1ecef450ab0f6a219d619a7274976324d5e377f7dceba" [[package]] +name = "paste" +version = "1.0.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "57c0d7b74b563b49d38dae00a0c37d4d6de9b432382b2892f0574ddcae73fd0a" + +[[package]] name = "pastey" version = "0.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "35fb2e5f958ec131621fdd531e9fc186ed768cbe395337403ae56c17a74c68ec" [[package]] +name = "percent-encoding" +version = "2.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9b4f627cb1b25917193a259e49bdad08f671f8d9708acfd5fe0a8c1455d87220" + +[[package]] name = "phf" version = "0.11.3" source = "registry+https://github.com/rust-lang/crates.io-index" @@ -1498,12 +2065,55 @@ dependencies = [ ] [[package]] +name = "potential_utf" +version = "0.1.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b73949432f5e2a09657003c25bca5e19a0e9c84f8058ca374f49e0ebe605af77" +dependencies = [ + "zerovec", +] + +[[package]] +name = "proc-macro-crate" +version = "1.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7f4c021e1093a56626774e81216a4ce732a735e5bad4868a03f3ed65ca0c3919" +dependencies = [ + "once_cell", + "toml_edit 0.19.15", +] + +[[package]] name = "proc-macro-crate" version = "3.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "219cb19e96be00ab2e37d6e299658a0cfa83e52429179969b0f0121b4ac46983" dependencies = [ - "toml_edit", + "toml_edit 0.23.7", +] + +[[package]] +name = "proc-macro-error" +version = "1.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "da25490ff9892aab3fcf7c36f08cfb902dd3e71ca0f9f9517bea02a73a5ce38c" +dependencies = [ + "proc-macro-error-attr", + "proc-macro2", + "quote", + "syn 1.0.109", + "version_check", +] + +[[package]] +name = "proc-macro-error-attr" +version = "1.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a1be40180e52ecc98ad80b184934baf3d0d29f979574e439af5a55274b35f869" +dependencies = [ + "proc-macro2", + "quote", + "version_check", ] [[package]] @@ -1517,6 +2127,16 @@ dependencies = [ [[package]] name = "quick-xml" +version = "0.37.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "331e97a1af0bf59823e6eadffe373d7b27f485be8748f71471c662c1f269b7fb" +dependencies = [ + "memchr", + "serde", +] + +[[package]] +name = "quick-xml" version = "0.38.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b66c2058c55a409d601666cffe35f04333cf1013010882cec174a7467cd4e21c" @@ -1535,6 +2155,12 @@ dependencies = [ ] [[package]] +name = "r-efi" +version = "5.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "69cdb34c158ceb288df11e18b4bd39de994f6657d83847bdffdbd7f346754b0f" + +[[package]] name = "regex" version = "1.12.2" source = "registry+https://github.com/rust-lang/crates.io-index" @@ -1627,7 +2253,65 @@ checksum = "175fce497fc6f11dde7ea56daa30ff7ad29a534bbc209d59d766659c880ba5f1" dependencies = [ "proc-macro2", "quote", - "syn", + "syn 2.0.110", +] + +[[package]] +name = "reqwest" +version = "0.12.24" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9d0946410b9f7b082a427e4ef5c8ff541a88b357bc6c637c40db3a68ac70a36f" +dependencies = [ + "base64", + "bytes", + "encoding_rs", + "futures-core", + "futures-util", + "h2", + "http", + "http-body", + "http-body-util", + "hyper", + "hyper-rustls", + "hyper-tls", + "hyper-util", + "js-sys", + "log", + "mime", + "mime_guess", + "native-tls", + "percent-encoding", + "pin-project-lite", + "rustls-pki-types", + "serde", + "serde_json", + "serde_urlencoded", + "sync_wrapper", + "tokio", + "tokio-native-tls", + "tokio-util", + "tower", + "tower-http", + "tower-service", + "url", + "wasm-bindgen", + "wasm-bindgen-futures", + "wasm-streams", + "web-sys", +] + +[[package]] +name = "ring" +version = "0.17.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a4689e6c2294d81e88dc6261c768b63bc4fcdb852be6d1352498b114f61383b7" +dependencies = [ + "cc", + "cfg-if", + "getrandom 0.2.16", + "libc", + "untrusted", + "windows-sys 0.52.0", ] [[package]] @@ -1646,6 +2330,52 @@ dependencies = [ ] [[package]] +name = "rustix" +version = "1.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cd15f8a2c5551a84d56efdc1cd049089e409ac19a3072d5037a17fd70719ff3e" +dependencies = [ + "bitflags", + "errno", + "libc", + "linux-raw-sys", + "windows-sys 0.61.2", +] + +[[package]] +name = "rustls" +version = "0.23.35" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "533f54bc6a7d4f647e46ad909549eda97bf5afc1585190ef692b4286b198bd8f" +dependencies = [ + "once_cell", + "rustls-pki-types", + "rustls-webpki", + "subtle", + "zeroize", +] + +[[package]] +name = "rustls-pki-types" +version = "1.13.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "94182ad936a0c91c324cd46c6511b9510ed16af436d7b5bab34beab0afd55f7a" +dependencies = [ + "zeroize", +] + +[[package]] +name = "rustls-webpki" +version = "0.103.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2ffdfa2f5286e2247234e03f680868ac2815974dc39e00ea15adc445d0aafe52" +dependencies = [ + "ring", + "rustls-pki-types", + "untrusted", +] + +[[package]] name = "rustversion" version = "1.0.22" source = "registry+https://github.com/rust-lang/crates.io-index" @@ -1667,12 +2397,44 @@ dependencies = [ ] [[package]] +name = "schannel" +version = "0.1.28" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "891d81b926048e76efe18581bf793546b4c0eaf8448d72be8de2bbee5fd166e1" +dependencies = [ + "windows-sys 0.61.2", +] + +[[package]] name = "scopeguard" version = "1.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "94143f37725109f92c262ed2cf5e59bce7498c01bcc1502d7b9afe439a4e9f49" [[package]] +name = "security-framework" +version = "2.11.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "897b2245f0b511c87893af39b033e5ca9cce68824c4d7e7630b5a1d339658d02" +dependencies = [ + "bitflags", + "core-foundation 0.9.4", + "core-foundation-sys", + "libc", + "security-framework-sys", +] + +[[package]] +name = "security-framework-sys" +version = "2.15.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cc1f0cbffaac4852523ce30d8bd3c5cdc873501d96ff467ca09b6767bb8cd5c0" +dependencies = [ + "core-foundation-sys", + "libc", +] + +[[package]] name = "semver" version = "1.0.27" source = "registry+https://github.com/rust-lang/crates.io-index" @@ -1705,7 +2467,7 @@ checksum = "d540f220d3187173da220f885ab66608367b6574e925011a9353e4badda91d79" dependencies = [ "proc-macro2", "quote", - "syn", + "syn 2.0.110", ] [[package]] @@ -1731,6 +2493,18 @@ dependencies = [ ] [[package]] +name = "serde_urlencoded" +version = "0.7.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d3491c14715ca2294c4d6a88f15e84739788c1d030eed8c110436aafdaa2f3fd" +dependencies = [ + "form_urlencoded", + "itoa", + "ryu", + "serde", +] + +[[package]] name = "shlex" version = "1.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" @@ -1761,6 +2535,16 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "67b1b7a3b5fe4f1376887184045fcf45c69e92af734b7aaddc05fb777b6fbd03" [[package]] +name = "socket2" +version = "0.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "17129e116933cf371d018bb80ae557e889637989d8638274fb25622827b03881" +dependencies = [ + "libc", + "windows-sys 0.60.2", +] + +[[package]] name = "spin" version = "0.9.8" source = "registry+https://github.com/rust-lang/crates.io-index" @@ -1770,12 +2554,40 @@ dependencies = [ ] [[package]] +name = "stable_deref_trait" +version = "1.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6ce2be8dc25455e1f91df71bfa12ad37d7af1092ae736f3a6cd0e37bc7810596" + +[[package]] name = "static_assertions" version = "1.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a2eb9349b6444b326872e140eb1cf5e7c522154d69e7a0ffb0fb81c06b37543f" [[package]] +name = "strsim" +version = "0.10.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "73473c0e59e6d5812c5dfe2a064a6444949f089e20eec9a2e5506596494e4623" + +[[package]] +name = "subtle" +version = "2.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "13c2bddecc57b384dee18652358fb23172facb8a2c51ccc10d74c157bdea3292" + +[[package]] +name = "syn" +version = "1.0.109" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "72b64191b275b66ffe2469e8af2c1cfe3bafa67b529ead792a6d0160888b4237" +dependencies = [ + "proc-macro2", + "unicode-ident", +] + +[[package]] name = "syn" version = "2.0.110" source = "registry+https://github.com/rust-lang/crates.io-index" @@ -1787,13 +2599,54 @@ dependencies = [ ] [[package]] +name = "sync_wrapper" +version = "1.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0bf256ce5efdfa370213c1dabab5935a12e49f2c58d15e9eac2870d3b4f27263" +dependencies = [ + "futures-core", +] + +[[package]] +name = "synstructure" +version = "0.13.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "728a70f3dbaf5bab7f0c4b1ac8d7ae5ea60a4b5549c8a5914361c99147a709d2" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.110", +] + +[[package]] +name = "system-configuration" +version = "0.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3c879d448e9d986b661742763247d3693ed13609438cf3d006f51f5368a5ba6b" +dependencies = [ + "bitflags", + "core-foundation 0.9.4", + "system-configuration-sys", +] + +[[package]] +name = "system-configuration-sys" +version = "0.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8e1d1b10ced5ca923a1fcb8d03e96b8d3268065d724548c0211415ff6ac6bac4" +dependencies = [ + "core-foundation-sys", + "libc", +] + +[[package]] name = "system-deps" version = "7.0.7" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "48c8f33736f986f16d69b6cb8b03f55ddcad5c41acc4ccc39dd88e84aa805e7f" dependencies = [ "cfg-expr", - "heck", + "heck 0.5.0", "pkg-config", "toml", "version-compare", @@ -1806,6 +2659,19 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "df7f62577c25e07834649fc3b39fafdc597c0a3527dc1c60129201ccfcbaa50c" [[package]] +name = "tempfile" +version = "3.23.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2d31c77bdf42a745371d260a26ca7163f1e0924b64afa0b688e61b5a9fa02f16" +dependencies = [ + "fastrand", + "getrandom 0.3.4", + "once_cell", + "rustix", + "windows-sys 0.61.2", +] + +[[package]] name = "thiserror" version = "2.0.17" source = "registry+https://github.com/rust-lang/crates.io-index" @@ -1822,7 +2688,17 @@ checksum = "3ff15c8ecd7de3849db632e14d18d2571fa09dfc5ed93479bc4485c7a517c913" dependencies = [ "proc-macro2", "quote", - "syn", + "syn 2.0.110", +] + +[[package]] +name = "tinystr" +version = "0.8.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "42d3e9c45c09de15d06dd8acf5f4e0e399e85927b7f00711024eb7ae10fa4869" +dependencies = [ + "displaydoc", + "zerovec", ] [[package]] @@ -1831,7 +2707,68 @@ version = "1.48.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ff360e02eab121e0bc37a2d3b4d4dc622e6eda3a8e5253d5435ecf5bd4c68408" dependencies = [ + "bytes", + "libc", + "mio", "pin-project-lite", + "socket2", + "tokio-macros", + "windows-sys 0.61.2", +] + +[[package]] +name = "tokio-macros" +version = "2.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "af407857209536a95c8e56f8231ef2c2e2aff839b22e07a1ffcbc617e9db9fa5" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.110", +] + +[[package]] +name = "tokio-native-tls" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bbae76ab933c85776efabc971569dd6119c580d8f5d448769dec1764bf796ef2" +dependencies = [ + "native-tls", + "tokio", +] + +[[package]] +name = "tokio-rustls" +version = "0.26.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1729aa945f29d91ba541258c8df89027d5792d85a8841fb65e8bf0f4ede4ef61" +dependencies = [ + "rustls", + "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.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2efa149fe76073d6e8fd97ef4f4eca7b67f599660115591483572e406e165594" +dependencies = [ + "bytes", + "futures-core", + "futures-sink", + "pin-project-lite", + "tokio", ] [[package]] @@ -1843,14 +2780,20 @@ dependencies = [ "indexmap", "serde_core", "serde_spanned", - "toml_datetime", + "toml_datetime 0.7.3", "toml_parser", "toml_writer", - "winnow", + "winnow 0.7.13", ] [[package]] name = "toml_datetime" +version = "0.6.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "22cddaf88f4fbc13c51aebbf5f8eceb5c7c5a9da2ac40a13519eb5b0a0e8f11c" + +[[package]] +name = "toml_datetime" version = "0.7.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f2cdb639ebbc97961c51720f858597f7f24c4fc295327923af55b74c3c724533" @@ -1860,14 +2803,25 @@ dependencies = [ [[package]] name = "toml_edit" +version = "0.19.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1b5bb770da30e5cbfde35a2d7b9b8a2c4b8ef89548a7a6aeab5c9a576e3e7421" +dependencies = [ + "indexmap", + "toml_datetime 0.6.11", + "winnow 0.5.40", +] + +[[package]] +name = "toml_edit" version = "0.23.7" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "6485ef6d0d9b5d0ec17244ff7eb05310113c3f316f2d14200d4de56b3cb98f8d" dependencies = [ "indexmap", - "toml_datetime", + "toml_datetime 0.7.3", "toml_parser", - "winnow", + "winnow 0.7.13", ] [[package]] @@ -1876,7 +2830,7 @@ version = "1.0.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c0cbe268d35bdb4bb5a56a2de88d0ad0eb70af5384a99d648cd4b3d04039800e" dependencies = [ - "winnow", + "winnow 0.7.13", ] [[package]] @@ -1886,6 +2840,51 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "df8b2b54733674ad286d16267dcfc7a71ed5c776e4ac7aa3c3e2561f7c637bf2" [[package]] +name = "tower" +version = "0.5.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d039ad9159c98b70ecfd540b2573b97f7f52c3e8d9f8ad57a24b916a536975f9" +dependencies = [ + "futures-core", + "futures-util", + "pin-project-lite", + "sync_wrapper", + "tokio", + "tower-layer", + "tower-service", +] + +[[package]] +name = "tower-http" +version = "0.6.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9cf146f99d442e8e68e585f5d798ccd3cad9a7835b917e09728880a862706456" +dependencies = [ + "bitflags", + "bytes", + "futures-util", + "http", + "http-body", + "iri-string", + "pin-project-lite", + "tower", + "tower-layer", + "tower-service", +] + +[[package]] +name = "tower-layer" +version = "0.3.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "121c2a6cda46980bb0fcd1647ffaf6cd3fc79a013de288782836f6df9c48780e" + +[[package]] +name = "tower-service" +version = "0.3.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8df9b6e13f2d32c91b9bd719c00d1958837bc7dec474d94952798cc8e69eeec3" + +[[package]] name = "tracing" version = "0.1.41" source = "registry+https://github.com/rust-lang/crates.io-index" @@ -1904,7 +2903,7 @@ checksum = "81383ab64e72a7a8b8e13130c49e3dab29def6d0c7d76a03087b3cf71c5c6903" dependencies = [ "proc-macro2", "quote", - "syn", + "syn 2.0.110", ] [[package]] @@ -1933,10 +2932,42 @@ checksum = "dc19eb2373ccf3d1999967c26c3d44534ff71ae5d8b9dacf78f4b13132229e48" dependencies = [ "proc-macro2", "quote", - "syn", + "syn 2.0.110", +] + +[[package]] +name = "try-lock" +version = "0.2.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e421abadd41a4225275504ea4d6566923418b7f05506fbc9c0fe86ba7396114b" + +[[package]] +name = "typed-builder" +version = "0.20.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cd9d30e3a08026c78f246b173243cf07b3696d274debd26680773b6773c2afc7" +dependencies = [ + "typed-builder-macro", +] + +[[package]] +name = "typed-builder-macro" +version = "0.20.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3c36781cc0e46a83726d9879608e4cf6c2505237e263a8eb8c24502989cfdb28" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.110", ] [[package]] +name = "unicase" +version = "2.8.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "75b844d17643ee918803943289730bec8aac480150456169e647ed0b576ba539" + +[[package]] name = "unicode-ident" version = "1.0.22" source = "registry+https://github.com/rust-lang/crates.io-index" @@ -1949,6 +2980,30 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f6ccf251212114b54433ec949fd6a7841275f9ada20dddd2f29e9ceea4501493" [[package]] +name = "untrusted" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8ecb6da28b8a351d773b68d5825ac39017e680750f980f3a1a85cd8dd28a47c1" + +[[package]] +name = "url" +version = "2.5.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "08bc136a29a3d1758e07a9cca267be308aeebf5cfd5a10f3f67ab2097683ef5b" +dependencies = [ + "form_urlencoded", + "idna", + "percent-encoding", + "serde", +] + +[[package]] +name = "utf8_iter" +version = "1.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b6c140620e7ffbb22c2dee59cafe6084a59b5ffc27a8859a5f0d494b5d52b6be" + +[[package]] name = "utf8parse" version = "0.2.2" source = "registry+https://github.com/rust-lang/crates.io-index" @@ -1967,6 +3022,12 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "03c2856837ef78f57382f06b2b8563a2f512f7185d732608fd9176cb3b8edf0e" [[package]] +name = "version_check" +version = "0.9.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0b928f33d975fc6ad9f86c8f283853ad26bdd5b10b7f1542aa2fa15e2289105a" + +[[package]] name = "walkdir" version = "2.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" @@ -1977,12 +3038,30 @@ dependencies = [ ] [[package]] +name = "want" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bfa7760aed19e106de2c7c0b581b509f2f25d3dacaf737cb82ac61bc6d760b0e" +dependencies = [ + "try-lock", +] + +[[package]] name = "wasi" version = "0.11.1+wasi-snapshot-preview1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ccf3ec651a847eb01de73ccad15eb7d99f80485de043efb2f370cd654f4ea44b" [[package]] +name = "wasip2" +version = "1.0.1+wasi-0.2.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0562428422c63773dad2c345a1882263bbf4d65cf3f42e90921f787ef5ad58e7" +dependencies = [ + "wit-bindgen", +] + +[[package]] name = "wasm-bindgen" version = "0.2.105" source = "registry+https://github.com/rust-lang/crates.io-index" @@ -1996,6 +3075,19 @@ dependencies = [ ] [[package]] +name = "wasm-bindgen-futures" +version = "0.4.55" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "551f88106c6d5e7ccc7cd9a16f312dd3b5d36ea8b4954304657d5dfba115d4a0" +dependencies = [ + "cfg-if", + "js-sys", + "once_cell", + "wasm-bindgen", + "web-sys", +] + +[[package]] name = "wasm-bindgen-macro" version = "0.2.105" source = "registry+https://github.com/rust-lang/crates.io-index" @@ -2014,7 +3106,7 @@ dependencies = [ "bumpalo", "proc-macro2", "quote", - "syn", + "syn 2.0.110", "wasm-bindgen-shared", ] @@ -2028,12 +3120,35 @@ dependencies = [ ] [[package]] +name = "wasm-streams" +version = "0.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "15053d8d85c7eccdbefef60f06769760a563c7f0a9d6902a13d35c7800b0ad65" +dependencies = [ + "futures-util", + "js-sys", + "wasm-bindgen", + "wasm-bindgen-futures", + "web-sys", +] + +[[package]] +name = "web-sys" +version = "0.3.82" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3a1f95c0d03a47f4ae1f7a64643a6bb97465d9b740f0fa8f90ea33915c99a9a1" +dependencies = [ + "js-sys", + "wasm-bindgen", +] + +[[package]] name = "winapi-util" version = "0.1.11" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c2a7b1c03c876122aa43f3020e6c3c3ee5c05081c9a00739faf7503aeba10d22" dependencies = [ - "windows-sys", + "windows-sys 0.61.2", ] [[package]] @@ -2057,7 +3172,7 @@ checksum = "053e2e040ab57b9dc951b72c264860db7eb3b0200ba345b4e4c3b14f67855ddf" dependencies = [ "proc-macro2", "quote", - "syn", + "syn 2.0.110", ] [[package]] @@ -2068,7 +3183,7 @@ checksum = "3f316c4a2570ba26bbec722032c4099d8c8bc095efccdc15688708623367e358" dependencies = [ "proc-macro2", "quote", - "syn", + "syn 2.0.110", ] [[package]] @@ -2078,6 +3193,17 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f0805222e57f7521d6a62e36fa9163bc891acd422f971defe97d64e70d0a4fe5" [[package]] +name = "windows-registry" +version = "0.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "02752bf7fbdcce7f2a27a742f798510f3e5ad88dbe84871e5168e2120c3d5720" +dependencies = [ + "windows-link", + "windows-result", + "windows-strings", +] + +[[package]] name = "windows-result" version = "0.4.1" source = "registry+https://github.com/rust-lang/crates.io-index" @@ -2097,6 +3223,24 @@ dependencies = [ [[package]] name = "windows-sys" +version = "0.52.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "282be5f36a8ce781fad8c8ae18fa3f9beff57ec1b52cb3de0789201425d9a33d" +dependencies = [ + "windows-targets 0.52.6", +] + +[[package]] +name = "windows-sys" +version = "0.60.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f2f500e4d28234f72040990ec9d39e3a6b950f9f22d3dba18416c35882612bcb" +dependencies = [ + "windows-targets 0.53.5", +] + +[[package]] +name = "windows-sys" version = "0.61.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ae137229bcbd6cdf0f7b80a31df61766145077ddf49416a728b02cb3921ff3fc" @@ -2105,6 +3249,144 @@ dependencies = [ ] [[package]] +name = "windows-targets" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9b724f72796e036ab90c1021d4780d4d3d648aca59e491e6b98e725b84e99973" +dependencies = [ + "windows_aarch64_gnullvm 0.52.6", + "windows_aarch64_msvc 0.52.6", + "windows_i686_gnu 0.52.6", + "windows_i686_gnullvm 0.52.6", + "windows_i686_msvc 0.52.6", + "windows_x86_64_gnu 0.52.6", + "windows_x86_64_gnullvm 0.52.6", + "windows_x86_64_msvc 0.52.6", +] + +[[package]] +name = "windows-targets" +version = "0.53.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4945f9f551b88e0d65f3db0bc25c33b8acea4d9e41163edf90dcd0b19f9069f3" +dependencies = [ + "windows-link", + "windows_aarch64_gnullvm 0.53.1", + "windows_aarch64_msvc 0.53.1", + "windows_i686_gnu 0.53.1", + "windows_i686_gnullvm 0.53.1", + "windows_i686_msvc 0.53.1", + "windows_x86_64_gnu 0.53.1", + "windows_x86_64_gnullvm 0.53.1", + "windows_x86_64_msvc 0.53.1", +] + +[[package]] +name = "windows_aarch64_gnullvm" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "32a4622180e7a0ec044bb555404c800bc9fd9ec262ec147edd5989ccd0c02cd3" + +[[package]] +name = "windows_aarch64_gnullvm" +version = "0.53.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a9d8416fa8b42f5c947f8482c43e7d89e73a173cead56d044f6a56104a6d1b53" + +[[package]] +name = "windows_aarch64_msvc" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "09ec2a7bb152e2252b53fa7803150007879548bc709c039df7627cabbd05d469" + +[[package]] +name = "windows_aarch64_msvc" +version = "0.53.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b9d782e804c2f632e395708e99a94275910eb9100b2114651e04744e9b125006" + +[[package]] +name = "windows_i686_gnu" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8e9b5ad5ab802e97eb8e295ac6720e509ee4c243f69d781394014ebfe8bbfa0b" + +[[package]] +name = "windows_i686_gnu" +version = "0.53.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "960e6da069d81e09becb0ca57a65220ddff016ff2d6af6a223cf372a506593a3" + +[[package]] +name = "windows_i686_gnullvm" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0eee52d38c090b3caa76c563b86c3a4bd71ef1a819287c19d586d7334ae8ed66" + +[[package]] +name = "windows_i686_gnullvm" +version = "0.53.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fa7359d10048f68ab8b09fa71c3daccfb0e9b559aed648a8f95469c27057180c" + +[[package]] +name = "windows_i686_msvc" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "240948bc05c5e7c6dabba28bf89d89ffce3e303022809e73deaefe4f6ec56c66" + +[[package]] +name = "windows_i686_msvc" +version = "0.53.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1e7ac75179f18232fe9c285163565a57ef8d3c89254a30685b57d83a38d326c2" + +[[package]] +name = "windows_x86_64_gnu" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "147a5c80aabfbf0c7d901cb5895d1de30ef2907eb21fbbab29ca94c5b08b1a78" + +[[package]] +name = "windows_x86_64_gnu" +version = "0.53.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9c3842cdd74a865a8066ab39c8a7a473c0778a3f29370b5fd6b4b9aa7df4a499" + +[[package]] +name = "windows_x86_64_gnullvm" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "24d5b23dc417412679681396f2b49f3de8c1473deb516bd34410872eff51ed0d" + +[[package]] +name = "windows_x86_64_gnullvm" +version = "0.53.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0ffa179e2d07eee8ad8f57493436566c7cc30ac536a3379fdf008f47f6bb7ae1" + +[[package]] +name = "windows_x86_64_msvc" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "589f6da84c646204747d1270a2a5661ea66ed1cced2631d546fdfb155959f9ec" + +[[package]] +name = "windows_x86_64_msvc" +version = "0.53.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d6bbff5f0aada427a1e5a6da5f1f98158182f26556f345ac9e04d36d0ebed650" + +[[package]] +name = "winnow" +version = "0.5.40" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f593a95398737aeed53e489c785df13f3618e41dbcd6718c6addbf1395aa6876" +dependencies = [ + "memchr", +] + +[[package]] name = "winnow" version = "0.7.13" source = "registry+https://github.com/rust-lang/crates.io-index" @@ -2114,6 +3396,41 @@ dependencies = [ ] [[package]] +name = "wit-bindgen" +version = "0.46.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f17a85883d4e6d00e8a97c586de764dabcc06133f7f1d55dce5cdc070ad7fe59" + +[[package]] +name = "writeable" +version = "0.6.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9edde0db4769d2dc68579893f2306b26c6ecfbe0ef499b013d731b7b9247e0b9" + +[[package]] +name = "yoke" +version = "0.8.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "72d6e5c6afb84d73944e5cedb052c4680d5657337201555f9f2a16b7406d4954" +dependencies = [ + "stable_deref_trait", + "yoke-derive", + "zerofrom", +] + +[[package]] +name = "yoke-derive" +version = "0.8.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b659052874eb698efe5b9e8cf382204678a0086ebf46982b79d6ca3182927e5d" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.110", + "synstructure", +] + +[[package]] name = "zerocopy" version = "0.8.28" source = "registry+https://github.com/rust-lang/crates.io-index" @@ -2130,7 +3447,67 @@ checksum = "c640b22cd9817fae95be82f0d2f90b11f7605f6c319d16705c459b27ac2cbc26" dependencies = [ "proc-macro2", "quote", - "syn", + "syn 2.0.110", +] + +[[package]] +name = "zerofrom" +version = "0.1.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "50cc42e0333e05660c3587f3bf9d0478688e15d870fab3346451ce7f8c9fbea5" +dependencies = [ + "zerofrom-derive", +] + +[[package]] +name = "zerofrom-derive" +version = "0.1.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d71e5d6e06ab090c67b5e44993ec16b72dcbaabc526db883a360057678b48502" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.110", + "synstructure", +] + +[[package]] +name = "zeroize" +version = "1.8.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b97154e67e32c85465826e8bcc1c59429aaaf107c1e4a9e53c8d8ccd5eff88d0" + +[[package]] +name = "zerotrie" +version = "0.2.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2a59c17a5562d507e4b54960e8569ebee33bee890c70aa3fe7b97e85a9fd7851" +dependencies = [ + "displaydoc", + "yoke", + "zerofrom", +] + +[[package]] +name = "zerovec" +version = "0.11.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6c28719294829477f525be0186d13efa9a3c602f7ec202ca9e353d310fb9a002" +dependencies = [ + "yoke", + "zerofrom", + "zerovec-derive", +] + +[[package]] +name = "zerovec-derive" +version = "0.11.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "eadce39539ca5cb3985590102671f2567e659fca9666581ad3411d59207951f3" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.110", ] [[package]] @@ -2141,7 +3518,7 @@ checksum = "2be61892e4f2b1772727be11630a62664a1826b62efa43a6fe7449521cb8744c" dependencies = [ "endi", "serde", - "winnow", + "winnow 0.7.13", "zvariant_derive", "zvariant_utils", ] @@ -2152,10 +3529,10 @@ version = "5.8.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "da58575a1b2b20766513b1ec59d8e2e68db2745379f961f86650655e862d2006" dependencies = [ - "proc-macro-crate", + "proc-macro-crate 3.4.0", "proc-macro2", "quote", - "syn", + "syn 2.0.110", "zvariant_utils", ] @@ -2168,6 +3545,6 @@ dependencies = [ "proc-macro2", "quote", "serde", - "syn", - "winnow", + "syn 2.0.110", + "winnow 0.7.13", ] diff --git a/Cargo.toml b/Cargo.toml index 13ff3a6..5f7af0f 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -23,6 +23,8 @@ isolang = { git = "https://github.com/humenda/isolang-rs" } serde = { version = "1.0.228", features = ["derive"] } serde_json = "1.0.145" relm4-icons = "0.10" +deepl = "0.7.3" +gsettings-macro = "0.2.2" [target.'cfg(target_os = "macos")'.dependencies] cocoa = "0.26" diff --git a/resources/style.css b/data/style.css index 44106e1..03dc022 100644 --- a/resources/style.css +++ b/data/style.css @@ -5,6 +5,10 @@ border-radius: 12px; } +.cue-view:disabled { + opacity: 0; +} + .cue-view link { color: @theme_fg_color; text-decoration: none; diff --git a/flake.nix b/flake.nix index af7d9f5..5469f03 100644 --- a/flake.nix +++ b/flake.nix @@ -52,6 +52,7 @@ ]; buildInputs = with pkgs; [ + openssl gtk4 libadwaita ffmpeg_8-full.dev diff --git a/src/app.rs b/src/app.rs index 951392e..bdb2ef9 100644 --- a/src/app.rs +++ b/src/app.rs @@ -5,33 +5,36 @@ use crate::{ icon_names, open_dialog::{OpenDialog, OpenDialogMsg, OpenDialogOutput}, player::{Player, PlayerMsg, PlayerOutput}, - preferences::{Preferences, PreferencesMsg}, - subtitle_extraction::{SubtitleExtractor, SubtitleExtractorMsg, SubtitleExtractorOutput}, + preferences_dialog::{PreferencesDialog, PreferencesDialogMsg}, subtitle_selection_dialog::{ SubtitleSelectionDialog, SubtitleSelectionDialogMsg, SubtitleSelectionDialogOutput, + SubtitleSettings, }, subtitle_view::{SubtitleView, SubtitleViewMsg, SubtitleViewOutput}, - tracks::{SUBTITLE_TRACKS, StreamIndex, SubtitleCue}, + subtitles::{ + MetadataCollection, SUBTITLE_TRACKS, StreamIndex, SubtitleCue, SubtitleTrack, + extraction::{SubtitleExtractor, SubtitleExtractorMsg, SubtitleExtractorOutput}, + state::SubtitleState, + }, transcript::{Transcript, TranscriptMsg, TranscriptOutput}, + translation::{DeeplTranslator, deepl::DeeplTranslatorMsg}, util::Tracker, }; pub struct App { + root: adw::ApplicationWindow, transcript: Controller<Transcript>, player: Controller<Player>, subtitle_view: Controller<SubtitleView>, extractor: WorkerController<SubtitleExtractor>, + deepl_translator: AsyncController<DeeplTranslator>, - preferences: Controller<Preferences>, + preferences: Controller<PreferencesDialog>, open_url_dialog: Controller<OpenDialog>, - subtitle_selection_dialog: Controller<SubtitleSelectionDialog>, + subtitle_selection_dialog: Option<Controller<SubtitleSelectionDialog>>, - primary_stream_ix: Option<StreamIndex>, - primary_cue: Tracker<Option<String>>, - primary_last_cue_ix: Tracker<Option<usize>>, - secondary_cue: Tracker<Option<String>>, - secondary_stream_ix: Option<StreamIndex>, - secondary_last_cue_ix: Tracker<Option<usize>>, + primary_subtitle_state: SubtitleState, + secondary_subtitle_state: SubtitleState, // for auto-pausing autopaused: bool, @@ -40,10 +43,9 @@ pub struct App { #[derive(Debug)] pub enum AppMsg { - NewCue(StreamIndex, SubtitleCue), + AddCue(StreamIndex, SubtitleCue), SubtitleExtractionComplete, - PrimarySubtitleTrackSelected(Option<StreamIndex>), - SecondarySubtitleTrackSelected(Option<StreamIndex>), + ApplySubtitleSettings(SubtitleSettings), PositionUpdate(gst::ClockTime), SetHoveringSubtitleCue(bool), ShowUrlOpenDialog, @@ -51,6 +53,7 @@ pub enum AppMsg { ShowSubtitleSelectionDialog, Play { url: String, + metadata: MetadataCollection, whisper_stream_index: Option<StreamIndex>, }, } @@ -123,52 +126,46 @@ impl SimpleComponent for App { sender.input_sender(), |output| match output { SubtitleExtractorOutput::NewCue(stream_index, cue) => { - AppMsg::NewCue(stream_index, cue) + AppMsg::AddCue(stream_index, cue) } SubtitleExtractorOutput::ExtractionComplete => AppMsg::SubtitleExtractionComplete, }, ); - let preferences = Preferences::builder().launch(root.clone().into()).detach(); + let deepl_translator = DeeplTranslator::builder().launch(()).detach(); + + let preferences = PreferencesDialog::builder() + .launch(root.clone().into()) + .detach(); let open_url_dialog = OpenDialog::builder().launch(root.clone().into()).forward( sender.input_sender(), |output| match output { OpenDialogOutput::Play { url, + metadata, whisper_stream_index, } => AppMsg::Play { url, + metadata, whisper_stream_index, }, }, ); - let subtitle_selection_dialog = SubtitleSelectionDialog::builder() - .launch(root.clone().into()) - .forward(sender.input_sender(), |output| match output { - SubtitleSelectionDialogOutput::PrimaryTrackSelected(ix) => { - AppMsg::PrimarySubtitleTrackSelected(ix) - } - SubtitleSelectionDialogOutput::SecondaryTrackSelected(ix) => { - AppMsg::SecondarySubtitleTrackSelected(ix) - } - }); let model = Self { + root: root.clone(), player, transcript, subtitle_view, extractor, + deepl_translator, preferences, open_url_dialog, - subtitle_selection_dialog, + subtitle_selection_dialog: None, - primary_stream_ix: None, - primary_cue: Tracker::new(None), - primary_last_cue_ix: Tracker::new(None), - secondary_stream_ix: None, - secondary_cue: Tracker::new(None), - secondary_last_cue_ix: Tracker::new(None), + primary_subtitle_state: SubtitleState::default(), + secondary_subtitle_state: SubtitleState::default(), autopaused: false, hovering_primary_cue: false, @@ -179,94 +176,45 @@ impl SimpleComponent for App { ComponentParts { model, widgets } } - fn update(&mut self, message: Self::Input, _sender: ComponentSender<Self>) { + fn update(&mut self, message: Self::Input, sender: ComponentSender<Self>) { match message { - AppMsg::NewCue(stream_index, cue) => { + AppMsg::AddCue(stream_ix, cue) => { + SUBTITLE_TRACKS + .write() + .get_mut(&stream_ix) + .unwrap() + .push_cue(cue.clone()); + self.transcript .sender() - .send(TranscriptMsg::NewCue(stream_index, cue)) + .send(TranscriptMsg::NewCue(stream_ix, cue)) .unwrap(); } AppMsg::SubtitleExtractionComplete => { log::info!("Subtitle extraction complete"); } - AppMsg::PrimarySubtitleTrackSelected(stream_index) => { - self.primary_stream_ix = stream_index; + AppMsg::ApplySubtitleSettings(settings) => { + self.primary_subtitle_state + .set_stream_ix(settings.primary_track_ix); + self.secondary_subtitle_state + .set_stream_ix(settings.secondary_track_ix); self.transcript .sender() - .send(TranscriptMsg::SelectTrack(stream_index)) + .send(TranscriptMsg::SelectTrack(settings.primary_track_ix)) + .unwrap(); + self.deepl_translator + .sender() + .send(DeeplTranslatorMsg::SelectTrack(settings.primary_track_ix)) .unwrap(); - } - AppMsg::SecondarySubtitleTrackSelected(stream_index) => { - self.secondary_stream_ix = stream_index; - } - AppMsg::PositionUpdate(pos) => { - if let Some(stream_ix) = self.primary_stream_ix { - // sometimes we get a few position update messages after - // auto-pausing; this prevents us from immediately un-autopausing - // again - if self.autopaused { - return; - } - - let cue_was_some = self.primary_cue.get().is_some(); - - Self::update_cue( - stream_ix, - pos, - &mut self.primary_cue, - &mut self.primary_last_cue_ix, - ); - - if self.primary_cue.is_dirty() { - // last cue just ended -> auto-pause - if cue_was_some && self.hovering_primary_cue { - self.player.sender().send(PlayerMsg::Pause).unwrap(); - self.autopaused = true; - return; - } - - self.subtitle_view - .sender() - .send(SubtitleViewMsg::SetPrimaryCue( - self.primary_cue.get().clone(), - )) - .unwrap(); - - self.primary_cue.reset(); - } - - if self.primary_last_cue_ix.is_dirty() { - if let Some(ix) = self.primary_last_cue_ix.get() { - self.transcript - .sender() - .send(TranscriptMsg::ScrollToCue(*ix)) - .unwrap(); - } - self.primary_last_cue_ix.reset(); - } - } - if let Some(stream_ix) = self.secondary_stream_ix { - Self::update_cue( - stream_ix, - pos, - &mut self.secondary_cue, - &mut self.secondary_last_cue_ix, - ); - - if !self.autopaused && self.secondary_cue.is_dirty() { - self.subtitle_view - .sender() - .send(SubtitleViewMsg::SetSecondaryCue( - self.secondary_cue.get().clone(), - )) - .unwrap(); - - self.secondary_cue.reset(); - } - } + self.subtitle_view + .sender() + .send(SubtitleViewMsg::ApplySubtitleSettings(settings)) + .unwrap(); + } + AppMsg::PositionUpdate(position) => { + self.update_subtitle_states(position); } AppMsg::SetHoveringSubtitleCue(hovering) => { self.hovering_primary_cue = hovering; @@ -284,17 +232,20 @@ impl SimpleComponent for App { AppMsg::ShowPreferences => { self.preferences .sender() - .send(PreferencesMsg::Show) + .send(PreferencesDialogMsg::Show) .unwrap(); } AppMsg::ShowSubtitleSelectionDialog => { - self.subtitle_selection_dialog - .sender() - .send(SubtitleSelectionDialogMsg::Show) - .unwrap(); + if let Some(ref dialog) = self.subtitle_selection_dialog { + dialog + .sender() + .send(SubtitleSelectionDialogMsg::Show) + .unwrap(); + } } AppMsg::Play { url, + metadata, whisper_stream_index, } => { self.player @@ -308,70 +259,128 @@ impl SimpleComponent for App { whisper_stream_index, }) .unwrap(); + + let subtitle_selection_dialog = SubtitleSelectionDialog::builder() + .launch((self.root.clone().into(), metadata)) + .forward(sender.input_sender(), |output| match output { + SubtitleSelectionDialogOutput::ApplySubtitleSettings(settings) => { + AppMsg::ApplySubtitleSettings(settings) + } + }); + self.subtitle_selection_dialog = Some(subtitle_selection_dialog); } } } } impl App { - fn update_cue( - stream_ix: StreamIndex, - position: gst::ClockTime, - cue: &mut Tracker<Option<String>>, - last_cue_ix: &mut Tracker<Option<usize>>, - ) { - let lock = SUBTITLE_TRACKS.read(); - let track = lock.get(&stream_ix).unwrap(); + fn update_subtitle_states(&mut self, position: gst::ClockTime) { + self.update_primary_subtitle_state(position); + self.update_secondary_subtitle_state(position); + } - // try to find current cue quickly (should usually succeed during playback) - if let Some(ix) = last_cue_ix.get() { - let last_cue = track.cues.get(*ix).unwrap(); - if last_cue.start <= position && position <= last_cue.end { - // still at current cue - return; - } else if let Some(next_cue) = track.cues.get(ix + 1) { - if last_cue.end < position && position < next_cue.start { - // strictly between cues - cue.set(None); - return; - } - if next_cue.start <= position && position <= next_cue.end { - // already in next cue (this happens when one cue immediately - // follows the previous one) - cue.set(Some(next_cue.text.clone())); - last_cue_ix.set(Some(ix + 1)); - return; - } + fn update_primary_subtitle_state(&mut self, position: gst::ClockTime) { + // sometimes we get a few position update messages after + // auto-pausing + if self.autopaused { + return; + } + + update_subtitle_state(&mut self.primary_subtitle_state, position); + + // last cue just ended -> auto-pause + if self.primary_subtitle_state.last_ended_cue_ix.is_dirty() && self.hovering_primary_cue { + self.player.sender().send(PlayerMsg::Pause).unwrap(); + self.autopaused = true; + return; + } + + if self.primary_subtitle_state.is_dirty() { + let cue = self.primary_subtitle_state.active_cue(); + + self.subtitle_view + .sender() + .send(SubtitleViewMsg::SetPrimaryCue(cue)) + .unwrap(); + } + + if self.primary_subtitle_state.last_started_cue_ix.is_dirty() { + if let Some(ix) = *self.primary_subtitle_state.last_started_cue_ix { + self.transcript + .sender() + .send(TranscriptMsg::ScrollToCue(ix)) + .unwrap(); } } - // if we are before the first subtitle, no need to look further - if track.cues.is_empty() || position < track.cues.first().unwrap().start { - cue.set(None); - last_cue_ix.set(None); + self.primary_subtitle_state.reset(); + } + + fn update_secondary_subtitle_state(&mut self, position: gst::ClockTime) { + // sometimes we get a few position update messages after + // auto-pausing + if self.autopaused { return; } - // otherwise, search the whole track (e.g. after seeking) - match track - .cues - .iter() - .enumerate() - .rev() - .find(|(_ix, cue)| cue.start <= position) - { - Some((ix, new_cue)) => { - last_cue_ix.set(Some(ix)); - if position <= new_cue.end { - cue.set(Some(new_cue.text.clone())); - } else { - cue.set(None); - } + update_subtitle_state(&mut self.secondary_subtitle_state, position); + + if self.secondary_subtitle_state.is_dirty() { + let cue = self.secondary_subtitle_state.active_cue(); + + self.subtitle_view + .sender() + .send(SubtitleViewMsg::SetSecondaryCue(cue)) + .unwrap(); + } + + self.secondary_subtitle_state.reset(); + } +} + +fn update_subtitle_state(state: &mut SubtitleState, position: gst::ClockTime) { + if let Some(stream_ix) = state.stream_ix { + let lock = SUBTITLE_TRACKS.read(); + let track = lock.get(&stream_ix).unwrap(); + + update_last_time_ix(&track.start_times, &mut state.last_started_cue_ix, position); + update_last_time_ix(&track.end_times, &mut state.last_ended_cue_ix, position); + } +} + +fn update_last_time_ix( + times: &Vec<gst::ClockTime>, + last_time_ix: &mut Tracker<Option<usize>>, + current_time: gst::ClockTime, +) { + // try to find index quickly (should succeed during normal playback) + if let Some(ix) = last_time_ix.get() { + let t0 = times.get(*ix).unwrap(); + match (times.get(ix + 1), times.get(ix + 2)) { + (None, _) if current_time >= *t0 => { + return; } - None => { - cue.set(None); - last_cue_ix.set(None); + (Some(t1), _) if current_time >= *t0 && current_time < *t1 => { + return; } - }; + (Some(t1), None) if current_time >= *t1 => { + last_time_ix.set(Some(ix + 1)); + return; + } + (Some(t1), Some(t2)) if current_time >= *t1 && current_time < *t2 => { + last_time_ix.set(Some(ix + 1)); + return; + } + _ => {} + } + } + + // if we are before the first timestamp, no need to look further + if times.is_empty() || current_time < *times.first().unwrap() { + last_time_ix.set_if_ne(None); + return; } + + // otherwise, search the whole array (e.g. after seeking) + last_time_ix.set(times.iter().rposition(|time| *time <= current_time)); } diff --git a/src/cue_view.rs b/src/cue_view.rs index fbf2520..05c45c4 100644 --- a/src/cue_view.rs +++ b/src/cue_view.rs @@ -8,18 +8,25 @@ use relm4::prelude::*; use relm4::{ComponentParts, SimpleComponent}; use unicode_segmentation::UnicodeSegmentation; +use crate::subtitles::state::CueAddress; +use crate::translation::TRANSLATIONS; use crate::util::Tracker; -pub struct CueView { - text: Tracker<Option<String>>, +pub struct ActiveCueViewState { + addr: CueAddress, + text: String, // byte ranges for the words in `text` word_ranges: Vec<Range<usize>>, } +pub struct CueView { + state: Tracker<Option<ActiveCueViewState>>, +} + #[derive(Debug)] pub enum CueViewMsg { // messages from the app - SetText(Option<String>), + SetCue(Option<CueAddress>), // messages from UI MouseMotion, } @@ -42,7 +49,7 @@ impl SimpleComponent for CueView { gtk::Label { add_controller: event_controller.clone(), set_use_markup: true, - set_visible: false, + set_sensitive: false, set_justify: gtk::Justification::Center, add_css_class: "cue-view", }, @@ -71,8 +78,7 @@ impl SimpleComponent for CueView { sender: relm4::ComponentSender<Self>, ) -> relm4::ComponentParts<Self> { let model = Self { - text: Tracker::new(None), - word_ranges: Vec::new(), + state: Tracker::new(None), }; let widgets = view_output!(); @@ -81,19 +87,26 @@ impl SimpleComponent for CueView { } fn update(&mut self, message: Self::Input, _sender: relm4::ComponentSender<Self>) { - match message { - CueViewMsg::SetText(text) => { - self.text.set(text); + self.state.reset(); - if let Some(text) = self.text.get() { - self.word_ranges = UnicodeSegmentation::unicode_word_indices(text.as_str()) + match message { + CueViewMsg::SetCue(addr) => { + if let Some(addr) = addr { + let text = addr.resolve_text(); + let word_ranges = UnicodeSegmentation::unicode_word_indices(text.as_str()) .map(|(offset, slice)| Range { start: offset, end: offset + slice.len(), }) .collect(); + + self.state.set(Some(ActiveCueViewState { + addr, + text, + word_ranges, + })) } else { - self.word_ranges = Vec::new(); + self.state.set(None); } } CueViewMsg::MouseMotion => { @@ -103,11 +116,16 @@ impl SimpleComponent for CueView { } fn post_view() { - if self.text.is_dirty() { - if let Some(text) = self.text.get() { + if self.state.is_dirty() { + if let Some(ActiveCueViewState { + addr: _, + text, + word_ranges, + }) = self.state.get() + { let mut markup = String::new(); - let mut it = self.word_ranges.iter().enumerate().peekable(); + let mut it = word_ranges.iter().enumerate().peekable(); if let Some((_, first_word_range)) = it.peek() { markup.push_str( glib::markup_escape_text(&text[..first_word_range.start]).as_str(), @@ -127,24 +145,35 @@ impl SimpleComponent for CueView { markup.push_str(glib::markup_escape_text(&text[next_gap_range]).as_str()); } - widgets.label.set_markup(markup.as_str()); - widgets.label.set_visible(true); + widgets.label.set_markup(&markup); + widgets.label.set_sensitive(true); } else { - widgets.label.set_visible(false); + // insensitive = invisible by css + widgets.label.set_sensitive(false); } } - if let Some(word_ix_str) = widgets.label.current_uri() { - let range = self - .word_ranges - .get(usize::from_str(word_ix_str.as_str()).unwrap()) - .unwrap(); - widgets - .popover_label - .set_text(&self.text.get().as_ref().unwrap()[range.clone()]); + if let ( + Some(ActiveCueViewState { + addr: CueAddress(stream_ix, cue_ix), + text: _, + word_ranges, + }), + Some(word_ix_str), + ) = (self.state.get(), widgets.label.current_uri()) + { + let word_ix = usize::from_str(word_ix_str.as_str()).unwrap(); + + { + // TODO get translation + widgets.popover_label.set_text(word_ix_str.as_str()); + } + + let range = word_ranges.get(word_ix).unwrap(); widgets .popover .set_pointing_to(Some(&Self::get_rect_of_byte_range(&widgets.label, &range))); + widgets.popover.popup(); } else { widgets.popover.popdown(); diff --git a/src/main.rs b/src/main.rs index f010c6a..69ccb38 100644 --- a/src/main.rs +++ b/src/main.rs @@ -2,13 +2,14 @@ mod app; mod cue_view; mod open_dialog; mod player; -mod preferences; -mod subtitle_extraction; +mod preferences_dialog; +mod settings; mod subtitle_selection_dialog; mod subtitle_view; +mod subtitles; mod track_selector; -mod tracks; mod transcript; +mod translation; mod util; use gtk::{CssProvider, STYLE_PROVIDER_PRIORITY_APPLICATION, gdk, glib}; @@ -38,7 +39,7 @@ fn main() { let css_provider = CssProvider::new(); css_provider.load_from_bytes(&glib::Bytes::from_static(include_bytes!( - "../resources/style.css" + "../data/style.css" ))); gtk::style_context_add_provider_for_display( &gdk::Display::default().unwrap(), diff --git a/src/open_dialog.rs b/src/open_dialog.rs index 2f17c59..3b822be 100644 --- a/src/open_dialog.rs +++ b/src/open_dialog.rs @@ -5,10 +5,10 @@ use gtk::gio; use gtk::glib::clone; use relm4::prelude::*; +use crate::subtitles::{MetadataCollection, StreamIndex, TrackMetadata}; use crate::track_selector::{ TrackInfo, TrackSelector, TrackSelectorInit, TrackSelectorMsg, TrackSelectorOutput, }; -use crate::tracks::{StreamIndex, TrackMetadata}; use crate::util::Tracker; pub struct OpenDialog { @@ -23,6 +23,7 @@ pub struct OpenDialog { whisper_stream_index: Option<StreamIndex>, metadata_command_running: bool, + metadata: Option<MetadataCollection>, } #[derive(Debug)] @@ -34,7 +35,7 @@ pub enum OpenDialogMsg { FileSelected(gio::File), UrlChanged(String), SetDoWhisperExtraction(bool), - WhisperTrackSelected(Option<StreamIndex>), + WhisperTrackSelected(StreamIndex), Play, } @@ -42,6 +43,7 @@ pub enum OpenDialogMsg { pub enum OpenDialogOutput { Play { url: String, + metadata: MetadataCollection, whisper_stream_index: Option<StreamIndex>, }, } @@ -51,7 +53,7 @@ impl Component for OpenDialog { type Init = adw::ApplicationWindow; type Input = OpenDialogMsg; type Output = OpenDialogOutput; - type CommandOutput = Result<BTreeMap<StreamIndex, TrackMetadata>, ffmpeg::Error>; + type CommandOutput = Result<MetadataCollection, ffmpeg::Error>; view! { #[root] @@ -186,6 +188,7 @@ impl Component for OpenDialog { whisper_stream_index: None, metadata_command_running: false, + metadata: None, }; let widgets = view_output!(); @@ -227,23 +230,28 @@ impl Component for OpenDialog { self.url.set(file.uri().into()); } OpenDialogMsg::Play => { - sender - .output(OpenDialogOutput::Play { - url: self.url.get().clone(), - whisper_stream_index: if self.do_whisper_extraction { - self.whisper_stream_index - } else { - None - }, - }) - .unwrap(); - self.dialog.close(); + if let Some(ref metadata) = self.metadata { + sender + .output(OpenDialogOutput::Play { + url: self.url.get().clone(), + metadata: metadata.clone(), + whisper_stream_index: if self.do_whisper_extraction { + self.whisper_stream_index + } else { + None + }, + }) + .unwrap(); + self.dialog.close(); + } else { + log::error!("metadata is unavailable, can't play"); + } } OpenDialogMsg::SetDoWhisperExtraction(val) => { self.do_whisper_extraction = val; } OpenDialogMsg::WhisperTrackSelected(track_index) => { - self.whisper_stream_index = track_index; + self.whisper_stream_index = Some(track_index); } } } @@ -259,10 +267,10 @@ impl Component for OpenDialog { self.metadata_command_running = false; match message { - Ok(audio_tracks) => { + Ok(metadata) => { let list_model = gio::ListStore::new::<TrackInfo>(); - for (&stream_index, track) in audio_tracks.iter() { + for (&stream_index, track) in metadata.audio.iter() { let track_info = TrackInfo::new( stream_index, track.language.map(|lang| lang.to_name()), @@ -276,6 +284,8 @@ impl Component for OpenDialog { .send(TrackSelectorMsg::SetListModel(list_model)) .unwrap(); + self.metadata = Some(metadata); + self.next(); } Err(e) => { @@ -302,7 +312,7 @@ impl OpenDialog { sender.spawn_oneshot_command(move || { let input = ffmpeg::format::input(&url)?; - let audio_tracks = input + let audio = input .streams() .filter_map(|stream| { if stream.parameters().medium() == ffmpeg::media::Type::Audio { @@ -312,8 +322,18 @@ impl OpenDialog { } }) .collect::<BTreeMap<_, _>>(); + let subtitles = input + .streams() + .filter_map(|stream| { + if stream.parameters().medium() == ffmpeg::media::Type::Subtitle { + Some((stream.index(), TrackMetadata::from_ffmpeg_stream(&stream))) + } else { + None + } + }) + .collect::<BTreeMap<_, _>>(); - Ok(audio_tracks) + Ok(MetadataCollection { audio, subtitles }) }); self.metadata_command_running = true; diff --git a/src/preferences.rs b/src/preferences_dialog.rs index c5f9bb1..5aacfe8 100644 --- a/src/preferences.rs +++ b/src/preferences_dialog.rs @@ -1,25 +1,23 @@ use adw::prelude::*; -use gtk::gio; use relm4::prelude::*; -pub struct Preferences { +use crate::settings::Settings; + +pub struct PreferencesDialog { parent_window: adw::ApplicationWindow, dialog: adw::PreferencesDialog, } #[derive(Debug)] -pub enum PreferencesMsg { +pub enum PreferencesDialogMsg { Show, } -#[derive(Debug)] -pub enum PreferencesOutput {} - #[relm4::component(pub)] -impl SimpleComponent for Preferences { +impl SimpleComponent for PreferencesDialog { type Init = adw::ApplicationWindow; - type Input = PreferencesMsg; - type Output = PreferencesOutput; + type Input = PreferencesDialogMsg; + type Output = (); view! { #[root] @@ -33,12 +31,9 @@ impl SimpleComponent for Preferences { adw::PreferencesGroup { set_title: "Machine Translation", + #[name(deepl_api_key_row)] adw::EntryRow { set_title: "DeepL API key", - set_text: settings.string("deepl-api-key").as_str(), - connect_changed[settings] => move |entry| { - settings.set_string("deepl-api-key", entry.text().as_str()).unwrap() - } }, } } @@ -49,7 +44,7 @@ impl SimpleComponent for Preferences { root: Self::Root, _sender: ComponentSender<Self>, ) -> ComponentParts<Self> { - let settings = gio::Settings::new("tc.mal.lleap"); + let settings = Settings::default(); let model = Self { parent_window, @@ -58,12 +53,16 @@ impl SimpleComponent for Preferences { let widgets = view_output!(); + settings + .bind_deepl_api_key(&widgets.deepl_api_key_row, "text") + .build(); + ComponentParts { model, widgets } } fn update(&mut self, msg: Self::Input, _sender: ComponentSender<Self>) { match msg { - PreferencesMsg::Show => { + PreferencesDialogMsg::Show => { self.dialog.present(Some(&self.parent_window)); } } diff --git a/src/settings.rs b/src/settings.rs new file mode 100644 index 0000000..eb1f6b9 --- /dev/null +++ b/src/settings.rs @@ -0,0 +1,5 @@ +use gsettings_macro::gen_settings; +use gtk::{gio, glib}; + +#[gen_settings(file = "./data/tc.mal.lleap.gschema.xml", id = "tc.mal.lleap")] +pub struct Settings; diff --git a/src/subtitle_selection_dialog.rs b/src/subtitle_selection_dialog.rs index 6136d56..8e5d283 100644 --- a/src/subtitle_selection_dialog.rs +++ b/src/subtitle_selection_dialog.rs @@ -2,37 +2,47 @@ use adw::prelude::*; use gtk::gio; use relm4::prelude::*; +use crate::subtitles::{MetadataCollection, StreamIndex}; use crate::track_selector::{ TrackInfo, TrackSelector, TrackSelectorInit, TrackSelectorMsg, TrackSelectorOutput, }; -use crate::tracks::{SUBTITLE_TRACKS, StreamIndex}; + +#[derive(Clone, Copy, Default, Debug)] +pub struct SubtitleSettings { + pub primary_track_ix: Option<StreamIndex>, + pub secondary_track_ix: Option<StreamIndex>, + pub show_secondary: bool, + pub show_machine_translation: bool, +} pub struct SubtitleSelectionDialog { parent_window: adw::ApplicationWindow, dialog: adw::PreferencesDialog, - track_list_model: gio::ListStore, primary_selector: Controller<TrackSelector>, secondary_selector: Controller<TrackSelector>, - primary_track_ix: Option<StreamIndex>, - secondary_track_ix: Option<StreamIndex>, + + settings: SubtitleSettings, } #[derive(Debug)] pub enum SubtitleSelectionDialogMsg { Show, - PrimaryTrackChanged(Option<StreamIndex>), - SecondaryTrackChanged(Option<StreamIndex>), + Close, + // ui messages + PrimaryTrackChanged(StreamIndex), + SecondaryTrackChanged(StreamIndex), + ShowSecondaryChanged(bool), + ShowMachineTranslationChanged(bool), } #[derive(Debug)] pub enum SubtitleSelectionDialogOutput { - PrimaryTrackSelected(Option<StreamIndex>), - SecondaryTrackSelected(Option<StreamIndex>), + ApplySubtitleSettings(SubtitleSettings), } #[relm4::component(pub)] impl SimpleComponent for SubtitleSelectionDialog { - type Init = adw::ApplicationWindow; + type Init = (adw::ApplicationWindow, MetadataCollection); type Input = SubtitleSelectionDialogMsg; type Output = SubtitleSelectionDialogOutput; @@ -41,22 +51,50 @@ impl SimpleComponent for SubtitleSelectionDialog { adw::PreferencesDialog { set_title: "Subtitle Settings", add: &page, + connect_closed => SubtitleSelectionDialogMsg::Close, }, #[name(page)] adw::PreferencesPage { adw::PreferencesGroup { model.primary_selector.widget(), - model.secondary_selector.widget(), + + adw::ExpanderRow { + set_title: "Show secondary subtitles", + set_subtitle: "Enable this if there exist subtitles a language you already know", + set_show_enable_switch: true, + #[watch] + set_enable_expansion: model.settings.show_secondary, + connect_enable_expansion_notify[sender] => move |expander_row| { + sender.input(SubtitleSelectionDialogMsg::ShowSecondaryChanged(expander_row.enables_expansion())) + }, + + add_row: model.secondary_selector.widget(), + }, + + adw::ExpanderRow { + set_title: "Show machine translations", + set_subtitle: "This is useful in case there are no subtitles in your native language or you prefer a more direct translation of the primary subtitles", + set_show_enable_switch: true, + #[watch] + set_enable_expansion: model.settings.show_machine_translation, + connect_enable_expansion_notify[sender] => move |expander_row| { + sender.input(SubtitleSelectionDialogMsg::ShowMachineTranslationChanged(expander_row.enables_expansion())) + }, + + // TODO add row for language selection + }, } }, } fn init( - parent_window: Self::Init, + init: Self::Init, root: Self::Root, sender: ComponentSender<Self>, ) -> ComponentParts<Self> { + let (parent_window, metadata) = init; + let primary_selector = TrackSelector::builder() .launch(TrackSelectorInit { title: "Primary subtitle track", @@ -81,73 +119,59 @@ impl SimpleComponent for SubtitleSelectionDialog { let model = Self { parent_window, dialog: root.clone(), - track_list_model: gio::ListStore::new::<TrackInfo>(), primary_selector, secondary_selector, - primary_track_ix: None, - secondary_track_ix: None, + settings: Default::default(), }; let widgets = view_output!(); + let track_list_model = gio::ListStore::new::<TrackInfo>(); + for (&stream_index, track_metadata) in metadata.subtitles.iter() { + let track_info = TrackInfo::new( + stream_index, + track_metadata.language.map(|lang| lang.to_name()), + track_metadata.title.clone(), + ); + track_list_model.append(&track_info); + } + + model + .primary_selector + .sender() + .send(TrackSelectorMsg::SetListModel(track_list_model.clone())) + .unwrap(); + model + .secondary_selector + .sender() + .send(TrackSelectorMsg::SetListModel(track_list_model.clone())) + .unwrap(); + ComponentParts { model, widgets } } fn update(&mut self, msg: Self::Input, sender: ComponentSender<Self>) { match msg { SubtitleSelectionDialogMsg::Show => { - self.update_track_list_model(); - - self.primary_selector - .sender() - .send(TrackSelectorMsg::SetListModel( - self.track_list_model.clone(), - )) - .unwrap(); - self.secondary_selector - .sender() - .send(TrackSelectorMsg::SetListModel( - self.track_list_model.clone(), - )) - .unwrap(); - self.dialog.present(Some(&self.parent_window)); } - SubtitleSelectionDialogMsg::PrimaryTrackChanged(stream_index) => { - self.primary_track_ix = stream_index; - sender - .output(SubtitleSelectionDialogOutput::PrimaryTrackSelected( - stream_index, - )) - .unwrap(); + SubtitleSelectionDialogMsg::Close => sender + .output(SubtitleSelectionDialogOutput::ApplySubtitleSettings( + self.settings, + )) + .unwrap(), + SubtitleSelectionDialogMsg::PrimaryTrackChanged(stream_ix) => { + self.settings.primary_track_ix = Some(stream_ix); } - SubtitleSelectionDialogMsg::SecondaryTrackChanged(stream_index) => { - self.secondary_track_ix = stream_index; - sender - .output(SubtitleSelectionDialogOutput::SecondaryTrackSelected( - stream_index, - )) - .unwrap(); + SubtitleSelectionDialogMsg::SecondaryTrackChanged(stream_ix) => { + self.settings.secondary_track_ix = Some(stream_ix); + } + SubtitleSelectionDialogMsg::ShowSecondaryChanged(val) => { + self.settings.show_secondary = val; + } + SubtitleSelectionDialogMsg::ShowMachineTranslationChanged(val) => { + self.settings.show_machine_translation = val; } - } - } -} - -impl SubtitleSelectionDialog { - fn update_track_list_model(&mut self) { - let tracks = SUBTITLE_TRACKS.read(); - - // Clear previous entries - self.track_list_model.remove_all(); - - // Add all available tracks - for (&stream_index, track) in tracks.iter() { - let track_info = TrackInfo::new( - stream_index, - track.metadata.language.map(|lang| lang.to_name()), - track.metadata.title.clone(), - ); - self.track_list_model.append(&track_info); } } } diff --git a/src/subtitle_view.rs b/src/subtitle_view.rs index fd98c60..4de73dd 100644 --- a/src/subtitle_view.rs +++ b/src/subtitle_view.rs @@ -1,17 +1,22 @@ use crate::cue_view::{CueView, CueViewMsg, CueViewOutput}; -use crate::util::Tracker; +use crate::subtitle_selection_dialog::SubtitleSettings; +use crate::subtitles::state::CueAddress; use gtk::prelude::*; use relm4::prelude::*; pub struct SubtitleView { primary_cue: Controller<CueView>, - secondary_cue: Tracker<Option<String>>, + secondary_cue: Option<String>, + machine_translation: Option<String>, + show_secondary: bool, + show_machine_translation: bool, } #[derive(Debug)] pub enum SubtitleViewMsg { - SetPrimaryCue(Option<String>), - SetSecondaryCue(Option<String>), + SetPrimaryCue(Option<CueAddress>), + SetSecondaryCue(Option<CueAddress>), + ApplySubtitleSettings(SubtitleSettings), } #[derive(Debug)] @@ -39,12 +44,30 @@ impl SimpleComponent for SubtitleView { model.primary_cue.widget(), gtk::Box { + #[watch] + set_visible: model.show_secondary, set_vexpand: true, }, gtk::Label { - #[track = "model.secondary_cue.is_dirty()"] - set_text: model.secondary_cue.get().as_ref().map(|val| val.as_str()).unwrap_or(""), + #[watch] + set_text: model.secondary_cue.as_ref().map(|val| val.as_str()).unwrap_or(""), + #[watch] + set_visible: model.show_secondary, + set_justify: gtk::Justification::Center, + }, + + gtk::Box { + #[watch] + set_visible: model.show_machine_translation, + set_vexpand: true, + }, + + gtk::Label { + #[watch] + set_text: model.machine_translation.as_ref().map(|val| val.as_str()).unwrap_or(""), + #[watch] + set_visible: model.show_machine_translation, set_justify: gtk::Justification::Center, }, @@ -67,7 +90,10 @@ impl SimpleComponent for SubtitleView { CueViewOutput::MouseEnter => SubtitleViewOutput::SetHoveringCue(true), CueViewOutput::MouseLeave => SubtitleViewOutput::SetHoveringCue(false), }), - secondary_cue: Tracker::new(None), + secondary_cue: None, + machine_translation: None, + show_secondary: false, + show_machine_translation: false, }; let widgets = view_output!(); @@ -76,18 +102,21 @@ impl SimpleComponent for SubtitleView { } fn update(&mut self, msg: Self::Input, _sender: ComponentSender<Self>) { - // Reset trackers - self.secondary_cue.reset(); - match msg { - SubtitleViewMsg::SetPrimaryCue(value) => { + SubtitleViewMsg::SetPrimaryCue(addr) => { self.primary_cue .sender() - .send(CueViewMsg::SetText(value)) + .send(CueViewMsg::SetCue(addr)) .unwrap(); + self.machine_translation = addr.and_then(|a| a.resolve_translation()) + } + SubtitleViewMsg::SetSecondaryCue(addr) => { + let text = addr.map(|addr| addr.resolve_text()); + self.secondary_cue = text; } - SubtitleViewMsg::SetSecondaryCue(value) => { - self.secondary_cue.set(value); + SubtitleViewMsg::ApplySubtitleSettings(settings) => { + self.show_secondary = settings.show_secondary; + self.show_machine_translation = settings.show_machine_translation; } } } diff --git a/src/subtitle_extraction/embedded.rs b/src/subtitles/extraction/embedded.rs index 0ba6178..920f52b 100644 --- a/src/subtitle_extraction/embedded.rs +++ b/src/subtitles/extraction/embedded.rs @@ -2,7 +2,7 @@ use std::sync::mpsc; use anyhow::Context; -use crate::subtitle_extraction::*; +use crate::{subtitles::SubtitleCue, subtitles::extraction::*}; pub fn extract_embedded_subtitles( // stream index to use when storing extracted subtitles, this index already @@ -23,12 +23,6 @@ pub fn extract_embedded_subtitles( match decoder.decode(&packet, &mut subtitle) { Ok(true) => { if let Some(cue) = parse_subtitle(&subtitle, &packet, time_base) { - SUBTITLE_TRACKS - .write() - .get_mut(&stream_ix) - .unwrap() - .cues - .push(cue.clone()); sender .output(SubtitleExtractorOutput::NewCue(stream_ix, cue)) .unwrap(); @@ -72,10 +66,14 @@ fn parse_subtitle( .collect::<Vec<String>>() .join("\n— "); - let start = pts_to_clock_time(packet.pts()?); - let end = pts_to_clock_time(packet.pts()? + packet.duration()); + let start_time = pts_to_clock_time(packet.pts()?); + let end_time = pts_to_clock_time(packet.pts()? + packet.duration()); - Some(SubtitleCue { start, end, text }) + Some(SubtitleCue { + text, + start_time, + end_time, + }) } fn extract_dialogue_text(dialogue_line: &str) -> Option<String> { diff --git a/src/subtitle_extraction/mod.rs b/src/subtitles/extraction/mod.rs index 9e7fff4..b012658 100644 --- a/src/subtitle_extraction/mod.rs +++ b/src/subtitles/extraction/mod.rs @@ -8,7 +8,7 @@ use std::{collections::BTreeMap, sync::mpsc, thread}; use ffmpeg::Rational; use relm4::{ComponentSender, Worker}; -use crate::tracks::{SUBTITLE_TRACKS, StreamIndex, SubtitleCue, SubtitleTrack, TrackMetadata}; +use crate::subtitles::{SUBTITLE_TRACKS, StreamIndex, SubtitleCue, SubtitleTrack, TrackMetadata}; pub struct SubtitleExtractor {} @@ -87,10 +87,7 @@ impl SubtitleExtractor { if stream.parameters().medium() == ffmpeg::media::Type::Subtitle { let metadata = TrackMetadata::from_ffmpeg_stream(&stream); - let track = SubtitleTrack { - metadata, - cues: Vec::new(), - }; + let track = SubtitleTrack::new(metadata); SUBTITLE_TRACKS.write().insert(stream_ix, track); @@ -117,10 +114,7 @@ impl SubtitleExtractor { None => "Auto-generated from audio (Whisper)".to_string(), }); - let track = SubtitleTrack { - metadata, - cues: Vec::new(), - }; + let track = SubtitleTrack::new(metadata); SUBTITLE_TRACKS.write().insert(stream_ix, track); diff --git a/src/subtitle_extraction/whisper.rs b/src/subtitles/extraction/whisper.rs index ffa2e47..bd6fba7 100644 --- a/src/subtitle_extraction/whisper.rs +++ b/src/subtitles/extraction/whisper.rs @@ -8,7 +8,10 @@ use anyhow::Context; use ffmpeg::{filter, frame}; use serde::Deserialize; -use crate::{subtitle_extraction::*, tracks::StreamIndex}; +use crate::{ + subtitles::extraction::*, + subtitles::{StreamIndex, SubtitleCue}, +}; #[derive(Debug, Deserialize)] struct WhisperCue { @@ -117,18 +120,11 @@ fn handle_packet( let whisper_cue: WhisperCue = serde_json::from_str(&line_buf)?; let cue = SubtitleCue { - start: gst::ClockTime::from_mseconds(whisper_cue.start), - end: gst::ClockTime::from_mseconds(whisper_cue.end), text: whisper_cue.text, + start_time: gst::ClockTime::from_mseconds(whisper_cue.start), + end_time: gst::ClockTime::from_mseconds(whisper_cue.end), }; - // TODO deduplicate this vs. the code in embedded.rs - SUBTITLE_TRACKS - .write() - .get_mut(&stream_ix) - .unwrap() - .cues - .push(cue.clone()); sender .output(SubtitleExtractorOutput::NewCue(stream_ix, cue)) .unwrap(); diff --git a/src/subtitles/mod.rs b/src/subtitles/mod.rs new file mode 100644 index 0000000..a545d52 --- /dev/null +++ b/src/subtitles/mod.rs @@ -0,0 +1,86 @@ +pub mod extraction; +pub mod state; + +use std::collections::BTreeMap; + +use relm4::SharedState; + +pub type StreamIndex = usize; + +#[derive(Debug, Clone)] +pub struct MetadataCollection { + pub audio: BTreeMap<StreamIndex, TrackMetadata>, + pub subtitles: BTreeMap<StreamIndex, TrackMetadata>, +} + +#[derive(Debug, Clone)] +pub struct TrackMetadata { + pub language: Option<isolang::Language>, + pub title: Option<String>, +} + +#[derive(Debug, Clone)] +pub struct SubtitleCue { + pub text: String, + pub start_time: gst::ClockTime, + pub end_time: gst::ClockTime, +} + +#[derive(Debug, Clone)] +pub struct SubtitleTrack { + pub metadata: TrackMetadata, + // SoA of cue text, start timestamp, end timestamp + pub texts: Vec<String>, + pub start_times: Vec<gst::ClockTime>, + pub end_times: Vec<gst::ClockTime>, +} + +pub static SUBTITLE_TRACKS: SharedState<BTreeMap<StreamIndex, SubtitleTrack>> = SharedState::new(); + +impl TrackMetadata { + pub fn from_ffmpeg_stream(stream: &ffmpeg::Stream) -> Self { + let language_code = stream.metadata().get("language").map(|s| s.to_string()); + let title = stream.metadata().get("title").map(|s| s.to_string()); + + Self { + language: language_code.and_then(|code| isolang::Language::from_639_2b(&code)), + title, + } + } +} + +impl SubtitleTrack { + pub fn new(metadata: TrackMetadata) -> Self { + Self { + metadata, + texts: Vec::new(), + start_times: Vec::new(), + end_times: Vec::new(), + } + } + + pub fn push_cue(&mut self, cue: SubtitleCue) { + let SubtitleCue { + text, + start_time, + end_time, + } = cue; + + self.texts.push(text); + self.start_times.push(start_time); + self.end_times.push(end_time); + } + + pub fn iter_cloned_cues(&self) -> impl Iterator<Item = SubtitleCue> { + self.texts + .iter() + .cloned() + .zip(self.start_times.iter().cloned()) + .zip(self.end_times.iter().cloned()) + .map(|((text, start_time), end_time)| SubtitleCue { + text, + start_time, + end_time, + }) + } +} diff --git a/src/subtitles/state.rs b/src/subtitles/state.rs new file mode 100644 index 0000000..6b1ebda --- /dev/null +++ b/src/subtitles/state.rs @@ -0,0 +1,63 @@ +use crate::{ + subtitles::{SUBTITLE_TRACKS, StreamIndex}, + translation::TRANSLATIONS, + util::Tracker, +}; + +#[derive(Default)] +pub struct SubtitleState { + pub stream_ix: Option<StreamIndex>, + pub last_started_cue_ix: Tracker<Option<usize>>, + pub last_ended_cue_ix: Tracker<Option<usize>>, +} + +#[derive(Clone, Copy, Debug)] +pub struct CueAddress(pub StreamIndex, pub usize); + +impl SubtitleState { + pub fn active_cue(&self) -> Option<CueAddress> { + if let Some(stream_ix) = self.stream_ix { + match (*self.last_started_cue_ix, *self.last_ended_cue_ix) { + (None, _) => None, + (Some(started_ix), None) => Some(CueAddress(stream_ix, started_ix)), + (Some(started_ix), Some(ended_ix)) => { + if started_ix > ended_ix { + Some(CueAddress(stream_ix, started_ix)) + } else { + None + } + } + } + } else { + None + } + } + + pub fn is_dirty(&self) -> bool { + self.last_started_cue_ix.is_dirty() || self.last_ended_cue_ix.is_dirty() + } + + pub fn reset(&mut self) { + self.last_started_cue_ix.reset(); + self.last_ended_cue_ix.reset(); + } + + pub fn set_stream_ix(&mut self, stream_ix: Option<StreamIndex>) { + self.stream_ix = stream_ix; + self.last_started_cue_ix.set(None); + self.last_ended_cue_ix.set(None); + } +} + +impl CueAddress { + pub fn resolve_text(&self) -> String { + SUBTITLE_TRACKS.read().get(&self.0).unwrap().texts[self.1].clone() + } + + pub fn resolve_translation(&self) -> Option<String> { + TRANSLATIONS + .read() + .get(&self.0) + .and_then(|tln| tln.get(self.1).cloned()) + } +} diff --git a/src/track_selector.rs b/src/track_selector.rs index 5c56e4d..ce04d07 100644 --- a/src/track_selector.rs +++ b/src/track_selector.rs @@ -2,7 +2,7 @@ use adw::prelude::*; use gtk::{gio, glib}; use relm4::prelude::*; -use crate::tracks::StreamIndex; +use crate::{subtitles::StreamIndex, util::Tracker}; glib::wrapper! { pub struct TrackInfo(ObjectSubclass<imp::TrackInfo>); @@ -65,11 +65,12 @@ pub struct TrackSelectorInit { #[derive(Debug)] pub enum TrackSelectorMsg { SetListModel(gio::ListStore), + Changed(StreamIndex), } #[derive(Debug)] pub enum TrackSelectorOutput { - Changed(Option<StreamIndex>), + Changed(StreamIndex), } #[relm4::component(pub)] @@ -87,11 +88,15 @@ impl SimpleComponent for TrackSelector { set_factory: Some(&track_factory), #[watch] set_model: Some(&model.track_list_model), - #[watch] - set_selected: model.track_ix.map_or(gtk::INVALID_LIST_POSITION, |ix| get_list_ix_from_stream_ix(&model.track_list_model, ix)), + // #[watch] + // set_selected: model.track_ix.map_or(gtk::INVALID_LIST_POSITION, |ix| get_list_ix_from_stream_ix(&model.track_list_model, ix)), connect_selected_notify[sender] => move |combo| { - let stream_index = get_stream_ix_from_combo(combo); - sender.output(TrackSelectorOutput::Changed(stream_index)).unwrap(); + if let Some(stream_ix) = get_stream_ix_from_combo(combo) { + println!("selected {}", stream_ix); + sender.input(TrackSelectorMsg::Changed(stream_ix)); + } else { + println!("selected none"); + } }, }, @@ -155,11 +160,18 @@ impl SimpleComponent for TrackSelector { ComponentParts { model, widgets } } - fn update(&mut self, msg: Self::Input, _sender: ComponentSender<Self>) { + fn update(&mut self, msg: Self::Input, sender: ComponentSender<Self>) { match msg { TrackSelectorMsg::SetListModel(list_model) => { self.track_list_model = list_model; } + TrackSelectorMsg::Changed(track_ix) => { + println!("changed {:?}", track_ix); + self.track_ix = Some(track_ix); + sender + .output(TrackSelectorOutput::Changed(track_ix)) + .unwrap(); + } } } } diff --git a/src/tracks.rs b/src/tracks.rs deleted file mode 100644 index 4d69e12..0000000 --- a/src/tracks.rs +++ /dev/null @@ -1,38 +0,0 @@ -use std::collections::BTreeMap; - -use relm4::SharedState; - -pub type StreamIndex = usize; - -#[derive(Debug, Clone)] -pub struct TrackMetadata { - pub language: Option<isolang::Language>, - pub title: Option<String>, -} - -#[derive(Debug, Clone)] -pub struct SubtitleTrack { - pub metadata: TrackMetadata, - pub cues: Vec<SubtitleCue>, -} - -#[derive(Debug, Clone)] -pub struct SubtitleCue { - pub start: gst::ClockTime, - pub end: gst::ClockTime, - pub text: String, -} - -pub static SUBTITLE_TRACKS: SharedState<BTreeMap<StreamIndex, SubtitleTrack>> = SharedState::new(); - -impl TrackMetadata { - pub fn from_ffmpeg_stream(stream: &ffmpeg::Stream) -> Self { - let language_code = stream.metadata().get("language").map(|s| s.to_string()); - let title = stream.metadata().get("title").map(|s| s.to_string()); - - Self { - language: language_code.and_then(|code| isolang::Language::from_639_2b(&code)), - title, - } - } -} diff --git a/src/transcript.rs b/src/transcript.rs index a8ae554..602e340 100644 --- a/src/transcript.rs +++ b/src/transcript.rs @@ -1,7 +1,7 @@ use gtk::{ListBox, pango::WrapMode, prelude::*}; use relm4::prelude::*; -use crate::tracks::{SUBTITLE_TRACKS, StreamIndex, SubtitleCue}; +use crate::subtitles::{SUBTITLE_TRACKS, StreamIndex, SubtitleCue}; #[derive(Debug)] pub enum SubtitleCueOutput { @@ -20,7 +20,7 @@ impl FactoryComponent for SubtitleCue { gtk::Button { inline_css: "padding: 5px; border-radius: 0;", connect_clicked: { - let start = self.start; + let start = self.start_time; move |_| { sender.output(SubtitleCueOutput::SeekTo(start)).unwrap() } @@ -124,8 +124,8 @@ impl SimpleComponent for Transcript { if let Some(stream_ix) = stream_index { let tracks = SUBTITLE_TRACKS.read(); if let Some(track) = tracks.get(&stream_ix) { - for cue in &track.cues { - self.active_cues.guard().push_back(cue.clone()); + for cue in track.iter_cloned_cues() { + self.active_cues.guard().push_back(cue); } } } diff --git a/src/translation/deepl.rs b/src/translation/deepl.rs new file mode 100644 index 0000000..f2e84d7 --- /dev/null +++ b/src/translation/deepl.rs @@ -0,0 +1,106 @@ +use std::{collections::BTreeMap, time::Duration}; + +use deepl::DeepLApi; +use relm4::prelude::*; + +use crate::{ + settings::Settings, + subtitles::{SUBTITLE_TRACKS, StreamIndex}, + translation::TRANSLATIONS, +}; + +pub struct DeeplTranslator { + stream_ix: Option<StreamIndex>, + next_cues_to_translate: BTreeMap<StreamIndex, usize>, +} + +#[derive(Debug)] +pub enum DeeplTranslatorMsg { + SelectTrack(Option<StreamIndex>), + // this is only used to drive the async translation + DoTranslate, +} + +impl AsyncComponent for DeeplTranslator { + type Init = (); + type Input = DeeplTranslatorMsg; + type Output = (); + type CommandOutput = (); + type Root = (); + type Widgets = (); + + async fn init( + _init: Self::Init, + _root: Self::Root, + sender: relm4::AsyncComponentSender<Self>, + ) -> AsyncComponentParts<Self> { + let model = Self { + stream_ix: None, + next_cues_to_translate: BTreeMap::new(), + }; + + sender.input(DeeplTranslatorMsg::DoTranslate); + + AsyncComponentParts { model, widgets: () } + } + + async fn update( + &mut self, + message: Self::Input, + sender: AsyncComponentSender<Self>, + _root: &Self::Root, + ) { + match message { + DeeplTranslatorMsg::SelectTrack(stream_ix) => { + self.stream_ix = stream_ix; + } + DeeplTranslatorMsg::DoTranslate => self.do_translate(sender).await, + } + } + + fn init_root() -> Self::Root { + () + } +} + +impl DeeplTranslator { + async fn do_translate(&mut self, sender: AsyncComponentSender<Self>) { + if let Some(stream_ix) = self.stream_ix { + let deepl = DeepLApi::with(&Settings::default().deepl_api_key()).new(); + + let next_cue_to_translate = self.next_cues_to_translate.entry(stream_ix).or_insert(0); + + if let Some(cue) = { + SUBTITLE_TRACKS + .read() + .get(&stream_ix) + .unwrap() + .texts + .get(*next_cue_to_translate) + .cloned() + } { + match deepl + .translate_text(cue, deepl::Lang::EN) + .model_type(deepl::ModelType::PreferQualityOptimized) + .await + { + Ok(mut resp) => { + TRANSLATIONS + .write() + .entry(stream_ix) + .or_insert(Vec::new()) + .push(resp.translations.pop().unwrap().text); + + *next_cue_to_translate = *next_cue_to_translate + 1; + } + Err(e) => { + log::error!("error fetching translation: {}", e) + } + }; + } + } + + relm4::tokio::time::sleep(Duration::from_secs(1)).await; + sender.input(DeeplTranslatorMsg::DoTranslate); + } +} diff --git a/src/translation/mod.rs b/src/translation/mod.rs new file mode 100644 index 0000000..4a1b358 --- /dev/null +++ b/src/translation/mod.rs @@ -0,0 +1,11 @@ +use std::collections::BTreeMap; + +use relm4::SharedState; + +use crate::subtitles::StreamIndex; + +pub mod deepl; + +pub use deepl::DeeplTranslator; + +pub static TRANSLATIONS: SharedState<BTreeMap<StreamIndex, Vec<String>>> = SharedState::new(); diff --git a/src/util/tracker.rs b/src/util/tracker.rs index 69a1c5f..060acae 100644 --- a/src/util/tracker.rs +++ b/src/util/tracker.rs @@ -1,3 +1,5 @@ +use std::ops::Deref; + pub struct Tracker<T> { inner: T, dirty: bool, @@ -40,8 +42,24 @@ impl<T> Tracker<T> { } } +impl<T> Deref for Tracker<T> { + type Target = T; + + fn deref(&self) -> &Self::Target { + self.get() + } +} + impl<T: Default> Default for Tracker<T> { fn default() -> Self { Self::new(T::default()) } } + +impl<T: Eq> Tracker<T> { + pub fn set_if_ne(&mut self, value: T) { + if self.inner != value { + self.set(value); + } + } +} diff --git a/subs.srt b/subs.srt deleted file mode 100644 index 5442ab9..0000000 --- a/subs.srt +++ /dev/null @@ -1,644 +0,0 @@ -0 -00:00:00.000 --> 00:00:00.040 -... - -0 -00:00:00.040 --> 00:00:13.000 -Alors, comment ça s'est passé ? - -0 -00:00:13.000 --> 00:00:17.180 -Formidable. - -0 -00:00:17.180 --> 00:00:23.000 -Vous voulez pas en parler ? - -0 -00:00:23.000 --> 00:00:25.040 -Si, je veux en parler. - -1 -00:00:29.994 --> 00:00:37.894 -Ça s'est passé quand ? - -1 -00:00:37.894 --> 00:00:38.414 -Ce matin. - -1 -00:00:38.414 --> 00:00:56.054 -Elle a pris ça comment ? - -1 -00:00:56.054 --> 00:00:58.814 -À votre avis ? - -1 -00:00:58.814 --> 00:00:59.974 -Et vous ? - -2 -00:00:59.988 --> 00:01:02.428 -ou à une vraie partie de plaisir. - -2 -00:01:02.428 --> 00:01:08.748 -Ça va ? - -2 -00:01:08.748 --> 00:01:09.428 -Je pars. - -2 -00:01:09.428 --> 00:01:12.788 -Tu pars où ? - -2 -00:01:12.788 --> 00:01:13.388 -En Jordanie. - -2 -00:01:13.388 --> 00:01:17.128 -Pour quoi faire ? - -2 -00:01:17.128 --> 00:01:19.488 -J'ai accepté un poste au lycée français d'Aman. - -2 -00:01:19.488 --> 00:01:23.368 -Mais longtemps ? - -2 -00:01:23.368 --> 00:01:24.688 -Ça remplace moi, je sais pas combien de temps. - -3 -00:01:29.983 --> 00:01:42.983 -Paul, tu es en train de me dire que tu pars ou tu es en train de me dire autre chose ? - -3 -00:01:42.983 --> 00:01:47.223 -Les deux. - -3 -00:01:47.223 --> 00:01:51.423 -Vous avez adopté quelle stratégie finalement ? - -3 -00:01:51.423 --> 00:01:51.983 -La plus nulle. - -3 -00:01:51.983 --> 00:01:54.023 -C'est une nuit à pas de stratégie. - -3 -00:01:54.023 --> 00:01:58.323 -Celle qui est naze, triste, juste on s'arrête quoi. - -4 -00:01:59.978 --> 00:02:03.938 -Vous avez dit quoi ? - -4 -00:02:03.938 --> 00:02:05.138 -Les trucs les plus nuls de la Terre. - -4 -00:02:05.138 --> 00:02:08.358 -Que je ne parvenais plus à comprendre ce que je faisais à Damas. - -4 -00:02:08.358 --> 00:02:10.978 -Que je vivais mal à mon divorce. - -4 -00:02:10.978 --> 00:02:14.398 -Que je ne supportais pas de vieillir, d'être prof. - -4 -00:02:14.398 --> 00:02:16.678 -Que ma fille me manquait. - -4 -00:02:16.678 --> 00:02:20.438 -Bref, qu'il fallait que je parte et que je devais partir seul. - -4 -00:02:20.438 --> 00:02:27.358 -Et qu'est-ce qu'elle a dit, elle ? - -4 -00:02:27.358 --> 00:02:29.358 -Qu'est-ce que vous voulez qu'elle dise ? - -5 -00:02:29.972 --> 00:02:31.772 -Elle m'a insulté. - -5 -00:02:31.772 --> 00:02:36.572 -Ça ne l'est pas. - -5 -00:02:36.572 --> 00:02:36.952 -Elle a rêvé. - -5 -00:02:36.952 --> 00:02:39.032 -Elle a un peu pleuré. - -5 -00:02:39.032 --> 00:02:47.412 -Je savais que tu n'allais pas rester éternellement en Syrie, surtout en ce moment. - -5 -00:02:47.412 --> 00:02:50.472 -Enfin, la scène habituelle, quoi. Je la largue un peu brutalement quand même. - -5 -00:02:50.472 --> 00:02:52.132 -Ça ne me dérangeait pas. - -5 -00:02:52.132 --> 00:02:56.232 -Je crois même que ça m'arrangeait à cause de Marwan. - -5 -00:02:56.232 --> 00:02:57.732 -Elle m'a crié dessus. - -6 -00:02:59.967 --> 00:03:02.407 -C'est triste. - -6 -00:03:02.407 --> 00:03:09.567 -Moi aussi, je suis triste. - -6 -00:03:09.567 --> 00:03:13.187 -Je t'aime bien. - -6 -00:03:13.187 --> 00:03:17.307 -C'est très bien. - -6 -00:03:17.307 --> 00:03:22.027 -Et puis c'est bien que tu me dises ça comme ça. - -6 -00:03:22.027 --> 00:03:25.047 -J'aurais pas aimé le savoir trop à l'avance. - -7 -00:03:29.962 --> 00:03:32.002 -Elle a voulu me jeter son sac au visage, mais elle s'est retenue. - -7 -00:03:32.002 --> 00:03:33.862 -Elle est partie. - -7 -00:03:33.862 --> 00:03:38.002 -En claquant la porte ? - -7 -00:03:38.002 --> 00:03:39.222 -En claquant la porte. - -7 -00:03:39.222 --> 00:03:47.862 -Bon, il va être temps que je quitte le pays, moi. - -7 -00:03:47.862 --> 00:03:51.242 -Il n'y a pas trop le choix, hein ? - -7 -00:03:51.242 --> 00:03:52.282 -Non. - -7 -00:03:52.282 --> 00:03:56.262 -Bon, je vais couper. - -7 -00:03:56.262 --> 00:03:57.222 -Ok, à bientôt. - -7 -00:03:57.222 --> 00:03:58.062 -À bientôt. - -8 -00:03:59.956 --> 00:04:09.956 -♪♪ - -8 -00:04:09.956 --> 00:04:19.956 -♪♪ - -8 -00:04:19.956 --> 00:04:29.956 -♪♪ - -9 -00:04:29.951 --> 00:04:43.131 -Vous allez avec eux, moi je vous suivrai derrière. - -9 -00:04:43.131 --> 00:04:47.991 -Bonjour, on m'appelle Pépé. - -9 -00:04:47.991 --> 00:04:50.011 -Et moi c'est Mémé. - -9 -00:04:50.011 --> 00:04:50.691 -Enchanté. - -9 -00:04:50.691 --> 00:04:52.211 -Derrière c'est la mule. - -10 -00:04:59.946 --> 00:05:22.606 -C'est quoi le programme ? - -10 -00:05:22.606 --> 00:05:24.766 -On vous conduit à votre nouveau chez vous, on va passer par le SASS. - -10 -00:05:24.766 --> 00:05:26.666 -Celui de l'étoile ? - -10 -00:05:26.666 --> 00:05:27.966 -Non, celui d'opéra. - -12 -00:06:29.295 --> 00:06:29.935 -Bon à partir de là, - -13 -00:06:29.930 --> 00:06:31.930 -A partir de maintenant, vous passez toujours par un sas. - -13 -00:06:31.930 --> 00:06:32.930 -D'accord ? - -13 -00:06:32.930 --> 00:06:34.930 -Quelle que soit votre destination, vous passez par un sas. - -13 -00:06:34.930 --> 00:06:39.930 -Et si vous allez boulevard Mortier ou si vous en revenez, vous passez par un sas et vous changez de moyen de locomotion. - -13 -00:06:39.930 --> 00:06:40.930 -D'accord ? - -13 -00:06:40.930 --> 00:06:43.930 -Si vous venez à pied, vous prenez une voiture et vice versa. - -13 -00:06:43.930 --> 00:06:46.930 -D'accord. - -14 -00:06:59.924 --> 00:07:04.924 -I'm going to make a fire. - -14 -00:07:04.924 --> 00:07:09.924 -I'll make a fire with a fire extinguisher. - -14 -00:07:09.924 --> 00:07:14.924 -I'll make a fire with a fire extinguisher. - -14 -00:07:14.924 --> 00:07:19.924 -I'll make a fire with a fire extinguisher. - -14 -00:07:19.924 --> 00:07:24.924 -I'll make a fire with a fire extinguisher. - -14 -00:07:24.924 --> 00:07:29.884 -Takk for watching! - -15 -00:07:29.919 --> 00:07:43.919 -موسيقى - -15 -00:07:43.919 --> 00:07:57.919 -موسيقى - -17 -00:08:29.908 --> 00:08:40.308 -Il conduisait à deux à l'heure, il paraît. - -17 -00:08:40.308 --> 00:08:42.768 -Il était complètement ivre. - -17 -00:08:42.768 --> 00:08:45.748 -Il aurait dit aux policiers qu'il allait lentement parce qu'il avait peur d'avoir un incident. - -17 -00:08:45.748 --> 00:08:47.288 -Ils l'ont amené au poste. - -17 -00:08:47.288 --> 00:08:50.888 -Là, c'est là qu'a eu lieu l'interpellation, à Belouisdad. - -17 -00:08:50.888 --> 00:08:53.248 -Au centre d'Alger, tout près du Sofitel. - -17 -00:08:53.248 --> 00:08:58.368 -Ça, c'est la reconstitution du trajet à partir des points où le téléphone a borné. - -17 -00:08:59.468 --> 00:08:59.868 -Merci. - -18 -00:08:59.903 --> 00:09:00.763 -J'ai suivi cet itinéraire. - -18 -00:09:00.763 --> 00:09:07.283 -Là, c'est le commissariat de police de Belouisdad. - -18 -00:09:07.283 --> 00:09:08.903 -Et depuis ? - -18 -00:09:08.903 --> 00:09:10.543 -Depuis, il n'a pas bougé. - -18 -00:09:10.543 --> 00:09:12.623 -On vérifie tous les quarts d'heure. - -18 -00:09:12.623 --> 00:09:14.663 -Cyclone est toujours à l'intérieur. - -18 -00:09:14.663 --> 00:09:16.723 -On a mis une équipe devant l'entrée. - -18 -00:09:16.723 --> 00:09:18.903 -Vous êtes sûr que Cyclone était bourré ? - -18 -00:09:18.903 --> 00:09:20.123 -Ouais. - -18 -00:09:20.123 --> 00:09:22.163 -Mais il y avait pas mal de témoins. - -18 -00:09:22.163 --> 00:09:25.903 -Il était musulman pratiquant ? - -18 -00:09:25.903 --> 00:09:26.863 -Ouais. - -19 -00:09:29.898 --> 00:09:35.058 -Je propose qu'on envoie quelqu'un au commissariat pour vérifier dans quel état il est. - -19 -00:09:35.058 --> 00:09:37.178 -Très bien. Et dès qu'il sort, vous me promenez. - -19 -00:09:37.178 --> 00:09:45.938 -Dites-moi, vous l'aviez entraîné à subir un interrogatoire sous alcool, je suppose ? - -19 -00:09:45.938 --> 00:09:46.338 -Bien sûr. - -19 -00:09:46.338 --> 00:09:50.038 -Je peux voir les enregistrements ? J'aimerais bien savoir comment il s'en était sorti. - -19 -00:09:50.038 --> 00:09:53.678 -Il s'en était bien sorti ? - -19 -00:09:53.678 --> 00:09:55.578 -Ouais. Je vous les trouve. - -20 -00:09:59.892 --> 00:10:02.892 -Vous avez besoin de quelque chose, monsieur Manon ? - -20 -00:10:02.892 --> 00:10:04.892 -Non, non, ça va, ça va. - -21 -00:10:29.887 --> 00:10:37.367 -Il vient d'arriver. - -21 -00:10:37.367 --> 00:10:39.447 -C'est une rôle, ça. - -21 -00:10:39.447 --> 00:10:43.607 -J'ai déjà essayé chez le DG, ça doit te coûter un peu du cul. - -21 -00:10:43.607 --> 00:10:45.207 -Tu peux le dire, ouais. - -21 -00:10:45.207 --> 00:10:47.267 -Ça, c'est fini le dos coincé. - -21 -00:10:47.267 --> 00:10:49.647 -Tu veux que je t'aille à le déballer ? - -21 -00:10:49.647 --> 00:10:51.727 -Non, merci. Plus tard. - -22 -00:10:59.882 --> 00:11:10.782 -Si y a quelque chose qui m'a échappé une fois, c'est ma chatte en pêche de foie. - -22 -00:11:10.782 --> 00:11:20.402 -Oui, monsieur Jacques. - -22 -00:11:20.402 --> 00:11:22.022 -Il a été muté. - -22 -00:11:22.022 --> 00:11:24.882 -Il a fait une offre qu'il pouvait pas refuser. - -22 -00:11:24.882 --> 00:11:26.882 -Promotion ou quoi ? - -22 -00:11:26.882 --> 00:11:28.762 -Moi, j'avais compris. - -23 -00:11:29.876 --> 00:11:30.976 -Il m'en avait informé. - -23 -00:11:30.976 --> 00:11:35.176 -Donc c'est moi qui serai votre nouveau référent au Droneur. - -23 -00:11:35.176 --> 00:11:36.876 -Je m'appelle Raymond Cisteron. - -23 -00:11:36.876 --> 00:11:40.576 -Cisteron ? - -23 -00:11:40.576 --> 00:11:40.936 -Bon. - -23 -00:11:40.936 --> 00:11:42.996 -Et vous êtes un... - -23 -00:11:42.996 --> 00:11:44.476 -Oui, j'ai étudié les dossiers dans le détail. - -23 -00:11:44.476 --> 00:11:48.896 -Si vous êtes d'accord, on va commencer par celui du colonel Bazir. - -23 -00:11:48.896 --> 00:11:50.536 -D'accord. - -24 -00:11:59.871 --> 00:12:17.171 -Qu'est-ce que c'est ? - -24 -00:12:17.171 --> 00:12:19.131 -Château Giravates 2005. - -24 -00:12:19.131 --> 00:12:20.991 -Un Pauillac du très bon. - -24 -00:12:20.991 --> 00:12:24.251 -Vous voulez que je le boive ? - -24 -00:12:24.251 --> 00:12:25.711 -Vous voulez boire autre chose ? - -24 -00:12:25.711 --> 00:12:26.871 -La bière, la vodka ? - -25 -00:12:29.866 --> 00:12:34.026 -Je suis un peu embêté, mais vous savez que je n'ai pas le droit de boire de l'alcool, c'est contraire à ma religion. - -25 -00:12:34.026 --> 00:12:36.486 -Vous allez faire une entorse. - -25 -00:12:36.486 --> 00:12:41.286 -On doit tester votre capacité à réciter votre légende sous alcool. - -25 -00:12:41.286 --> 00:12:45.326 -Désolé, ce n'est pas possible. - -25 -00:12:45.326 --> 00:12:49.286 -Rachid, vous allez boire, ce n'est pas la peine de discuter, c'est le protocole. - -25 -00:12:49.286 --> 00:12:54.466 -En même temps, si vous m'envoyez à Alger, c'est aussi parce que je suis musulman. - -25 -00:12:55.286 --> 00:12:59.826 -Il est impossible de vous laisser partir si on ne sait pas comment vous tenez votre légende quand vous perdez le compte. - -26 -00:12:59.860 --> 00:13:00.260 -Je contrôle. - -26 -00:13:00.260 --> 00:13:06.900 -Si je n'avais jamais bu une goutte d'alcool, - -26 -00:13:06.900 --> 00:13:07.960 -je n'en boirais jamais de mon plein gré. - -26 -00:13:07.960 --> 00:13:11.160 -Si je me trouve dans une situation où on m'oblige à boire, - -26 -00:13:11.160 --> 00:13:12.780 -c'est que je suis déjà dans une très mauvaise posture. - -26 -00:13:12.780 --> 00:13:14.200 -Vous êtes d'accord ? - -26 -00:13:14.200 --> 00:13:15.480 -C'est que ma légende, elle pèse plus grand-chose. - -26 -00:13:15.480 --> 00:13:19.660 -Si on en est à me faire boire en Algérie, - -26 -00:13:19.660 --> 00:13:21.700 -c'est que c'est déjà trop tard. - -27 -00:13:29.855 --> 00:13:31.495 -Vous êtes vraiment buté. - |