diff --git a/Cargo.lock b/Cargo.lock index 583a83d..5865228 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -46,6 +46,24 @@ dependencies = [ "memchr", ] +[[package]] +name = "aligned" +version = "0.4.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ee4508988c62edf04abd8d92897fca0c2995d907ce1dfeaf369dac3716a40685" +dependencies = [ + "as-slice", +] + +[[package]] +name = "aligned-vec" +version = "0.6.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dc890384c8602f339876ded803c97ad529f3842aba97f6392b3dba0dd171769b" +dependencies = [ + "equator", +] + [[package]] name = "allocator-api2" version = "0.2.21" @@ -103,6 +121,29 @@ version = "1.0.14" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "940b3a0ca603d1eade50a4846a2afffd5ef57a9feac2c0e2ec2e14f9ead76000" +[[package]] +name = "anyhow" +version = "1.0.104" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "330a5ed07fa54e4702c9d6c4174f74427fc0ef6e214bbd677ae50a5099946470" + +[[package]] +name = "arbitrary" +version = "1.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c3d036a3c4ab069c7b410a2ce876bd74808d2d0888a82667669f8e783a898bf1" + +[[package]] +name = "arg_enum_proc_macro" +version = "0.3.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0ae92a5119aa49cdbcf6b9f893fe4e1d98b04ccbf82ee0584ad948a44a734dea" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.119", +] + [[package]] name = "arrayref" version = "0.3.9" @@ -121,6 +162,15 @@ version = "1.0.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "175571dd1d178ced59193a6fc02dde1b972eb0bc56c892cde9beeceac5bf0f6b" +[[package]] +name = "as-slice" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "516b6b4f0e40d50dcda9365d53964ec74560ad4284da2e7fc97122cd83174516" +dependencies = [ + "stable_deref_trait", +] + [[package]] name = "async-broadcast" version = "0.7.2" @@ -276,6 +326,49 @@ version = "1.5.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f2032f911046de80f0a198e0901378627c33f59ea0ac00e363d481118bd70a53" +[[package]] +name = "av-scenechange" +version = "0.14.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0f321d77c20e19b92c39e7471cf986812cbb46659d2af674adc4331ef3f18394" +dependencies = [ + "aligned", + "anyhow", + "arg_enum_proc_macro", + "arrayvec", + "log", + "num-rational", + "num-traits", + "pastey", + "rayon", + "thiserror 2.0.19", + "v_frame", + "y4m", +] + +[[package]] +name = "av1-grain" +version = "0.2.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8cfddb07216410377231960af4fcab838eaa12e013417781b78bd95ee22077f8" +dependencies = [ + "anyhow", + "arrayvec", + "log", + "nom 8.0.0", + "num-rational", + "v_frame", +] + +[[package]] +name = "avif-serialize" +version = "0.8.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e7178fe5f7d460b13895ebb9dcb28a3a6216d2df2574a0806cb51b555d297f38" +dependencies = [ + "arrayvec", +] + [[package]] name = "aws-lc-rs" version = "1.17.3" @@ -335,6 +428,12 @@ dependencies = [ "syn 2.0.119", ] +[[package]] +name = "bit_field" +version = "0.10.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1e4b40c7323adcfc0a41c4b88143ed58346ff65a288fc144329c5c45e05d70c6" + [[package]] name = "bitflags" version = "1.3.2" @@ -350,6 +449,15 @@ dependencies = [ "serde_core", ] +[[package]] +name = "bitstream-io" +version = "4.10.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7eff00be299a18769011411c9def0d827e8f2d7bf0c3dbf53633147a8867fd1f" +dependencies = [ + "no_std_io2", +] + [[package]] name = "block2" version = "0.5.1" @@ -391,6 +499,12 @@ dependencies = [ "cfg_aliases", ] +[[package]] +name = "built" +version = "0.8.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5c0e531d93d39c34eef561e929e8a7f86d77a5af08aac4f6d6e39976c51858e9" + [[package]] name = "bumpalo" version = "3.20.3" @@ -504,7 +618,7 @@ version = "0.6.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "6fac387a98bb7c37292057cffc56d62ecb629900026402633ae9160df93a8766" dependencies = [ - "nom", + "nom 7.1.3", ] [[package]] @@ -536,7 +650,7 @@ checksum = "d524456ba66e72eb8b115ff89e01e497f8e6d11d78b70b1aa13c0fbd97540a81" dependencies = [ "cfg-if", "cpufeatures", - "rand_core", + "rand_core 0.10.1", ] [[package]] @@ -757,12 +871,37 @@ dependencies = [ "crossbeam-utils", ] +[[package]] +name = "crossbeam-deque" +version = "0.8.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5181e0de7b61eb03a81e347d6dd8797bae9da5146707b51077e2d71a54ec0ceb" +dependencies = [ + "crossbeam-epoch", + "crossbeam-utils", +] + +[[package]] +name = "crossbeam-epoch" +version = "0.9.20" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2d6914041f254d6e9176c01941b21115dcfb7089e55135a35411081bd106ef3f" +dependencies = [ + "crossbeam-utils", +] + [[package]] name = "crossbeam-utils" version = "0.8.22" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "61803da095bee82a81bb1a452ecc25d3b2f1416d1897eb86430c6159ef717c17" +[[package]] +name = "crunchy" +version = "0.2.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "460fbee9c2c2f33933d720630a6a0bac33ba7053db5344fac858d4b8952d77d5" + [[package]] name = "ctor" version = "0.10.1" @@ -951,6 +1090,26 @@ dependencies = [ "syn 2.0.119", ] +[[package]] +name = "equator" +version = "0.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4711b213838dfee0117e3be6ac926007d7f433d7bbe33595975d4190cb07e6fc" +dependencies = [ + "equator-macro", +] + +[[package]] +name = "equator-macro" +version = "0.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "44f23cf4b44bfce11a86ace86f8a73ffdec849c9fd00a386a53d278bd9e81fb3" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.119", +] + [[package]] name = "equivalent" version = "1.0.2" @@ -1002,12 +1161,35 @@ dependencies = [ "pin-project-lite", ] +[[package]] +name = "exr" +version = "1.74.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "711fe42c9964295e01ee3fba3f9fe0e1d24b98886950d68efe81b1c76e21adf3" +dependencies = [ + "bit_field", + "half", + "lebe", + "miniz_oxide", + "num-complex", + "pulp", + "rayon-core", + "smallvec", + "zune-inflate", +] + [[package]] name = "fastrand" version = "2.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "da7c62ceae207dd37ea5b845da6a0696c799f85e97da1ab5b7910be3c1c80223" +[[package]] +name = "fax" +version = "0.2.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "caf1079563223d5d59d83c85886a56e586cfd5c1a26292e971a0fa266531ac5a" + [[package]] name = "fdeflate" version = "0.3.7" @@ -1347,7 +1529,7 @@ dependencies = [ "js-sys", "libc", "r-efi 6.0.0", - "rand_core", + "rand_core 0.10.1", "wasm-bindgen", ] @@ -1481,6 +1663,7 @@ dependencies = [ "serde", "serde_json", "slint", + "slint-build", "tokio", ] @@ -1512,6 +1695,17 @@ version = "1.0.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b40ca9252762c466af32d0b1002e91e4e1bc5398f77455e55474deb466355ff5" +[[package]] +name = "half" +version = "2.7.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6ea2d84b969582b4b1864a92dc5d27cd2b77b622a8d79306834f1be5ba20d84b" +dependencies = [ + "cfg-if", + "crunchy", + "zerocopy", +] + [[package]] name = "harfrust" version = "0.8.4" @@ -1797,6 +1991,7 @@ dependencies = [ "derive_more", "i-slint-common", "icu_normalizer", + "image", "itertools 0.14.0", "linked_hash_set", "lyon_extra", @@ -1804,9 +1999,14 @@ dependencies = [ "num_enum", "proc-macro2", "quote", + "rayon", + "resvg", "rowan", + "rspolib", + "skrifa 0.42.1", "smol_str 0.3.2", "strum", + "swash", "typed-index-collections", "unicode-segmentation", "url", @@ -2131,11 +2331,20 @@ checksum = "e6506c6c10786659413faa717ceebcb8f70731c0a60cbae39795fdf114519c1a" dependencies = [ "bytemuck", "byteorder-lite", + "color_quant", + "exr", + "gif", + "image-webp", "moxcms", "num-traits", "png", - "zune-core", - "zune-jpeg", + "qoi", + "ravif", + "rayon", + "rgb", + "tiff", + "zune-core 0.5.1", + "zune-jpeg 0.5.15", ] [[package]] @@ -2154,6 +2363,12 @@ version = "0.14.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "09e54e57b4c48b40f7aec75635392b12b3421fa26fe8b4332e63138ed278459c" +[[package]] +name = "imgref" +version = "1.12.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "89194689a993ab15268672e99e7b0e19da2da3268ac682e8f02d29d4d1434cd7" + [[package]] name = "indexmap" version = "2.14.0" @@ -2192,6 +2407,17 @@ dependencies = [ "num-traits", ] +[[package]] +name = "interpolate_name" +version = "0.2.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c34819042dc3d3971c46c2190835914dfbe0c3c13f61449b2997f4e9722dfa60" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.119", +] + [[package]] name = "io-lifetimes" version = "1.0.11" @@ -2341,12 +2567,34 @@ dependencies = [ "smallvec", ] +[[package]] +name = "lazy_static" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bbd2bcb4c963f2ddae06a2efc7e9f3591312473c50c6685e1f298068316e66fe" + +[[package]] +name = "lebe" +version = "0.5.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7a79a3332a6609480d7d0c9eab957bca6b455b91bb84e66d19f5ff66294b85b8" + [[package]] name = "libc" version = "0.2.189" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3eaf3ede3fee6db1a4c2ee091bf8a8b4dccdc6d17f656fb07896ee72867612f2" +[[package]] +name = "libfuzzer-sys" +version = "0.4.13" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a9fd2f41a1cba099f79a0b6b6c35656cf7c03351a7bae8ff0f28f25270f929d2" +dependencies = [ + "arbitrary", + "cc", +] + [[package]] name = "libloading" version = "0.8.9" @@ -2436,6 +2684,15 @@ version = "0.4.33" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0ceec5bc11778974d1bcb055b18002eba7f4b3518b6a0081b3af5f21666da9ad" +[[package]] +name = "loop9" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0fae87c125b03c1d2c0150c90365d7d6bcc53fb73a9acaef207d2d065860f062" +dependencies = [ + "imgref", +] + [[package]] name = "lru-slab" version = "0.1.2" @@ -2483,6 +2740,16 @@ dependencies = [ "num-traits", ] +[[package]] +name = "maybe-rayon" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8ea1f30cedd69f0a2954655f7188c6a834246d2bcf1e315e2ac40c4b24dc9519" +dependencies = [ + "cfg-if", + "rayon", +] + [[package]] name = "memchr" version = "2.8.3" @@ -2579,6 +2846,12 @@ dependencies = [ "windows-sys 0.61.2", ] +[[package]] +name = "natord" +version = "1.0.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "308d96db8debc727c3fd9744aac51751243420e46edf401010908da7f8d5e57c" + [[package]] name = "ndk" version = "0.9.0" @@ -2609,6 +2882,12 @@ dependencies = [ "jni-sys 0.3.1", ] +[[package]] +name = "new_debug_unreachable" +version = "1.0.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "650eef8c711430f1a879fdd01d4745a7deea475becfb90269c06775983bbf086" + [[package]] name = "nix" version = "0.31.3" @@ -2621,6 +2900,15 @@ dependencies = [ "libc", ] +[[package]] +name = "no_std_io2" +version = "0.9.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "418abd1b6d34fbf6cae440dc874771b0525a604428704c76e48b29a5e67b8003" +dependencies = [ + "memchr", +] + [[package]] name = "nom" version = "7.1.3" @@ -2631,6 +2919,72 @@ dependencies = [ "minimal-lexical", ] +[[package]] +name = "nom" +version = "8.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "df9761775871bdef83bee530e60050f7e54b1105350d6884eb0fb4f46c2f9405" +dependencies = [ + "memchr", +] + +[[package]] +name = "noop_proc_macro" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0676bb32a98c1a483ce53e500a81ad9c3d5b3f7c920c28c24e9cb0980d0b5bc8" + +[[package]] +name = "num-bigint" +version = "0.4.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c89e69e7e0f03bea5ef08013795c25018e101932225a656383bd384495ecc367" +dependencies = [ + "num-integer", + "num-traits", +] + +[[package]] +name = "num-complex" +version = "0.4.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "73f88a1307638156682bada9d7604135552957b7818057dcef22705b4d509495" +dependencies = [ + "bytemuck", + "num-traits", +] + +[[package]] +name = "num-derive" +version = "0.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ed3955f1a9c7c0c15e092f9c887db08b1fc683305fdf6eb6684f22555355e202" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.119", +] + +[[package]] +name = "num-integer" +version = "0.1.46" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7969661fd2958a5cb096e56c8e1ad0444ac2bbcd0061bd28660485a44879858f" +dependencies = [ + "num-traits", +] + +[[package]] +name = "num-rational" +version = "0.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f83d14da390562dca69fc84082e73e548e1ad308d24accdedd2720017cb37824" +dependencies = [ + "num-bigint", + "num-integer", + "num-traits", +] + [[package]] name = "num-traits" version = "0.2.19" @@ -3122,6 +3476,18 @@ dependencies = [ "icu_properties", ] +[[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" @@ -3251,6 +3617,15 @@ dependencies = [ "zerovec", ] +[[package]] +name = "ppv-lite86" +version = "0.2.21" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "85eae3c4ed2f50dcfe72643da4befc30deadb458a9b590d720cde2f2b1e97da9" +dependencies = [ + "zerocopy", +] + [[package]] name = "prettyplease" version = "0.2.37" @@ -3279,6 +3654,25 @@ dependencies = [ "unicode-ident", ] +[[package]] +name = "profiling" +version = "1.0.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3d595e54a326bc53c1c197b32d295e14b169e3cfeaa8dc82b529f947fba6bcf5" +dependencies = [ + "profiling-procmacros", +] + +[[package]] +name = "profiling-procmacros" +version = "1.0.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4488a4a36b9a4ba6b9334a32a39971f77c1436ec82c38707bce707699cc3bbcb" +dependencies = [ + "quote", + "syn 2.0.119", +] + [[package]] name = "pulldown-cmark" version = "0.13.4" @@ -3298,12 +3692,44 @@ version = "0.11.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "007d8adb5ddab6f8e3f491ac63566a7d5002cc7ed73901f72057943fa71ae1ae" +[[package]] +name = "pulp" +version = "0.22.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "046aa45b989642ec2e4717c8e72d677b13edd831a4d3b6cf37d9a3e54912496a" +dependencies = [ + "bytemuck", + "cfg-if", + "libm", + "num-complex", + "paste", + "pulp-wasm-simd-flag", + "raw-cpuid", + "reborrow", + "version_check", +] + +[[package]] +name = "pulp-wasm-simd-flag" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1d8f70e07b9c3962945a74e59ca1c511bba65b6419468acc217c457d93f3c740" + [[package]] name = "pxfm" version = "0.1.30" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d55d956fa96f5ec02be2e13af0e20391a5aa83d6a074e3ad368959d0fab299ea" +[[package]] +name = "qoi" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7f6d64c71eb498fe9eae14ce4ec935c555749aef511cca85b5568910d6e48001" +dependencies = [ + "bytemuck", +] + [[package]] name = "quick-error" version = "2.0.1" @@ -3349,7 +3775,7 @@ dependencies = [ "bytes", "getrandom 0.4.3", "lru-slab", - "rand", + "rand 0.10.2", "rand_pcg", "ring", "rustc-hash 2.1.3", @@ -3397,6 +3823,16 @@ version = "6.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f8dcc9c7d52a811697d2151c701e0d08956f92b0e24136cf4cf27b57a6a0d9bf" +[[package]] +name = "rand" +version = "0.9.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b9ef1d0d795eb7d84685bca4f72f3649f064e6641543d3a8c415898726a57b41" +dependencies = [ + "rand_chacha", + "rand_core 0.9.5", +] + [[package]] name = "rand" version = "0.10.2" @@ -3405,7 +3841,26 @@ checksum = "c7f5fa3a058cd35567ef9bfa5e75732bee0f9e4c55fa90477bef2dfcdbc4be80" dependencies = [ "chacha20", "getrandom 0.4.3", - "rand_core", + "rand_core 0.10.1", +] + +[[package]] +name = "rand_chacha" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d3022b5f1df60f26e1ffddd6c66e8aa15de382ae63b3a0c1bfc0e4d3e3f325cb" +dependencies = [ + "ppv-lite86", + "rand_core 0.9.5", +] + +[[package]] +name = "rand_core" +version = "0.9.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "76afc826de14238e6e8c374ddcc1fa19e374fd8dd986b0d2af0d02377261d83c" +dependencies = [ + "getrandom 0.3.4", ] [[package]] @@ -3420,7 +3875,66 @@ version = "0.10.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "caa0f4137e1c0a72f4c651489402276c8e8e1cf081f3b0ba156d2cbeef09e86a" dependencies = [ - "rand_core", + "rand_core 0.10.1", +] + +[[package]] +name = "rav1e" +version = "0.8.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "43b6dd56e85d9483277cde964fd1bdb0428de4fec5ebba7540995639a21cb32b" +dependencies = [ + "aligned-vec", + "arbitrary", + "arg_enum_proc_macro", + "arrayvec", + "av-scenechange", + "av1-grain", + "bitstream-io", + "built", + "cfg-if", + "interpolate_name", + "itertools 0.14.0", + "libc", + "libfuzzer-sys", + "log", + "maybe-rayon", + "new_debug_unreachable", + "noop_proc_macro", + "num-derive", + "num-traits", + "paste", + "profiling", + "rand 0.9.5", + "rand_chacha", + "simd_helpers", + "thiserror 2.0.19", + "v_frame", + "wasm-bindgen", +] + +[[package]] +name = "ravif" +version = "0.12.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ef69c1990ceef18a116855938e74793a5f7496ee907562bd0857b6ac734ab285" +dependencies = [ + "avif-serialize", + "imgref", + "loop9", + "quick-error", + "rav1e", + "rayon", + "rgb", +] + +[[package]] +name = "raw-cpuid" +version = "11.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "498cd0dc59d73224351ee52a95fee0f1a617a2eae0e7d9d720cc622c73a54186" +dependencies = [ + "bitflags 2.13.1", ] [[package]] @@ -3441,6 +3955,26 @@ dependencies = [ "objc2-quartz-core 0.3.2", ] +[[package]] +name = "rayon" +version = "1.12.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fb39b166781f92d482534ef4b4b1b2568f42613b53e5b6c160e24cfbfa30926d" +dependencies = [ + "either", + "rayon-core", +] + +[[package]] +name = "rayon-core" +version = "1.13.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "22e18b0f0062d30d4230b2e85ff77fdfe4326feb054b9783a3460d8435c8ab91" +dependencies = [ + "crossbeam-deque", + "crossbeam-utils", +] + [[package]] name = "read-fonts" version = "0.39.2" @@ -3462,6 +3996,12 @@ dependencies = [ "once_cell", ] +[[package]] +name = "reborrow" +version = "0.5.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "03251193000f4bd3b042892be858ee50e8b3719f2b08e5833ac4353724632430" + [[package]] name = "redox_syscall" version = "0.4.1" @@ -3574,7 +4114,7 @@ dependencies = [ "svgtypes", "tiny-skia 0.12.0", "usvg", - "zune-jpeg", + "zune-jpeg 0.5.15", ] [[package]] @@ -3632,6 +4172,19 @@ dependencies = [ "windows-sys 0.61.2", ] +[[package]] +name = "rspolib" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4fda9a7796aff63a7b1b39ccc93fffaaf65e20042984b4843041a49ca4677535" +dependencies = [ + "lazy_static", + "natord", + "snafu", + "unicode-linebreak", + "unicode-width", +] + [[package]] name = "rtoolbox" version = "0.0.5" @@ -3998,6 +4551,15 @@ dependencies = [ "simdutf8", ] +[[package]] +name = "simd_helpers" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "95890f873bec569a0362c235787f3aca6e1e887302ba4840839bcc6459c42da6" +dependencies = [ + "quote", +] + [[package]] name = "simdutf8" version = "0.1.5" @@ -4094,6 +4656,19 @@ dependencies = [ "vtable", ] +[[package]] +name = "slint-build" +version = "1.17.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "76684c11441f775b10bbc7b0e6b3016a39f17efdd659ffca112f17cfd6bdaed1" +dependencies = [ + "derive_more", + "fontique", + "i-slint-compiler", + "spin_on", + "toml_edit", +] + [[package]] name = "slint-macros" version = "1.17.1" @@ -4203,6 +4778,27 @@ dependencies = [ "serde", ] +[[package]] +name = "snafu" +version = "0.8.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6e84b3f4eacbf3a1ce05eac6763b4d629d60cbc94d632e4092c54ade71f1e1a2" +dependencies = [ + "snafu-derive", +] + +[[package]] +name = "snafu-derive" +version = "0.8.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c1c97747dbf44bb1ca44a561ece23508e99cb592e862f22222dcf42f51d1e451" +dependencies = [ + "heck", + "proc-macro2", + "quote", + "syn 2.0.119", +] + [[package]] name = "socket2" version = "0.6.5" @@ -4453,6 +5049,20 @@ dependencies = [ "syn 3.0.3", ] +[[package]] +name = "tiff" +version = "0.10.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "af9605de7fee8d9551863fd692cce7637f548dbd9db9180fcc07ccc6d26c336f" +dependencies = [ + "fax", + "flate2", + "half", + "quick-error", + "weezl", + "zune-jpeg 0.4.21", +] + [[package]] name = "tiny-skia" version = "0.11.4" @@ -4626,6 +5236,7 @@ dependencies = [ "indexmap", "toml_datetime", "toml_parser", + "toml_writer", "winnow", ] @@ -4916,6 +5527,17 @@ dependencies = [ "wasm-bindgen", ] +[[package]] +name = "v_frame" +version = "0.3.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "666b7727c8875d6ab5db9533418d7c764233ac9c0cff1d469aec8fa127597be2" +dependencies = [ + "aligned-vec", + "num-traits", + "wasm-bindgen", +] + [[package]] name = "version_check" version = "0.9.5" @@ -5695,6 +6317,12 @@ version = "0.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ec7a2a501ed189703dba8b08142f057e887dfc4b2cc4db2d343ac6376ba3e0b9" +[[package]] +name = "y4m" +version = "0.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7a5a4b21e1a62b67a2970e6831bc091d7b87e119e7f9791aef9702e3bef04448" + [[package]] name = "yazi" version = "0.2.1" @@ -5890,19 +6518,43 @@ version = "1.0.23" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "29666d0abbfad1e3dc4dcf6144730dd3a3ab225bbbdac83319345b1b44ccfc1b" +[[package]] +name = "zune-core" +version = "0.4.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3f423a2c17029964870cfaabb1f13dfab7d092a62a29a89264f4d36990ca414a" + [[package]] name = "zune-core" version = "0.5.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "cb8a0807f7c01457d0379ba880ba6322660448ddebc890ce29bb64da71fb40f9" +[[package]] +name = "zune-inflate" +version = "0.2.54" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "73ab332fe2f6680068f3582b16a24f90ad7096d5d39b974d1c0aff0125116f02" +dependencies = [ + "simd-adler32", +] + +[[package]] +name = "zune-jpeg" +version = "0.4.21" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "29ce2c8a9384ad323cf564b67da86e21d3cfdff87908bc1223ed5c99bc792713" +dependencies = [ + "zune-core 0.4.12", +] + [[package]] name = "zune-jpeg" version = "0.5.15" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "27bc9d5b815bc103f142aa054f561d9187d191692ec7c2d1e2b4737f8dbd7296" dependencies = [ - "zune-core", + "zune-core 0.5.1", ] [[package]] diff --git a/crates/app/Cargo.toml b/crates/app/Cargo.toml index 18b391e..8fd655e 100644 --- a/crates/app/Cargo.toml +++ b/crates/app/Cargo.toml @@ -18,3 +18,6 @@ serde_json.workspace = true security-framework = "3" slint.workspace = true tokio.workspace = true + +[build-dependencies] +slint-build = "=1.17.1" diff --git a/crates/app/build.rs b/crates/app/build.rs new file mode 100644 index 0000000..c19a7b8 --- /dev/null +++ b/crates/app/build.rs @@ -0,0 +1,3 @@ +fn main() { + slint_build::compile("ui/app.slint").unwrap(); +} diff --git a/crates/app/src/ui.rs b/crates/app/src/ui.rs index c1adf51..d174e0c 100644 --- a/crates/app/src/ui.rs +++ b/crates/app/src/ui.rs @@ -1,944 +1 @@ -slint::slint! { - import { Button, ComboBox, LineEdit, ListView, Palette, ScrollView, Spinner } from "std-widgets.slint"; - - export struct ServerRow { name: string, url: string } - export struct RepositoryRow { - name: string, owner: string, description: string, meta: string, favorite: bool, - } - export struct LabelRow { name: string, background: color, foreground: color } - export struct IssueRow { - number: int, title: string, summary: string, meta: string, labels: [LabelRow], - } - export struct PullRow { - number: int, owner: string, repository: string, title: string, summary: string, meta: string, - } - export struct MarkdownBlock { text: styled-text, kind: string } - export struct CommentRow { author: string, body: [MarkdownBlock], meta: string } - export struct GraphLane { color: color, top: bool, bottom: bool, node: bool, connect_left: bool } - export struct CommitRow { - sha: string, title: string, meta: string, refs: string, lanes: [GraphLane], - } - export struct FileRow { path: string, status: string } - export struct ActivityRow { - icon: string, title: string, detail: string, meta: string, - target: string, owner: string, repository: string, number: int, sha: string, - } - export struct DiffLine { old_number: string, new_number: string, text: string, kind: string } - export struct HeatCell { week: int, day: int, level: int } - - export global IconTheme { - in-out property style: 0; - } - - export global AppTheme { - in-out property mode: 0; - private property system-background: Palette.background; - out property dark: mode == 2 || (mode == 0 && system-background.to-oklch().lightness < 0.5); - out property accent: dark ? #0a84ff : #0879e1; - out property accent-soft: dark ? #17324d : #e5f2fd; - out property page: dark ? #1c1c1e : #f2f2f7; - out property surface: dark ? #2c2c2e : #ffffff; - out property surface-muted: dark ? #3a3a3c : #f5f5f8; - out property header: dark ? #242426 : #f8f8fa; - out property toolbar: dark ? #242426 : #fbfbfc; - out property border: dark ? #48484a : #dedee3; - out property separator: dark ? #3a3a3c : #d8d8dd; - out property text: dark ? #f2f2f7 : #17171a; - out property text-secondary: dark ? #c7c7cc : #5f5f66; - out property text-tertiary: dark ? #8e8e93 : #85858c; - out property code-background: dark ? #242426 : #e9e9ed; - out property quote-background: dark ? #242426 : #f6f6f8; - out property loading-overlay: dark ? #00000088 : #ffffff88; - changed mode => { - Palette.color-scheme = mode == 1 ? ColorScheme.light : mode == 2 ? ColorScheme.dark : ColorScheme.unknown; - } - } - - component AppIcon inherits Rectangle { - in property kind; - in property style: IconTheme.style; - in property icon-color: AppTheme.accent; - background: transparent; - - private property duotone: root.style == 1; - private property bold: root.style == 2; - private property issue-kind: root.kind == "issues" || root.kind == "issue-open" || root.kind == "issue-close" || root.kind == "issue-reopen" || root.kind == "issue-comment"; - private property repository-kind: root.kind == "repositories" || root.kind == "repo-create" || root.kind == "repo-rename" || root.kind == "repo-star" || root.kind == "repo-watch" || root.kind == "update"; - private property pull-kind: root.kind == "pulls" || root.kind == "pull-open" || root.kind == "pull-merge" || root.kind == "pull-close" || root.kind == "pull-reopen" || root.kind == "pull-comment" || root.kind == "pull-approve" || root.kind == "pull-reject"; - private property tag-kind: root.kind == "tag-push" || root.kind == "tag-delete"; - private property branch-kind: root.kind == "branch-create" || root.kind == "branch-delete"; - private property closed-shape: root.kind == "home" || root.issue-kind || root.repository-kind || root.kind == "favorite" || root.kind == "favorite-filled" || root.tag-kind || root.kind == "release"; - private property has-badge: root.kind == "branch-create" || root.kind == "branch-delete" || root.kind == "push" || root.kind == "repo-create" || root.kind == "repo-rename" || root.kind == "repo-star" || root.kind == "repo-watch" || root.kind == "update" || root.kind == "issue-open" || root.kind == "issue-close" || root.kind == "issue-reopen" || root.kind == "issue-comment" || root.kind == "pull-open" || root.kind == "pull-merge" || root.kind == "pull-close" || root.kind == "pull-reopen" || root.kind == "pull-comment" || root.kind == "pull-approve" || root.kind == "pull-reject" || root.kind == "tag-push" || root.kind == "tag-delete"; - private property positive: root.kind == "issue-close" || root.kind == "pull-merge" || root.kind == "pull-approve"; - private property destructive: root.kind == "branch-delete" || root.kind == "pull-close" || root.kind == "pull-reject" || root.kind == "tag-delete"; - private property badge-color: root.positive ? #20a866 : root.destructive ? #e45b4d : root.duotone ? #26bfd1 : root.icon-color; - private property shape: root.kind == "home" - ? "M3 10.5 L12 3 L21 10.5 V21 H15 V14 H9 V21 H3 Z" - : root.issue-kind - ? "M4 5 H20 V9 C18 9 18 15 20 15 V19 H4 V15 C6 15 6 9 4 9 Z" - : root.repository-kind - ? "M6 3 H18 C19.1 3 20 3.9 20 5 V21 H6 C4.9 21 4 20.1 4 19 V5 C4 3.9 4.9 3 6 3 Z" - : root.pull-kind - ? "M7 4 V20 M17 4 V11 C17 15 14 17 10 17 M7 4 H7 M17 4 H17" - : root.kind == "settings" - ? "M12 3 V6 M12 18 V21 M3 12 H6 M18 12 H21 M5.6 5.6 L7.8 7.8 M16.2 16.2 L18.4 18.4 M18.4 5.6 L16.2 7.8 M7.8 16.2 L5.6 18.4" - : root.kind == "back" - ? (root.bold ? "M16 4 L7 12 L16 20 Z" : "M16 4 L7 12 L16 20") - : root.kind == "filter" - ? (root.bold ? "M3 5 H21 L14 13 V20 L10 22 V13 Z" : "M4 6 H20 M4 12 H20 M4 18 H20") - : root.kind == "favorite" || root.kind == "favorite-filled" - ? "M12 3 L14.8 8.7 L21 9.6 L16.5 14 L17.6 20.3 L12 17.3 L6.4 20.3 L7.5 14 L3 9.6 L9.2 8.7 Z" - : root.kind == "disclosure" - ? (root.bold ? "M8 4 L17 12 L8 20 Z" : "M8 4 L17 12 L8 20") - : root.branch-kind - ? "M7 4 V20 M7 11 C11 11 14 9 14 5" - : root.kind == "push" - ? "M4 8 H13 M4 12 H13 M4 16 H13" - : root.tag-kind - ? "M3 5 V13 L11 21 L21 11 L13 3 H5 C3.9 3 3 3.9 3 5 Z" - : root.kind == "release" - ? "M4 8 L12 4 L20 8 V18 L12 22 L4 18 Z" - : "M12 4 V20 M4 12 H20"; - private property detail: root.kind == "home" - ? "M9 21 V14 H15 V21" - : root.issue-kind - ? "M12 8 V13 M12 16.5 V17" - : root.repository-kind - ? "M7 3 V21 M11 9 L8.5 12 L11 15 M15 9 L17.5 12 L15 15" - : root.pull-kind - ? "M7 4 A1.8 1.8 0 1 1 6.99 4 M7 20 A1.8 1.8 0 1 1 6.99 20 M17 4 A1.8 1.8 0 1 1 16.99 4" - : root.kind == "settings" - ? "M12 8 A4 4 0 1 1 11.99 8" - : root.kind == "filter" && !root.bold - ? "M9 4 V8 M15 10 V14 M8 16 V20" - : root.tag-kind - ? "M8 8 A1.5 1.5 0 1 1 7.99 8" - : root.kind == "release" - ? "M12 18 V8 M9 11 L12 8 L15 11 M7 6 V3 M17 6 V3" - : ""; - private property badge-detail: root.kind == "branch-create" || root.kind == "repo-create" || root.kind == "issue-open" || root.kind == "pull-open" - ? "M18 15.5 V20.5 M15.5 18 H20.5" - : root.kind == "issue-close" || root.kind == "pull-merge" || root.kind == "pull-approve" - ? "M15.4 18 L17.2 19.8 L20.7 16.2" - : root.kind == "branch-delete" || root.kind == "tag-delete" - ? "M15.3 18 H20.7" - : root.kind == "pull-close" || root.kind == "pull-reject" - ? "M16 16 L20 20 M20 16 L16 20" - : root.kind == "push" || root.kind == "tag-push" - ? "M18 20.5 V15.5 M15.7 17.8 L18 15.5 L20.3 17.8" - : root.kind == "repo-rename" - ? "M15.3 20.6 L16 18 L19.7 14.3 L21.7 16.3 L18 20 Z" - : root.kind == "repo-star" - ? "M18 14.5 L19 16.7 L21.4 17 L19.6 18.7 L20.1 21 L18 19.8 L15.9 21 L16.4 18.7 L14.6 17 L17 16.7 Z" - : root.kind == "repo-watch" - ? "M14.5 18 C16.3 15.8 19.7 15.8 21.5 18 C19.7 20.2 16.3 20.2 14.5 18 M18 17 A1 1 0 1 1 17.99 17" - : root.kind == "issue-reopen" || root.kind == "pull-reopen" - ? "M20.8 17 A3 3 0 1 0 20.2 20.3 M20.8 17 H18.5 M20.8 17 V19.3" - : root.kind == "issue-comment" || root.kind == "pull-comment" - ? "M15.5 17 H20.5 V20 H18 L16 21 V20 H15.5 Z M17 18.5 H17.1 M19 18.5 H19.1" - : root.kind == "update" - ? "M18 14.8 L18.8 17.2 L21.2 18 L18.8 18.8 L18 21.2 L17.2 18.8 L14.8 18 L17.2 17.2 Z" - : ""; - - Path { - width: 100%; height: 100%; viewbox-width: 24; viewbox-height: 24; - commands: root.shape; - fill: root.closed-shape ? (root.bold || root.kind == "favorite-filled" ? root.icon-color : root.duotone ? #26bfd133 : transparent) : transparent; - stroke: root.closed-shape && (root.bold || root.kind == "favorite-filled") ? transparent : root.icon-color; - stroke-width: root.bold ? 3px : root.duotone ? 2.2px : 1.8px; - stroke-line-cap: round; stroke-line-join: round; - } - if root.detail != "": Path { - width: 100%; height: 100%; viewbox-width: 24; viewbox-height: 24; - commands: root.detail; fill: transparent; - stroke: root.bold && root.closed-shape ? #ffffff : root.icon-color; - stroke-width: root.bold ? 2.4px : 1.8px; - stroke-line-cap: round; stroke-line-join: round; - } - if root.has-badge: Path { - width: 100%; height: 100%; viewbox-width: 24; viewbox-height: 24; - commands: "M18 13.5 A4.5 4.5 0 1 1 17.99 13.5 Z"; - fill: root.style == 0 ? AppTheme.surface : root.badge-color; - stroke: root.style == 0 ? root.badge-color : #ffffff; - stroke-width: root.style == 2 ? 1.5px : 1.2px; - } - if root.has-badge: Path { - width: 100%; height: 100%; viewbox-width: 24; viewbox-height: 24; - commands: root.badge-detail; fill: root.style == 0 ? transparent : #ffffff; - stroke: root.style == 0 ? root.badge-color : #ffffff; - stroke-width: root.bold ? 2px : 1.6px; - stroke-line-cap: round; stroke-line-join: round; - } - } - - component IconStyleChoice inherits Rectangle { - in property value; - in property label; - callback clicked(); - background: IconTheme.style == root.value ? AppTheme.accent-soft : AppTheme.surface-muted; - border-color: IconTheme.style == root.value ? AppTheme.accent : AppTheme.border; - border-width: IconTheme.style == root.value ? 2px : 1px; - border-radius: 11px; - TouchArea { clicked => { root.clicked(); } } - AppIcon { x: (parent.width - 30px) / 2; y: 10px; width: 30px; height: 30px; kind: "repositories"; style: root.value; } - Text { y: 48px; width: 100%; text: root.label; font-size: 12px; font-weight: IconTheme.style == root.value ? 650 : 500; color: AppTheme.text; horizontal-alignment: center; } - } - - component AppearanceChoice inherits Rectangle { - in property value; - in property label; - callback clicked(); - background: AppTheme.mode == root.value ? AppTheme.accent-soft : AppTheme.surface-muted; - border-color: AppTheme.mode == root.value ? AppTheme.accent : AppTheme.border; - border-width: AppTheme.mode == root.value ? 2px : 1px; - border-radius: 11px; - TouchArea { clicked => { root.clicked(); } } - Text { - width: 100%; height: 100%; text: root.label; - font-size: 14px; font-weight: AppTheme.mode == root.value ? 650 : 500; - color: AppTheme.mode == root.value ? AppTheme.accent : AppTheme.text; - horizontal-alignment: center; vertical-alignment: center; - } - } - - component Header inherits Rectangle { - in property title; - in property can_back; - in property can_filter: false; - callback back(); - callback filter(); - height: 58px; - width: 100%; - background: AppTheme.header; - border-color: AppTheme.border; - border-width: 0px; - - if can_back: TouchArea { - x: 8px; width: 54px; - clicked => { root.back(); } - AppIcon { x: 7px; y: 17px; width: 24px; height: 24px; kind: "back"; } - } - Text { - x: root.can_back || root.can_filter ? 62px : 12px; - width: parent.width - 2 * self.x; - text: root.title; - horizontal-alignment: center; - vertical-alignment: center; - font-size: 18px; - font-weight: 600; - color: AppTheme.text; - overflow: elide; - } - if can_filter: TouchArea { - x: parent.width - 58px; width: 58px; - clicked => { root.filter(); } - AppIcon { x: 17px; y: 17px; width: 24px; height: 24px; kind: "filter"; } - } - Rectangle { y: parent.height - 1px; height: 1px; background: AppTheme.border; } - } - - component EdgeBack inherits SwipeGestureHandler { - callback back(); - width: 24px; - height: 100%; - handle-swipe-right: true; - swiped => { root.back(); } - } - - component EmptyState inherits VerticalLayout { - in property title; - in property detail; - alignment: center; - spacing: 8px; - Text { text: root.title; font-size: 21px; font-weight: 600; horizontal-alignment: center; color: AppTheme.text; } - Text { text: root.detail; font-size: 15px; horizontal-alignment: center; wrap: word-wrap; color: AppTheme.text-secondary; } - } - - component MarkdownContent inherits VerticalLayout { - in property <[MarkdownBlock]> blocks; - spacing: 8px; - - for block in root.blocks: Rectangle { - height: block.kind == "rule" ? 9px : rendered.preferred-height + (block.kind == "code" || block.kind == "table" ? 16px : 0px); - background: block.kind == "code" || block.kind == "table" ? AppTheme.code-background : block.kind == "quote" ? AppTheme.quote-background : transparent; - border-radius: block.kind == "code" || block.kind == "table" ? 7px : 0px; - if block.kind == "quote": Rectangle { width: 3px; height: 100%; background: AppTheme.text-tertiary; border-radius: 2px; } - if block.kind == "rule": Rectangle { y: 4px; width: 100%; height: 1px; background: AppTheme.separator; } - rendered := StyledText { - x: block.kind == "code" || block.kind == "table" ? 8px : block.kind == "quote" ? 11px : 0px; - y: block.kind == "code" || block.kind == "table" ? 8px : 0px; - width: parent.width - self.x - (block.kind == "code" || block.kind == "table" ? 8px : 0px); - text: block.text; - default-color: AppTheme.text; - link-color: AppTheme.accent; - default-font-family: block.kind == "code" || block.kind == "table" ? "monospace" : ""; - default-font-size: block.kind == "heading1" ? 24px : block.kind == "heading2" ? 21px : block.kind == "heading3" ? 19px : block.kind == "heading4" ? 17px : 16px; - } - } - } - - component CommentCard inherits Rectangle { - in property comment; - height: content.preferred-height + 24px; - background: AppTheme.surface; - border-radius: 10px; - content := VerticalLayout { - x: 12px; y: 10px; width: parent.width - 24px; - spacing: 5px; - Text { text: root.comment.author; font-size: 13px; font-weight: 600; color: AppTheme.text; } - MarkdownContent { width: parent.width; blocks: root.comment.body; } - Text { text: root.comment.meta; font-size: 11px; color: AppTheme.text-tertiary; } - } - } - - component FileCard inherits Rectangle { - in property file; - callback clicked(); - height: 66px; - background: AppTheme.surface; - border-color: AppTheme.border; - border-width: 1px; - border-radius: 10px; - TouchArea { clicked => { root.clicked(); } } - Text { x: 13px; y: 10px; width: parent.width - 26px; text: root.file.path; font-size: 14px; font-weight: 600; color: AppTheme.text; overflow: elide; } - Text { x: 13px; y: 38px; width: parent.width - 26px; text: root.file.status; font-size: 12px; color: AppTheme.text-secondary; } - } - - component TabButton inherits Rectangle { - in property kind; - in property label; - in property active; - private property icon-area-height: root.height * 3 / 4; - private property icon-size: min(42px, self.icon-area-height - 6px); - callback clicked(); - background: transparent; - TouchArea { clicked => { root.clicked(); } } - AppIcon { - x: (parent.width - root.icon-size) / 2; - y: (root.icon-area-height - root.icon-size) / 2; - width: root.icon-size; height: root.icon-size; - kind: root.kind; icon-color: root.active ? AppTheme.accent : AppTheme.text-tertiary; - } - Text { - y: root.icon-area-height; height: parent.height - self.y; width: 100%; text: root.label; - color: root.active ? AppTheme.accent : AppTheme.text-tertiary; font-size: 9px; - font-weight: root.active ? 600 : 400; - horizontal-alignment: center; vertical-alignment: center; - } - } - - component PullToRefreshGesture inherits SwipeGestureHandler { - in property at_top; - in property refreshing; - callback refresh(); - enabled: root.at_top && !root.refreshing; - handle-swipe-down: true; - swiped => { root.refresh(); } - @children - if self.swiping: Rectangle { - x: (parent.width - 142px) / 2; y: 8px; width: 142px; height: 32px; - background: AppTheme.surface-muted; border-radius: 16px; - Text { text: "Release to refresh"; font-size: 12px; color: AppTheme.text-secondary; horizontal-alignment: center; vertical-alignment: center; } - } - } - - export component AppWindow inherits Window { - title: "Gotcha"; - preferred-width: 390px; - preferred-height: 844px; - background: AppTheme.page; - private property toolbar-height: 68px; - - in-out property tab: "home"; - in-out property page: "servers"; - in-out property <[ServerRow]> servers; - in-out property <[RepositoryRow]> repositories; - in-out property <[IssueRow]> issues; - in-out property <[PullRow]> pulls; - in-out property <[CommentRow]> comments; - in-out property <[CommitRow]> commits; - in-out property <[FileRow]> files; - in-out property <[ActivityRow]> activities; - in-out property <[HeatCell]> heat_cells; - in-out property contribution_count; - in-out property has_active_server; - in-out property home_server; - in-out property server_title; - in-out property repository_title; - in-out property issue_title; - in-out property issue_meta; - in-out property <[MarkdownBlock]> issue_body; - in-out property detail_title; - in-out property detail_meta; - in-out property detail_files_ref; - in-out property <[MarkdownBlock]> detail_body; - in-out property <[DiffLine]> diff_lines; - in-out property diff_columns; - in-out property issue_filter: "open"; - in-out property pull_filter: "open"; - in-out property <[string]> branch_choices; - in-out property branch_index; - in-out property filter_open; - in-out property loading; - in-out property home_loading; - in-out property refreshing; - in-out property error; - in-out property home_scroll_request; - in-out property servers_scroll_request; - in-out property repositories_scroll_request; - in-out property issues_scroll_request; - in-out property issue_scroll_request; - private property home_at_top: true; - private property repositories_at_top: true; - private property issues_at_top: true; - private property issue_at_top: true; - - callback select_tab(string); - callback refresh_home(); - callback refresh_repositories(); - callback refresh_issues(); - callback refresh_issue(); - callback refresh_pulls(); - callback refresh_pull(); - callback refresh_commits(); - callback show_add_server(); - callback save_server(string, string, string); - callback select_server(int); - callback select_repository(string, string); - callback toggle_favorite(string, string); - callback select_issue(int); - callback select_pull(string, string, int); - callback select_commit(string); - callback select_branch(string); - callback select_file(string); - callback select_pull_file(string); - callback select_activity(string, string, string, int, string); - callback choose_filter(string); - callback choose_icon_style(int); - callback choose_appearance(int); - callback back(); - - if tab == "home": Rectangle { - x: root.safe-area-insets.left; y: root.safe-area-insets.top; - width: root.width - root.safe-area-insets.left - root.safe-area-insets.right; - height: root.height - root.safe-area-insets.top - root.safe-area-insets.bottom - root.toolbar-height; - background: AppTheme.page; - Header { x: 0; y: 0; title: root.has_active_server ? root.home_server : "Home"; can_back: false; } - - if !root.has_active_server: EmptyState { - x: 32px; width: parent.width - 64px; y: 210px; height: 190px; - title: "No server selected"; - detail: "Open Issues to select a server or add your first one."; - } - - if root.has_active_server: Rectangle { - x: 16px; y: 72px; width: parent.width - 32px; height: 118px; - background: AppTheme.surface; border-radius: 12px; - Text { x: 13px; y: 10px; text: "Activity · last 12 months"; font-size: 13px; font-weight: 600; color: AppTheme.text; } - Text { x: 13px; y: 86px; text: root.contribution_count + " contributions"; font-size: 12px; color: AppTheme.text-secondary; } - for cell in root.heat_cells: Rectangle { - x: 13px + cell.week * 6.7px; y: 39px + cell.day * 6.2px; - width: 5.2px; height: 5.2px; border-radius: 1px; - background: cell.level == 0 ? (AppTheme.dark ? #3a3a3c : #e5e5e9) : cell.level == 1 ? #b8d9f4 : cell.level == 2 ? #72b5e8 : cell.level == 3 ? #278bd4 : #0969b7; - } - } - - if root.has_active_server && !root.home_loading && root.activities.length == 0: EmptyState { - x: 32px; width: parent.width - 64px; y: 250px; height: 150px; - title: "No recent activity"; - detail: "Your recent server actions will appear here."; - } - - if root.has_active_server: PullToRefreshGesture { - x: 16px; y: 202px; width: parent.width - 32px; height: parent.height - 214px; - at_top: root.home_at_top; refreshing: root.home_loading; - refresh => { root.refresh_home(); } - home_list := ListView { - property scroll_request: root.home_scroll_request; - width: 100%; height: 100%; mouse-drag-pan-enabled: true; - changed scroll_request => { self.viewport-y = 0px; root.home_at_top = true; } - scrolled => { root.home_at_top = self.viewport-y >= 0px; } - for activity in root.activities: Rectangle { - height: 94px; - background: AppTheme.surface; border-radius: 12px; - TouchArea { - enabled: activity.target != ""; - clicked => { root.select_activity(activity.target, activity.owner, activity.repository, activity.number, activity.sha); } - } - Rectangle { - x: 12px; y: 14px; width: 34px; height: 34px; border-radius: 17px; - background: AppTheme.accent-soft; - AppIcon { x: 5px; y: 5px; width: 24px; height: 24px; kind: activity.icon; } - } - Text { x: 57px; y: 11px; width: parent.width - 69px; height: 24px; text: activity.title; font-size: 15px; font-weight: 600; color: AppTheme.text; overflow: elide; } - Text { x: 57px; y: 36px; width: parent.width - 69px; height: 35px; text: activity.detail; font-size: 13px; color: AppTheme.text-secondary; wrap: word-wrap; overflow: elide; } - Text { x: 57px; y: 72px; width: parent.width - 69px; text: activity.meta; font-size: 11px; color: AppTheme.text-tertiary; overflow: elide; } - } - } - } - - if root.home_loading: Spinner { width: 34px; height: 34px; x: (parent.width - self.width) / 2; y: 222px; } - } - - if tab == "settings": Rectangle { - x: root.safe-area-insets.left; y: root.safe-area-insets.top; - width: root.width - root.safe-area-insets.left - root.safe-area-insets.right; - height: root.height - root.safe-area-insets.top - root.safe-area-insets.bottom - root.toolbar-height; - background: AppTheme.page; - Header { - x: 0; y: 0; can_back: false; - title: "Settings"; - } - Rectangle { - x: 16px; y: 76px; width: parent.width - 32px; height: 184px; - background: AppTheme.surface; border-radius: 12px; - Text { x: 15px; y: 13px; text: "Icon style"; font-size: 18px; font-weight: 650; color: AppTheme.text; } - Text { x: 15px; y: 43px; width: parent.width - 30px; text: "Choose how navigation and activity icons look throughout Gotcha."; font-size: 13px; color: AppTheme.text-secondary; wrap: word-wrap; } - IconStyleChoice { x: 12px; y: 91px; width: (parent.width - 36px) / 3; height: 78px; value: 0; label: "Outline"; clicked => { root.choose_icon_style(0); } } - IconStyleChoice { x: 18px + (parent.width - 36px) / 3; y: 91px; width: (parent.width - 36px) / 3; height: 78px; value: 1; label: "Duotone"; clicked => { root.choose_icon_style(1); } } - IconStyleChoice { x: 24px + 2 * (parent.width - 36px) / 3; y: 91px; width: (parent.width - 36px) / 3; height: 78px; value: 2; label: "Bold"; clicked => { root.choose_icon_style(2); } } - } - Rectangle { - x: 16px; y: 276px; width: parent.width - 32px; height: 136px; - background: AppTheme.surface; border-radius: 12px; - Text { x: 15px; y: 13px; text: "Appearance"; font-size: 18px; font-weight: 650; color: AppTheme.text; } - Text { x: 15px; y: 43px; width: parent.width - 30px; text: "Follow iOS automatically or choose a fixed light or dark appearance."; font-size: 13px; color: AppTheme.text-secondary; wrap: word-wrap; } - AppearanceChoice { x: 12px; y: 81px; width: (parent.width - 36px) / 3; height: 42px; value: 0; label: "Auto"; clicked => { root.choose_appearance(0); } } - AppearanceChoice { x: 18px + (parent.width - 36px) / 3; y: 81px; width: (parent.width - 36px) / 3; height: 42px; value: 1; label: "Light"; clicked => { root.choose_appearance(1); } } - AppearanceChoice { x: 24px + 2 * (parent.width - 36px) / 3; y: 81px; width: (parent.width - 36px) / 3; height: 42px; value: 2; label: "Dark"; clicked => { root.choose_appearance(2); } } - } - } - - if (tab == "issues" || tab == "repositories") && page == "servers": Rectangle { - x: root.safe-area-insets.left; y: root.safe-area-insets.top; - width: root.width - root.safe-area-insets.left - root.safe-area-insets.right; - height: root.height - root.safe-area-insets.top - root.safe-area-insets.bottom - root.toolbar-height; - background: AppTheme.page; - Header { x: 0; y: 0; title: "Servers"; can_back: false; } - if root.servers.length > 0: servers_list := ListView { - property scroll_request: root.servers_scroll_request; - x: 16px; y: 76px; width: parent.width - 32px; height: parent.height - 154px; - mouse-drag-pan-enabled: true; - changed scroll_request => { self.viewport-y = 0px; } - for server[index] in root.servers: Rectangle { - height: 76px; - background: AppTheme.surface; - border-color: AppTheme.border; - border-width: 1px; - border-radius: 12px; - TouchArea { clicked => { root.select_server(index); } } - Text { x: 16px; y: 13px; width: parent.width - 54px; text: server.name; font-size: 17px; font-weight: 600; color: AppTheme.text; overflow: elide; } - Text { x: 16px; y: 42px; width: parent.width - 54px; text: server.url; font-size: 13px; color: AppTheme.text-secondary; overflow: elide; } - AppIcon { x: parent.width - 29px; y: 26px; width: 20px; height: 20px; kind: "disclosure"; icon-color: AppTheme.text-tertiary; } - } - } - Button { x: 16px; y: parent.height - 66px; width: parent.width - 32px; height: 48px; text: "Add Server"; clicked => { root.show_add_server(); } } - } - - if (tab == "issues" || tab == "repositories") && page == "add": Rectangle { - x: root.safe-area-insets.left; y: root.safe-area-insets.top; - width: root.width - root.safe-area-insets.left - root.safe-area-insets.right; - height: root.height - root.safe-area-insets.top - root.safe-area-insets.bottom - root.toolbar-height; - background: AppTheme.page; - Header { x: 0; y: 0; title: "Add Server"; can_back: true; back => { root.back(); } } - VerticalLayout { - x: 20px; y: 92px; width: parent.width - 40px; height: 260px; spacing: 10px; - Text { text: "Name"; font-size: 13px; color: AppTheme.text-secondary; } - server_name := LineEdit { placeholder-text: "Work"; } - Text { text: "Server URL"; font-size: 13px; color: AppTheme.text-secondary; } - server_url := LineEdit { placeholder-text: "https://gitea.example.com"; } - Text { text: "Access token"; font-size: 13px; color: AppTheme.text-secondary; } - server_token := LineEdit { placeholder-text: "Token"; input-type: InputType.password; } - } - Button { - x: 20px; y: 380px; width: parent.width - 40px; height: 48px; - text: root.loading ? "Connecting…" : "Add Server"; - enabled: !root.loading; - clicked => { root.save_server(server_name.text, server_url.text, server_token.text); } - } - EdgeBack { x: 0; y: 0; back => { root.back(); } } - } - - if (tab == "issues" || tab == "repositories") && page == "repositories": Rectangle { - x: root.safe-area-insets.left; y: root.safe-area-insets.top; - width: root.width - root.safe-area-insets.left - root.safe-area-insets.right; - height: root.height - root.safe-area-insets.top - root.safe-area-insets.bottom - root.toolbar-height; - background: AppTheme.page; - Header { x: 0; y: 0; title: root.server_title; can_back: true; back => { root.back(); } } - if root.refreshing: Spinner { x: parent.width - 42px; y: 17px; width: 24px; height: 24px; } - if !root.loading && root.repositories.length == 0: EmptyState { - x: 34px; width: parent.width - 68px; y: 220px; height: 160px; - title: "No repositories"; - detail: "This account does not own any repositories on this server."; - } - PullToRefreshGesture { - x: 20px; y: 70px; width: parent.width - 40px; height: parent.height - 82px; - at_top: root.repositories_at_top; refreshing: root.refreshing; - refresh => { root.refresh_repositories(); } - repositories_list := ListView { - property scroll_request: root.repositories_scroll_request; - width: 100%; height: 100%; mouse-drag-pan-enabled: true; - changed scroll_request => { self.viewport-y = 0px; root.repositories_at_top = true; } - scrolled => { root.repositories_at_top = self.viewport-y >= 0px; } - for repo in root.repositories: Rectangle { - height: 104px; - background: AppTheme.surface; - border-color: AppTheme.border; - border-width: 1px; - border-radius: 12px; - TouchArea { clicked => { root.select_repository(repo.owner, repo.name); } } - Text { x: 15px; y: 11px; width: parent.width - 62px; text: repo.name; font-size: 17px; font-weight: 600; color: AppTheme.text; overflow: elide; } - Text { x: 15px; y: 39px; width: parent.width - 30px; text: repo.description; font-size: 14px; color: AppTheme.text-secondary; overflow: elide; } - Text { x: 15px; y: 71px; width: parent.width - 30px; text: repo.meta; font-size: 12px; color: AppTheme.text-tertiary; overflow: elide; } - TouchArea { - x: parent.width - 52px; y: 2px; width: 48px; height: 48px; - clicked => { root.toggle_favorite(repo.owner, repo.name); } - AppIcon { x: 10px; y: 10px; width: 28px; height: 28px; kind: repo.favorite ? "favorite-filled" : "favorite"; icon-color: repo.favorite ? #f2a900 : AppTheme.text-tertiary; } - } - } - } - } - EdgeBack { x: 0; y: 0; back => { root.back(); } } - } - - if tab == "issues" && page == "issues": Rectangle { - x: root.safe-area-insets.left; y: root.safe-area-insets.top; - width: root.width - root.safe-area-insets.left - root.safe-area-insets.right; - height: root.height - root.safe-area-insets.top - root.safe-area-insets.bottom - root.toolbar-height; - background: AppTheme.page; - Header { - x: 0; y: 0; title: root.repository_title; can_back: true; can_filter: true; - back => { root.back(); } - filter => { root.filter_open = true; } - } - if root.refreshing: Spinner { x: parent.width - 42px; y: 17px; width: 24px; height: 24px; } - if !root.loading && root.issues.length == 0: EmptyState { - x: 34px; width: parent.width - 68px; y: 220px; height: 160px; - title: "No " + root.issue_filter + " issues"; - detail: "No issues match the selected status."; - } - PullToRefreshGesture { - x: 20px; y: 70px; width: parent.width - 40px; height: parent.height - 82px; - at_top: root.issues_at_top; refreshing: root.refreshing; - refresh => { root.refresh_issues(); } - issues_list := ListView { - property scroll_request: root.issues_scroll_request; - width: 100%; height: 100%; mouse-drag-pan-enabled: true; - changed scroll_request => { self.viewport-y = 0px; root.issues_at_top = true; } - scrolled => { root.issues_at_top = self.viewport-y >= 0px; } - for issue in root.issues: Rectangle { - height: issue.labels.length > 0 ? 138px : 112px; - background: AppTheme.surface; - border-color: AppTheme.border; - border-width: 1px; - border-radius: 12px; - TouchArea { clicked => { root.select_issue(issue.number); } } - Text { x: 15px; y: 11px; width: parent.width - 44px; text: issue.title; font-size: 16px; font-weight: 600; color: AppTheme.text; overflow: elide; } - Text { x: 15px; y: 39px; width: parent.width - 30px; height: 38px; text: issue.summary; font-size: 13px; color: AppTheme.text-secondary; wrap: word-wrap; overflow: elide; } - if issue.labels.length > 0: HorizontalLayout { - x: 15px; y: 82px; width: parent.width - 30px; height: 22px; spacing: 5px; - for label in issue.labels: Rectangle { - width: min(104px, max(48px, (parent.width - (issue.labels.length - 1) * 5px) / issue.labels.length)); - height: 22px; background: label.background; border-radius: 11px; - Text { x: 7px; width: parent.width - 14px; text: label.name; color: label.foreground; font-size: 11px; font-weight: 600; horizontal-alignment: center; vertical-alignment: center; overflow: elide; } - } - } - Text { x: 15px; y: issue.labels.length > 0 ? 112px : 86px; width: parent.width - 30px; text: issue.meta; font-size: 12px; color: AppTheme.text-tertiary; overflow: elide; } - } - } - } - EdgeBack { x: 0; y: 0; back => { root.back(); } } - } - - if tab == "issues" && page == "issue": Rectangle { - x: root.safe-area-insets.left; y: root.safe-area-insets.top; - width: root.width - root.safe-area-insets.left - root.safe-area-insets.right; - height: root.height - root.safe-area-insets.top - root.safe-area-insets.bottom - root.toolbar-height; - background: AppTheme.page; - Header { x: 0; y: 0; title: "Issue"; can_back: true; back => { root.back(); } } - if root.refreshing: Spinner { x: parent.width - 42px; y: 17px; width: 24px; height: 24px; } - PullToRefreshGesture { - x: 18px; y: 78px; width: parent.width - 36px; height: parent.height - 96px; - at_top: root.issue_at_top; refreshing: root.refreshing; - refresh => { root.refresh_issue(); } - issue_scroll := ScrollView { - property scroll_request: root.issue_scroll_request; - width: 100%; height: 100%; mouse-drag-pan-enabled: true; - changed scroll_request => { self.viewport-y = 0px; root.issue_at_top = true; } - scrolled => { root.issue_at_top = self.viewport-y >= 0px; } - VerticalLayout { - width: parent.width; - spacing: 12px; - alignment: start; - Text { text: root.issue_title; font-size: 23px; font-weight: 650; color: AppTheme.text; wrap: word-wrap; } - Text { text: root.issue_meta; font-size: 13px; color: AppTheme.text-secondary; wrap: word-wrap; } - Rectangle { height: 1px; background: AppTheme.separator; } - MarkdownContent { width: parent.width; blocks: root.issue_body; } - if root.comments.length > 0: Text { text: "Comments"; font-size: 17px; font-weight: 600; color: AppTheme.text; } - for comment in root.comments: CommentCard { width: parent.width; comment: comment; } - } - } - } - EdgeBack { x: 0; y: 0; back => { root.back(); } } - } - - if tab == "pulls" && !root.has_active_server: Rectangle { - x: root.safe-area-insets.left; y: root.safe-area-insets.top; - width: root.width - root.safe-area-insets.left - root.safe-area-insets.right; - height: root.height - root.safe-area-insets.top - root.safe-area-insets.bottom - root.toolbar-height; - background: AppTheme.page; - Header { x: 0; y: 0; title: "Home"; can_back: false; } - EmptyState { - x: 32px; width: parent.width - 64px; y: 210px; height: 190px; - title: "No server selected"; - detail: "Open Issues or Repositories to select a server or add your first one."; - } - } - - if tab == "pulls" && root.has_active_server && page == "pulls": Rectangle { - x: root.safe-area-insets.left; y: root.safe-area-insets.top; - width: root.width - root.safe-area-insets.left - root.safe-area-insets.right; - height: root.height - root.safe-area-insets.top - root.safe-area-insets.bottom - root.toolbar-height; - background: AppTheme.page; - Header { - x: 0; y: 0; title: "Pull Requests"; can_back: false; can_filter: true; - filter => { root.filter_open = true; } - } - if root.refreshing: Spinner { x: parent.width - 92px; y: 17px; width: 24px; height: 24px; } - if !root.loading && root.pulls.length == 0: EmptyState { - x: 34px; width: parent.width - 68px; y: 220px; height: 160px; - title: "No " + root.pull_filter + " pull requests"; - detail: "No pull requests match the selected status."; - } - PullToRefreshGesture { - x: 16px; y: 70px; width: parent.width - 32px; height: parent.height - 82px; - at_top: root.issues_at_top; refreshing: root.refreshing; - refresh => { root.refresh_pulls(); } - ListView { - width: 100%; height: 100%; mouse-drag-pan-enabled: true; - scrolled => { root.issues_at_top = self.viewport-y >= 0px; } - for pull in root.pulls: Rectangle { - height: 112px; - background: AppTheme.surface; border-color: AppTheme.border; border-width: 1px; border-radius: 12px; - TouchArea { clicked => { root.select_pull(pull.owner, pull.repository, pull.number); } } - Text { x: 14px; y: 10px; width: parent.width - 28px; text: pull.repository + " #" + pull.number; font-size: 12px; color: AppTheme.text-secondary; overflow: elide; } - Text { x: 14px; y: 31px; width: parent.width - 28px; text: pull.title; font-size: 16px; font-weight: 600; color: AppTheme.text; overflow: elide; } - Text { x: 14px; y: 57px; width: parent.width - 28px; text: pull.summary; font-size: 13px; color: AppTheme.text-secondary; overflow: elide; } - Text { x: 14px; y: 85px; width: parent.width - 28px; text: pull.meta; font-size: 12px; color: AppTheme.text-tertiary; overflow: elide; } - } - } - } - } - - if tab == "pulls" && page == "pull": Rectangle { - x: root.safe-area-insets.left; y: root.safe-area-insets.top; - width: root.width - root.safe-area-insets.left - root.safe-area-insets.right; - height: root.height - root.safe-area-insets.top - root.safe-area-insets.bottom - root.toolbar-height; - background: AppTheme.page; - Header { x: 0; y: 0; title: "Pull Request"; can_back: true; back => { root.back(); } } - if root.refreshing: Spinner { x: parent.width - 42px; y: 17px; width: 24px; height: 24px; } - PullToRefreshGesture { - x: 18px; y: 78px; width: parent.width - 36px; height: parent.height - 96px; - at_top: root.issue_at_top; refreshing: root.refreshing; - refresh => { root.refresh_pull(); } - ScrollView { - width: 100%; height: 100%; mouse-drag-pan-enabled: true; - scrolled => { root.issue_at_top = self.viewport-y >= 0px; } - VerticalLayout { - width: parent.width; spacing: 12px; alignment: start; - Text { text: root.detail_title; font-size: 23px; font-weight: 650; color: AppTheme.text; wrap: word-wrap; } - Text { text: root.detail_meta; font-size: 13px; color: AppTheme.text-secondary; wrap: word-wrap; } - Rectangle { height: 1px; background: AppTheme.separator; } - MarkdownContent { width: parent.width; blocks: root.detail_body; } - if root.files.length > 0: Text { text: root.detail_files_ref; font-size: 17px; font-weight: 600; color: AppTheme.text; } - for file in root.files: FileCard { width: parent.width; file: file; clicked => { root.select_pull_file(file.path); } } - if root.comments.length > 0: Text { text: "Comments"; font-size: 17px; font-weight: 600; color: AppTheme.text; } - for comment in root.comments: CommentCard { width: parent.width; comment: comment; } - } - } - } - EdgeBack { x: 0; y: 0; back => { root.back(); } } - } - - if tab == "repositories" && page == "commits": Rectangle { - x: root.safe-area-insets.left; y: root.safe-area-insets.top; - width: root.width - root.safe-area-insets.left - root.safe-area-insets.right; - height: root.height - root.safe-area-insets.top - root.safe-area-insets.bottom - root.toolbar-height; - background: AppTheme.page; - Header { x: 0; y: 0; title: root.repository_title; can_back: true; back => { root.back(); } } - ComboBox { - x: 16px; y: 66px; width: parent.width - 32px; height: 42px; - model: root.branch_choices; current-index: root.branch_index; - selected(value) => { root.select_branch(value); } - } - if root.refreshing: Spinner { x: parent.width - 42px; y: 17px; width: 24px; height: 24px; } - if !root.loading && root.commits.length == 0: EmptyState { - x: 34px; width: parent.width - 68px; y: 220px; height: 160px; - title: "No commits"; - detail: "This repository has no commit history."; - } - PullToRefreshGesture { - x: 16px; y: 116px; width: parent.width - 32px; height: parent.height - 128px; - at_top: root.repositories_at_top; refreshing: root.refreshing; - refresh => { root.refresh_commits(); } - ListView { - width: 100%; height: 100%; mouse-drag-pan-enabled: true; - scrolled => { root.repositories_at_top = self.viewport-y >= 0px; } - for commit in root.commits: commit_row := Rectangle { - property graph-width: commit.lanes.length > 0 ? min(72px, commit.lanes.length * 10px + 8px) : 0px; - property lane-spacing: (self.graph-width - 8px) / max(1, commit.lanes.length); - height: 86px; background: transparent; - Rectangle { width: 100%; height: 100%; background: AppTheme.surface; border-color: AppTheme.border; border-width: 1px; border-radius: 12px; } - TouchArea { clicked => { root.select_commit(commit.sha); } } - for lane[index] in commit.lanes: Rectangle { - x: 4px + index * commit_row.lane-spacing; - width: 3px; height: 100%; background: transparent; - if lane.top: Rectangle { - y: -1px; width: 3px; - height: lane.connect_left && !lane.bottom ? 37px : 42px; - background: lane.color; - } - if lane.bottom: Rectangle { - y: lane.connect_left && !lane.top ? 45px : 40px; - width: 3px; - height: parent.height - (lane.connect_left && !lane.top ? 44px : 39px); - background: lane.color; - } - if lane.connect_left && lane.top == lane.bottom: Rectangle { - x: -commit_row.lane-spacing; y: 39px; - width: commit_row.lane-spacing + 3px; height: 3px; - background: lane.color; - } - if lane.connect_left && lane.bottom && !lane.top: Path { - x: 1.5px - commit_row.lane-spacing; y: 35px; - width: commit_row.lane-spacing; height: 11px; - viewbox-width: 10; viewbox-height: 11; - commands: "M 0 5.5 L 5 5.5 Q 10 5.5 10 10.5"; - fill: transparent; stroke: lane.color; stroke-width: 3px; stroke-line-cap: round; - } - if lane.connect_left && lane.top && !lane.bottom: Path { - x: 1.5px - commit_row.lane-spacing; y: 35px; - width: commit_row.lane-spacing; height: 11px; - viewbox-width: 10; viewbox-height: 11; - commands: "M 10 0.5 Q 10 5.5 5 5.5 L 0 5.5"; - fill: transparent; stroke: lane.color; stroke-width: 3px; stroke-line-cap: round; - } - if lane.node: Rectangle { - x: -3px; y: 36px; width: 9px; height: 9px; border-radius: 5px; - background: lane.color; border-color: AppTheme.surface; border-width: 2px; - } - } - Text { x: parent.graph-width + 14px; y: 8px; width: parent.width - self.x - 14px; text: commit.title; font-size: 15px; font-weight: 600; color: AppTheme.text; overflow: elide; } - Text { x: parent.graph-width + 14px; y: 33px; width: parent.width - self.x - 14px; text: commit.refs != "" ? commit.refs : commit.meta; font-size: 12px; color: commit.refs != "" ? AppTheme.accent : AppTheme.text-secondary; overflow: elide; } - Text { x: parent.graph-width + 14px; y: 53px; width: parent.width - self.x - 14px; text: commit.refs != "" ? commit.meta + " · " + commit.sha : commit.sha; font-size: 11px; color: AppTheme.text-tertiary; overflow: elide; } - } - } - } - EdgeBack { x: 0; y: 0; back => { root.back(); } } - } - - if tab == "repositories" && page == "files": Rectangle { - x: root.safe-area-insets.left; y: root.safe-area-insets.top; - width: root.width - root.safe-area-insets.left - root.safe-area-insets.right; - height: root.height - root.safe-area-insets.top - root.safe-area-insets.bottom - root.toolbar-height; - background: AppTheme.page; - Header { x: 0; y: 0; title: "Changed Files"; can_back: true; back => { root.back(); } } - if !root.loading && root.files.length == 0: EmptyState { - x: 34px; width: parent.width - 68px; y: 220px; height: 160px; - title: "No changed files"; - detail: "This commit does not contain file changes."; - } - ListView { - x: 16px; y: 70px; width: parent.width - 32px; height: parent.height - 82px; mouse-drag-pan-enabled: true; - for file in root.files: FileCard { file: file; clicked => { root.select_file(file.path); } } - } - EdgeBack { x: 0; y: 0; back => { root.back(); } } - } - - if (tab == "repositories" || tab == "pulls") && page == "diff": Rectangle { - x: root.safe-area-insets.left; y: root.safe-area-insets.top; - width: root.width - root.safe-area-insets.left - root.safe-area-insets.right; - height: root.height - root.safe-area-insets.top - root.safe-area-insets.bottom - root.toolbar-height; - background: AppTheme.page; - Header { x: 0; y: 0; title: root.detail_title; can_back: true; back => { root.back(); } } - ScrollView { - x: 12px; y: 70px; width: parent.width - 24px; height: parent.height - 82px; mouse-drag-pan-enabled: true; - viewport-width: max(self.visible-width, root.diff_columns * 7px + 100px); - viewport-height: diff_rows.preferred-height; - diff_rows := VerticalLayout { - width: parent.viewport-width; - for line in root.diff_lines: Rectangle { - height: 20px; - background: line.kind == "addition" ? (AppTheme.dark ? #173d25 : #ddf5e3) : line.kind == "removal" ? (AppTheme.dark ? #4a2020 : #fde2e1) : line.kind == "hunk" ? (AppTheme.dark ? #17324d : #dcecff) : line.kind == "header" ? AppTheme.surface-muted : AppTheme.surface; - Text { x: 0; width: 38px; text: line.old_number; font-family: "monospace"; font-size: 11px; color: AppTheme.text-tertiary; horizontal-alignment: right; vertical-alignment: center; } - Text { x: 42px; width: 38px; text: line.new_number; font-family: "monospace"; font-size: 11px; color: AppTheme.text-tertiary; horizontal-alignment: right; vertical-alignment: center; } - Rectangle { x: 84px; width: 1px; height: 100%; background: AppTheme.separator; } - Text { - x: 90px; width: parent.width - 94px; text: line.text; font-family: "monospace"; font-size: 11px; - color: line.kind == "addition" ? (AppTheme.dark ? #7fe787 : #176b2c) : line.kind == "removal" ? (AppTheme.dark ? #ff7b72 : #a12720) : line.kind == "hunk" ? (AppTheme.dark ? #79c0ff : #175b9e) : AppTheme.text; - wrap: no-wrap; overflow: clip; vertical-alignment: center; - } - } - } - } - EdgeBack { x: 0; y: 0; back => { root.back(); } } - } - - Rectangle { - x: root.safe-area-insets.left; - y: root.height - root.safe-area-insets.bottom - root.toolbar-height; - width: root.width - root.safe-area-insets.left - root.safe-area-insets.right; - height: root.toolbar-height; - background: AppTheme.toolbar; - TabButton { - x: 0; width: parent.width / 5; height: parent.height; kind: "home"; label: "Home"; active: root.tab == "home"; - clicked => { - if root.tab == "home" { root.home_scroll_request += 1; } - root.select_tab("home"); - } - } - TabButton { - x: parent.width / 5; width: parent.width / 5; height: parent.height; kind: "issues"; label: "Issues"; active: root.tab == "issues"; - clicked => { - if root.tab == "issues" { - if root.page == "servers" { root.servers_scroll_request += 1; } - if root.page == "repositories" { root.repositories_scroll_request += 1; } - if root.page == "issues" { root.issues_scroll_request += 1; } - if root.page == "issue" { root.issue_scroll_request += 1; } - } - root.select_tab("issues"); - } - } - TabButton { x: parent.width * 2 / 5; width: parent.width / 5; height: parent.height; kind: "repositories"; label: "Repos"; active: root.tab == "repositories"; clicked => { root.select_tab("repositories"); } } - TabButton { x: parent.width * 3 / 5; width: parent.width / 5; height: parent.height; kind: "pulls"; label: "Pulls"; active: root.tab == "pulls"; clicked => { root.select_tab("pulls"); } } - TabButton { x: parent.width * 4 / 5; width: parent.width / 5; height: parent.height; kind: "settings"; label: "Settings"; active: root.tab == "settings"; clicked => { root.select_tab("settings"); } } - } - - if root.loading && (root.tab == "issues" || root.tab == "repositories" || root.tab == "pulls"): Rectangle { - width: 100%; height: 100%; - background: AppTheme.loading-overlay; - TouchArea { } - Spinner { width: 38px; height: 38px; x: (parent.width - self.width) / 2; y: (parent.height - self.height) / 2; } - } - if root.filter_open: Rectangle { - width: 100%; height: 100%; background: #00000066; - TouchArea { clicked => { root.filter_open = false; } } - Rectangle { - x: 28px; y: (parent.height - 190px) / 2; width: parent.width - 56px; height: 190px; - background: AppTheme.surface; border-radius: 14px; - Text { x: 18px; y: 16px; width: parent.width - 36px; text: "Status"; font-size: 19px; font-weight: 650; color: AppTheme.text; } - VerticalLayout { - x: 18px; y: 54px; width: parent.width - 36px; height: 118px; spacing: 8px; - Button { text: "Open"; clicked => { root.choose_filter("open"); root.filter_open = false; } } - Button { text: "Closed"; clicked => { root.choose_filter("closed"); root.filter_open = false; } } - } - } - } - if root.error != "": Rectangle { - x: 14px; y: parent.height - root.safe-area-insets.bottom - 132px; width: parent.width - 28px; height: 66px; - background: #d93d35; border-radius: 12px; - Text { x: 14px; width: parent.width - 28px; text: root.error; color: white; font-size: 13px; wrap: word-wrap; vertical-alignment: center; } - TouchArea { clicked => { root.error = ""; } } - } - } -} +slint::include_modules!(); diff --git a/crates/app/ui/app.slint b/crates/app/ui/app.slint new file mode 100644 index 0000000..a1370cf --- /dev/null +++ b/crates/app/ui/app.slint @@ -0,0 +1,614 @@ +import { Button, ComboBox, LineEdit, ListView, ScrollView, Spinner } from "std-widgets.slint"; +export { + ActivityRow, CommentRow, CommitRow, DiffLine, FileRow, GraphLane, HeatCell, IssueRow, + LabelRow, MarkdownBlock, PullRow, RepositoryRow, ServerRow, +} from "types.slint"; +import { + ActivityRow, CommentRow, CommitRow, DiffLine, FileRow, GraphLane, HeatCell, IssueRow, + MarkdownBlock, PullRow, RepositoryRow, ServerRow, +} from "types.slint"; +export { AppTheme, IconTheme } from "components.slint"; +import { + AppearanceChoice, AppIcon, AppTheme, CommentCard, EdgeBack, EmptyState, FileCard, Header, + IconStyleChoice, MarkdownContent, PullToRefreshGesture, TabButton, +} from "components.slint"; + +export component AppWindow inherits Window { + title: "Gotcha"; + preferred-width: 390px; + preferred-height: 844px; + background: AppTheme.page; + private property toolbar-height: 68px; + + in-out property tab: "home"; + in-out property page: "servers"; + in-out property <[ServerRow]> servers; + in-out property <[RepositoryRow]> repositories; + in-out property <[IssueRow]> issues; + in-out property <[PullRow]> pulls; + in-out property <[CommentRow]> comments; + in-out property <[CommitRow]> commits; + in-out property <[FileRow]> files; + in-out property <[ActivityRow]> activities; + in-out property <[HeatCell]> heat_cells; + in-out property contribution_count; + in-out property has_active_server; + in-out property home_server; + in-out property server_title; + in-out property repository_title; + in-out property issue_title; + in-out property issue_meta; + in-out property <[MarkdownBlock]> issue_body; + in-out property detail_title; + in-out property detail_meta; + in-out property detail_files_ref; + in-out property <[MarkdownBlock]> detail_body; + in-out property <[DiffLine]> diff_lines; + in-out property diff_columns; + in-out property issue_filter: "open"; + in-out property pull_filter: "open"; + in-out property <[string]> branch_choices; + in-out property branch_index; + in-out property filter_open; + in-out property loading; + in-out property home_loading; + in-out property refreshing; + in-out property error; + in-out property home_scroll_request; + in-out property servers_scroll_request; + in-out property repositories_scroll_request; + in-out property issues_scroll_request; + in-out property issue_scroll_request; + private property home_at_top: true; + private property repositories_at_top: true; + private property issues_at_top: true; + private property issue_at_top: true; + + callback select_tab(string); + callback refresh_home(); + callback refresh_repositories(); + callback refresh_issues(); + callback refresh_issue(); + callback refresh_pulls(); + callback refresh_pull(); + callback refresh_commits(); + callback show_add_server(); + callback save_server(string, string, string); + callback select_server(int); + callback select_repository(string, string); + callback toggle_favorite(string, string); + callback select_issue(int); + callback select_pull(string, string, int); + callback select_commit(string); + callback select_branch(string); + callback select_file(string); + callback select_pull_file(string); + callback select_activity(string, string, string, int, string); + callback choose_filter(string); + callback choose_icon_style(int); + callback choose_appearance(int); + callback back(); + + if tab == "home": Rectangle { + x: root.safe-area-insets.left; y: root.safe-area-insets.top; + width: root.width - root.safe-area-insets.left - root.safe-area-insets.right; + height: root.height - root.safe-area-insets.top - root.safe-area-insets.bottom - root.toolbar-height; + background: AppTheme.page; + Header { x: 0; y: 0; title: root.has_active_server ? root.home_server : "Home"; can_back: false; } + + if !root.has_active_server: EmptyState { + x: 32px; width: parent.width - 64px; y: 210px; height: 190px; + title: "No server selected"; + detail: "Open Issues to select a server or add your first one."; + } + + if root.has_active_server: Rectangle { + x: 16px; y: 72px; width: parent.width - 32px; height: 118px; + background: AppTheme.surface; border-radius: 12px; + Text { x: 13px; y: 10px; text: "Activity · last 12 months"; font-size: 13px; font-weight: 600; color: AppTheme.text; } + Text { x: 13px; y: 86px; text: root.contribution_count + " contributions"; font-size: 12px; color: AppTheme.text-secondary; } + for cell in root.heat_cells: Rectangle { + x: 13px + cell.week * 6.7px; y: 39px + cell.day * 6.2px; + width: 5.2px; height: 5.2px; border-radius: 1px; + background: cell.level == 0 ? (AppTheme.dark ? #3a3a3c : #e5e5e9) : cell.level == 1 ? #b8d9f4 : cell.level == 2 ? #72b5e8 : cell.level == 3 ? #278bd4 : #0969b7; + } + } + + if root.has_active_server && !root.home_loading && root.activities.length == 0: EmptyState { + x: 32px; width: parent.width - 64px; y: 250px; height: 150px; + title: "No recent activity"; + detail: "Your recent server actions will appear here."; + } + + if root.has_active_server: PullToRefreshGesture { + x: 16px; y: 202px; width: parent.width - 32px; height: parent.height - 214px; + at_top: root.home_at_top; refreshing: root.home_loading; + refresh => { root.refresh_home(); } + home_list := ListView { + property scroll_request: root.home_scroll_request; + width: 100%; height: 100%; mouse-drag-pan-enabled: true; + changed scroll_request => { self.viewport-y = 0px; root.home_at_top = true; } + scrolled => { root.home_at_top = self.viewport-y >= 0px; } + for activity in root.activities: Rectangle { + height: 94px; + background: AppTheme.surface; border-radius: 12px; + TouchArea { + enabled: activity.target != ""; + clicked => { root.select_activity(activity.target, activity.owner, activity.repository, activity.number, activity.sha); } + } + Rectangle { + x: 12px; y: 14px; width: 34px; height: 34px; border-radius: 17px; + background: AppTheme.accent-soft; + AppIcon { x: 5px; y: 5px; width: 24px; height: 24px; kind: activity.icon; } + } + Text { x: 57px; y: 11px; width: parent.width - 69px; height: 24px; text: activity.title; font-size: 15px; font-weight: 600; color: AppTheme.text; overflow: elide; } + Text { x: 57px; y: 36px; width: parent.width - 69px; height: 35px; text: activity.detail; font-size: 13px; color: AppTheme.text-secondary; wrap: word-wrap; overflow: elide; } + Text { x: 57px; y: 72px; width: parent.width - 69px; text: activity.meta; font-size: 11px; color: AppTheme.text-tertiary; overflow: elide; } + } + } + } + + if root.home_loading: Spinner { width: 34px; height: 34px; x: (parent.width - self.width) / 2; y: 222px; } + } + + if tab == "settings": Rectangle { + x: root.safe-area-insets.left; y: root.safe-area-insets.top; + width: root.width - root.safe-area-insets.left - root.safe-area-insets.right; + height: root.height - root.safe-area-insets.top - root.safe-area-insets.bottom - root.toolbar-height; + background: AppTheme.page; + Header { + x: 0; y: 0; can_back: false; + title: "Settings"; + } + Rectangle { + x: 16px; y: 76px; width: parent.width - 32px; height: 184px; + background: AppTheme.surface; border-radius: 12px; + Text { x: 15px; y: 13px; text: "Icon style"; font-size: 18px; font-weight: 650; color: AppTheme.text; } + Text { x: 15px; y: 43px; width: parent.width - 30px; text: "Choose how navigation and activity icons look throughout Gotcha."; font-size: 13px; color: AppTheme.text-secondary; wrap: word-wrap; } + IconStyleChoice { x: 12px; y: 91px; width: (parent.width - 36px) / 3; height: 78px; value: 0; label: "Outline"; clicked => { root.choose_icon_style(0); } } + IconStyleChoice { x: 18px + (parent.width - 36px) / 3; y: 91px; width: (parent.width - 36px) / 3; height: 78px; value: 1; label: "Duotone"; clicked => { root.choose_icon_style(1); } } + IconStyleChoice { x: 24px + 2 * (parent.width - 36px) / 3; y: 91px; width: (parent.width - 36px) / 3; height: 78px; value: 2; label: "Bold"; clicked => { root.choose_icon_style(2); } } + } + Rectangle { + x: 16px; y: 276px; width: parent.width - 32px; height: 136px; + background: AppTheme.surface; border-radius: 12px; + Text { x: 15px; y: 13px; text: "Appearance"; font-size: 18px; font-weight: 650; color: AppTheme.text; } + Text { x: 15px; y: 43px; width: parent.width - 30px; text: "Follow iOS automatically or choose a fixed light or dark appearance."; font-size: 13px; color: AppTheme.text-secondary; wrap: word-wrap; } + AppearanceChoice { x: 12px; y: 81px; width: (parent.width - 36px) / 3; height: 42px; value: 0; label: "Auto"; clicked => { root.choose_appearance(0); } } + AppearanceChoice { x: 18px + (parent.width - 36px) / 3; y: 81px; width: (parent.width - 36px) / 3; height: 42px; value: 1; label: "Light"; clicked => { root.choose_appearance(1); } } + AppearanceChoice { x: 24px + 2 * (parent.width - 36px) / 3; y: 81px; width: (parent.width - 36px) / 3; height: 42px; value: 2; label: "Dark"; clicked => { root.choose_appearance(2); } } + } + } + + if (tab == "issues" || tab == "repositories") && page == "servers": Rectangle { + x: root.safe-area-insets.left; y: root.safe-area-insets.top; + width: root.width - root.safe-area-insets.left - root.safe-area-insets.right; + height: root.height - root.safe-area-insets.top - root.safe-area-insets.bottom - root.toolbar-height; + background: AppTheme.page; + Header { x: 0; y: 0; title: "Servers"; can_back: false; } + if root.servers.length > 0: servers_list := ListView { + property scroll_request: root.servers_scroll_request; + x: 16px; y: 76px; width: parent.width - 32px; height: parent.height - 154px; + mouse-drag-pan-enabled: true; + changed scroll_request => { self.viewport-y = 0px; } + for server[index] in root.servers: Rectangle { + height: 76px; + background: AppTheme.surface; + border-color: AppTheme.border; + border-width: 1px; + border-radius: 12px; + TouchArea { clicked => { root.select_server(index); } } + Text { x: 16px; y: 13px; width: parent.width - 54px; text: server.name; font-size: 17px; font-weight: 600; color: AppTheme.text; overflow: elide; } + Text { x: 16px; y: 42px; width: parent.width - 54px; text: server.url; font-size: 13px; color: AppTheme.text-secondary; overflow: elide; } + AppIcon { x: parent.width - 29px; y: 26px; width: 20px; height: 20px; kind: "disclosure"; icon-color: AppTheme.text-tertiary; } + } + } + Button { x: 16px; y: parent.height - 66px; width: parent.width - 32px; height: 48px; text: "Add Server"; clicked => { root.show_add_server(); } } + } + + if (tab == "issues" || tab == "repositories") && page == "add": Rectangle { + x: root.safe-area-insets.left; y: root.safe-area-insets.top; + width: root.width - root.safe-area-insets.left - root.safe-area-insets.right; + height: root.height - root.safe-area-insets.top - root.safe-area-insets.bottom - root.toolbar-height; + background: AppTheme.page; + Header { x: 0; y: 0; title: "Add Server"; can_back: true; back => { root.back(); } } + VerticalLayout { + x: 20px; y: 92px; width: parent.width - 40px; height: 260px; spacing: 10px; + Text { text: "Name"; font-size: 13px; color: AppTheme.text-secondary; } + server_name := LineEdit { placeholder-text: "Work"; } + Text { text: "Server URL"; font-size: 13px; color: AppTheme.text-secondary; } + server_url := LineEdit { placeholder-text: "https://gitea.example.com"; } + Text { text: "Access token"; font-size: 13px; color: AppTheme.text-secondary; } + server_token := LineEdit { placeholder-text: "Token"; input-type: InputType.password; } + } + Button { + x: 20px; y: 380px; width: parent.width - 40px; height: 48px; + text: root.loading ? "Connecting…" : "Add Server"; + enabled: !root.loading; + clicked => { root.save_server(server_name.text, server_url.text, server_token.text); } + } + EdgeBack { x: 0; y: 0; back => { root.back(); } } + } + + if (tab == "issues" || tab == "repositories") && page == "repositories": Rectangle { + x: root.safe-area-insets.left; y: root.safe-area-insets.top; + width: root.width - root.safe-area-insets.left - root.safe-area-insets.right; + height: root.height - root.safe-area-insets.top - root.safe-area-insets.bottom - root.toolbar-height; + background: AppTheme.page; + Header { x: 0; y: 0; title: root.server_title; can_back: true; back => { root.back(); } } + if root.refreshing: Spinner { x: parent.width - 42px; y: 17px; width: 24px; height: 24px; } + if !root.loading && root.repositories.length == 0: EmptyState { + x: 34px; width: parent.width - 68px; y: 220px; height: 160px; + title: "No repositories"; + detail: "This account does not own any repositories on this server."; + } + PullToRefreshGesture { + x: 20px; y: 70px; width: parent.width - 40px; height: parent.height - 82px; + at_top: root.repositories_at_top; refreshing: root.refreshing; + refresh => { root.refresh_repositories(); } + repositories_list := ListView { + property scroll_request: root.repositories_scroll_request; + width: 100%; height: 100%; mouse-drag-pan-enabled: true; + changed scroll_request => { self.viewport-y = 0px; root.repositories_at_top = true; } + scrolled => { root.repositories_at_top = self.viewport-y >= 0px; } + for repo in root.repositories: Rectangle { + height: 104px; + background: AppTheme.surface; + border-color: AppTheme.border; + border-width: 1px; + border-radius: 12px; + TouchArea { clicked => { root.select_repository(repo.owner, repo.name); } } + Text { x: 15px; y: 11px; width: parent.width - 62px; text: repo.name; font-size: 17px; font-weight: 600; color: AppTheme.text; overflow: elide; } + Text { x: 15px; y: 39px; width: parent.width - 30px; text: repo.description; font-size: 14px; color: AppTheme.text-secondary; overflow: elide; } + Text { x: 15px; y: 71px; width: parent.width - 30px; text: repo.meta; font-size: 12px; color: AppTheme.text-tertiary; overflow: elide; } + TouchArea { + x: parent.width - 52px; y: 2px; width: 48px; height: 48px; + clicked => { root.toggle_favorite(repo.owner, repo.name); } + AppIcon { x: 10px; y: 10px; width: 28px; height: 28px; kind: repo.favorite ? "favorite-filled" : "favorite"; icon-color: repo.favorite ? #f2a900 : AppTheme.text-tertiary; } + } + } + } + } + EdgeBack { x: 0; y: 0; back => { root.back(); } } + } + + if tab == "issues" && page == "issues": Rectangle { + x: root.safe-area-insets.left; y: root.safe-area-insets.top; + width: root.width - root.safe-area-insets.left - root.safe-area-insets.right; + height: root.height - root.safe-area-insets.top - root.safe-area-insets.bottom - root.toolbar-height; + background: AppTheme.page; + Header { + x: 0; y: 0; title: root.repository_title; can_back: true; can_filter: true; + back => { root.back(); } + filter => { root.filter_open = true; } + } + if root.refreshing: Spinner { x: parent.width - 42px; y: 17px; width: 24px; height: 24px; } + if !root.loading && root.issues.length == 0: EmptyState { + x: 34px; width: parent.width - 68px; y: 220px; height: 160px; + title: "No " + root.issue_filter + " issues"; + detail: "No issues match the selected status."; + } + PullToRefreshGesture { + x: 20px; y: 70px; width: parent.width - 40px; height: parent.height - 82px; + at_top: root.issues_at_top; refreshing: root.refreshing; + refresh => { root.refresh_issues(); } + issues_list := ListView { + property scroll_request: root.issues_scroll_request; + width: 100%; height: 100%; mouse-drag-pan-enabled: true; + changed scroll_request => { self.viewport-y = 0px; root.issues_at_top = true; } + scrolled => { root.issues_at_top = self.viewport-y >= 0px; } + for issue in root.issues: Rectangle { + height: issue.labels.length > 0 ? 138px : 112px; + background: AppTheme.surface; + border-color: AppTheme.border; + border-width: 1px; + border-radius: 12px; + TouchArea { clicked => { root.select_issue(issue.number); } } + Text { x: 15px; y: 11px; width: parent.width - 44px; text: issue.title; font-size: 16px; font-weight: 600; color: AppTheme.text; overflow: elide; } + Text { x: 15px; y: 39px; width: parent.width - 30px; height: 38px; text: issue.summary; font-size: 13px; color: AppTheme.text-secondary; wrap: word-wrap; overflow: elide; } + if issue.labels.length > 0: HorizontalLayout { + x: 15px; y: 82px; width: parent.width - 30px; height: 22px; spacing: 5px; + for label in issue.labels: Rectangle { + width: min(104px, max(48px, (parent.width - (issue.labels.length - 1) * 5px) / issue.labels.length)); + height: 22px; background: label.background; border-radius: 11px; + Text { x: 7px; width: parent.width - 14px; text: label.name; color: label.foreground; font-size: 11px; font-weight: 600; horizontal-alignment: center; vertical-alignment: center; overflow: elide; } + } + } + Text { x: 15px; y: issue.labels.length > 0 ? 112px : 86px; width: parent.width - 30px; text: issue.meta; font-size: 12px; color: AppTheme.text-tertiary; overflow: elide; } + } + } + } + EdgeBack { x: 0; y: 0; back => { root.back(); } } + } + + if tab == "issues" && page == "issue": Rectangle { + x: root.safe-area-insets.left; y: root.safe-area-insets.top; + width: root.width - root.safe-area-insets.left - root.safe-area-insets.right; + height: root.height - root.safe-area-insets.top - root.safe-area-insets.bottom - root.toolbar-height; + background: AppTheme.page; + Header { x: 0; y: 0; title: "Issue"; can_back: true; back => { root.back(); } } + if root.refreshing: Spinner { x: parent.width - 42px; y: 17px; width: 24px; height: 24px; } + PullToRefreshGesture { + x: 18px; y: 78px; width: parent.width - 36px; height: parent.height - 96px; + at_top: root.issue_at_top; refreshing: root.refreshing; + refresh => { root.refresh_issue(); } + issue_scroll := ScrollView { + property scroll_request: root.issue_scroll_request; + width: 100%; height: 100%; mouse-drag-pan-enabled: true; + changed scroll_request => { self.viewport-y = 0px; root.issue_at_top = true; } + scrolled => { root.issue_at_top = self.viewport-y >= 0px; } + VerticalLayout { + width: parent.width; + spacing: 12px; + alignment: start; + Text { text: root.issue_title; font-size: 23px; font-weight: 650; color: AppTheme.text; wrap: word-wrap; } + Text { text: root.issue_meta; font-size: 13px; color: AppTheme.text-secondary; wrap: word-wrap; } + Rectangle { height: 1px; background: AppTheme.separator; } + MarkdownContent { width: parent.width; blocks: root.issue_body; } + if root.comments.length > 0: Text { text: "Comments"; font-size: 17px; font-weight: 600; color: AppTheme.text; } + for comment in root.comments: CommentCard { width: parent.width; comment: comment; } + } + } + } + EdgeBack { x: 0; y: 0; back => { root.back(); } } + } + + if tab == "pulls" && !root.has_active_server: Rectangle { + x: root.safe-area-insets.left; y: root.safe-area-insets.top; + width: root.width - root.safe-area-insets.left - root.safe-area-insets.right; + height: root.height - root.safe-area-insets.top - root.safe-area-insets.bottom - root.toolbar-height; + background: AppTheme.page; + Header { x: 0; y: 0; title: "Home"; can_back: false; } + EmptyState { + x: 32px; width: parent.width - 64px; y: 210px; height: 190px; + title: "No server selected"; + detail: "Open Issues or Repositories to select a server or add your first one."; + } + } + + if tab == "pulls" && root.has_active_server && page == "pulls": Rectangle { + x: root.safe-area-insets.left; y: root.safe-area-insets.top; + width: root.width - root.safe-area-insets.left - root.safe-area-insets.right; + height: root.height - root.safe-area-insets.top - root.safe-area-insets.bottom - root.toolbar-height; + background: AppTheme.page; + Header { + x: 0; y: 0; title: "Pull Requests"; can_back: false; can_filter: true; + filter => { root.filter_open = true; } + } + if root.refreshing: Spinner { x: parent.width - 92px; y: 17px; width: 24px; height: 24px; } + if !root.loading && root.pulls.length == 0: EmptyState { + x: 34px; width: parent.width - 68px; y: 220px; height: 160px; + title: "No " + root.pull_filter + " pull requests"; + detail: "No pull requests match the selected status."; + } + PullToRefreshGesture { + x: 16px; y: 70px; width: parent.width - 32px; height: parent.height - 82px; + at_top: root.issues_at_top; refreshing: root.refreshing; + refresh => { root.refresh_pulls(); } + ListView { + width: 100%; height: 100%; mouse-drag-pan-enabled: true; + scrolled => { root.issues_at_top = self.viewport-y >= 0px; } + for pull in root.pulls: Rectangle { + height: 112px; + background: AppTheme.surface; border-color: AppTheme.border; border-width: 1px; border-radius: 12px; + TouchArea { clicked => { root.select_pull(pull.owner, pull.repository, pull.number); } } + Text { x: 14px; y: 10px; width: parent.width - 28px; text: pull.repository + " #" + pull.number; font-size: 12px; color: AppTheme.text-secondary; overflow: elide; } + Text { x: 14px; y: 31px; width: parent.width - 28px; text: pull.title; font-size: 16px; font-weight: 600; color: AppTheme.text; overflow: elide; } + Text { x: 14px; y: 57px; width: parent.width - 28px; text: pull.summary; font-size: 13px; color: AppTheme.text-secondary; overflow: elide; } + Text { x: 14px; y: 85px; width: parent.width - 28px; text: pull.meta; font-size: 12px; color: AppTheme.text-tertiary; overflow: elide; } + } + } + } + } + + if tab == "pulls" && page == "pull": Rectangle { + x: root.safe-area-insets.left; y: root.safe-area-insets.top; + width: root.width - root.safe-area-insets.left - root.safe-area-insets.right; + height: root.height - root.safe-area-insets.top - root.safe-area-insets.bottom - root.toolbar-height; + background: AppTheme.page; + Header { x: 0; y: 0; title: "Pull Request"; can_back: true; back => { root.back(); } } + if root.refreshing: Spinner { x: parent.width - 42px; y: 17px; width: 24px; height: 24px; } + PullToRefreshGesture { + x: 18px; y: 78px; width: parent.width - 36px; height: parent.height - 96px; + at_top: root.issue_at_top; refreshing: root.refreshing; + refresh => { root.refresh_pull(); } + ScrollView { + width: 100%; height: 100%; mouse-drag-pan-enabled: true; + scrolled => { root.issue_at_top = self.viewport-y >= 0px; } + VerticalLayout { + width: parent.width; spacing: 12px; alignment: start; + Text { text: root.detail_title; font-size: 23px; font-weight: 650; color: AppTheme.text; wrap: word-wrap; } + Text { text: root.detail_meta; font-size: 13px; color: AppTheme.text-secondary; wrap: word-wrap; } + Rectangle { height: 1px; background: AppTheme.separator; } + MarkdownContent { width: parent.width; blocks: root.detail_body; } + if root.files.length > 0: Text { text: root.detail_files_ref; font-size: 17px; font-weight: 600; color: AppTheme.text; } + for file in root.files: FileCard { width: parent.width; file: file; clicked => { root.select_pull_file(file.path); } } + if root.comments.length > 0: Text { text: "Comments"; font-size: 17px; font-weight: 600; color: AppTheme.text; } + for comment in root.comments: CommentCard { width: parent.width; comment: comment; } + } + } + } + EdgeBack { x: 0; y: 0; back => { root.back(); } } + } + + if tab == "repositories" && page == "commits": Rectangle { + x: root.safe-area-insets.left; y: root.safe-area-insets.top; + width: root.width - root.safe-area-insets.left - root.safe-area-insets.right; + height: root.height - root.safe-area-insets.top - root.safe-area-insets.bottom - root.toolbar-height; + background: AppTheme.page; + Header { x: 0; y: 0; title: root.repository_title; can_back: true; back => { root.back(); } } + ComboBox { + x: 16px; y: 66px; width: parent.width - 32px; height: 42px; + model: root.branch_choices; current-index: root.branch_index; + selected(value) => { root.select_branch(value); } + } + if root.refreshing: Spinner { x: parent.width - 42px; y: 17px; width: 24px; height: 24px; } + if !root.loading && root.commits.length == 0: EmptyState { + x: 34px; width: parent.width - 68px; y: 220px; height: 160px; + title: "No commits"; + detail: "This repository has no commit history."; + } + PullToRefreshGesture { + x: 16px; y: 116px; width: parent.width - 32px; height: parent.height - 128px; + at_top: root.repositories_at_top; refreshing: root.refreshing; + refresh => { root.refresh_commits(); } + ListView { + width: 100%; height: 100%; mouse-drag-pan-enabled: true; + scrolled => { root.repositories_at_top = self.viewport-y >= 0px; } + for commit in root.commits: commit_row := Rectangle { + property graph-width: commit.lanes.length > 0 ? min(72px, commit.lanes.length * 10px + 8px) : 0px; + property lane-spacing: (self.graph-width - 8px) / max(1, commit.lanes.length); + height: 86px; background: transparent; + Rectangle { width: 100%; height: 100%; background: AppTheme.surface; border-color: AppTheme.border; border-width: 1px; border-radius: 12px; } + TouchArea { clicked => { root.select_commit(commit.sha); } } + for lane[index] in commit.lanes: Rectangle { + x: 4px + index * commit_row.lane-spacing; + width: 3px; height: 100%; background: transparent; + if lane.top: Rectangle { + y: -1px; width: 3px; + height: lane.connect_left && !lane.bottom ? 37px : 42px; + background: lane.color; + } + if lane.bottom: Rectangle { + y: lane.connect_left && !lane.top ? 45px : 40px; + width: 3px; + height: parent.height - (lane.connect_left && !lane.top ? 44px : 39px); + background: lane.color; + } + if lane.connect_left && lane.top == lane.bottom: Rectangle { + x: -commit_row.lane-spacing; y: 39px; + width: commit_row.lane-spacing + 3px; height: 3px; + background: lane.color; + } + if lane.connect_left && lane.bottom && !lane.top: Path { + x: 1.5px - commit_row.lane-spacing; y: 35px; + width: commit_row.lane-spacing; height: 11px; + viewbox-width: 10; viewbox-height: 11; + commands: "M 0 5.5 L 5 5.5 Q 10 5.5 10 10.5"; + fill: transparent; stroke: lane.color; stroke-width: 3px; stroke-line-cap: round; + } + if lane.connect_left && lane.top && !lane.bottom: Path { + x: 1.5px - commit_row.lane-spacing; y: 35px; + width: commit_row.lane-spacing; height: 11px; + viewbox-width: 10; viewbox-height: 11; + commands: "M 10 0.5 Q 10 5.5 5 5.5 L 0 5.5"; + fill: transparent; stroke: lane.color; stroke-width: 3px; stroke-line-cap: round; + } + if lane.node: Rectangle { + x: -3px; y: 36px; width: 9px; height: 9px; border-radius: 5px; + background: lane.color; border-color: AppTheme.surface; border-width: 2px; + } + } + Text { x: parent.graph-width + 14px; y: 8px; width: parent.width - self.x - 14px; text: commit.title; font-size: 15px; font-weight: 600; color: AppTheme.text; overflow: elide; } + Text { x: parent.graph-width + 14px; y: 33px; width: parent.width - self.x - 14px; text: commit.refs != "" ? commit.refs : commit.meta; font-size: 12px; color: commit.refs != "" ? AppTheme.accent : AppTheme.text-secondary; overflow: elide; } + Text { x: parent.graph-width + 14px; y: 53px; width: parent.width - self.x - 14px; text: commit.refs != "" ? commit.meta + " · " + commit.sha : commit.sha; font-size: 11px; color: AppTheme.text-tertiary; overflow: elide; } + } + } + } + EdgeBack { x: 0; y: 0; back => { root.back(); } } + } + + if tab == "repositories" && page == "files": Rectangle { + x: root.safe-area-insets.left; y: root.safe-area-insets.top; + width: root.width - root.safe-area-insets.left - root.safe-area-insets.right; + height: root.height - root.safe-area-insets.top - root.safe-area-insets.bottom - root.toolbar-height; + background: AppTheme.page; + Header { x: 0; y: 0; title: "Changed Files"; can_back: true; back => { root.back(); } } + if !root.loading && root.files.length == 0: EmptyState { + x: 34px; width: parent.width - 68px; y: 220px; height: 160px; + title: "No changed files"; + detail: "This commit does not contain file changes."; + } + ListView { + x: 16px; y: 70px; width: parent.width - 32px; height: parent.height - 82px; mouse-drag-pan-enabled: true; + for file in root.files: FileCard { file: file; clicked => { root.select_file(file.path); } } + } + EdgeBack { x: 0; y: 0; back => { root.back(); } } + } + + if (tab == "repositories" || tab == "pulls") && page == "diff": Rectangle { + x: root.safe-area-insets.left; y: root.safe-area-insets.top; + width: root.width - root.safe-area-insets.left - root.safe-area-insets.right; + height: root.height - root.safe-area-insets.top - root.safe-area-insets.bottom - root.toolbar-height; + background: AppTheme.page; + Header { x: 0; y: 0; title: root.detail_title; can_back: true; back => { root.back(); } } + ScrollView { + x: 12px; y: 70px; width: parent.width - 24px; height: parent.height - 82px; mouse-drag-pan-enabled: true; + viewport-width: max(self.visible-width, root.diff_columns * 7px + 100px); + viewport-height: diff_rows.preferred-height; + diff_rows := VerticalLayout { + width: parent.viewport-width; + for line in root.diff_lines: Rectangle { + height: 20px; + background: line.kind == "addition" ? (AppTheme.dark ? #173d25 : #ddf5e3) : line.kind == "removal" ? (AppTheme.dark ? #4a2020 : #fde2e1) : line.kind == "hunk" ? (AppTheme.dark ? #17324d : #dcecff) : line.kind == "header" ? AppTheme.surface-muted : AppTheme.surface; + Text { x: 0; width: 38px; text: line.old_number; font-family: "monospace"; font-size: 11px; color: AppTheme.text-tertiary; horizontal-alignment: right; vertical-alignment: center; } + Text { x: 42px; width: 38px; text: line.new_number; font-family: "monospace"; font-size: 11px; color: AppTheme.text-tertiary; horizontal-alignment: right; vertical-alignment: center; } + Rectangle { x: 84px; width: 1px; height: 100%; background: AppTheme.separator; } + Text { + x: 90px; width: parent.width - 94px; text: line.text; font-family: "monospace"; font-size: 11px; + color: line.kind == "addition" ? (AppTheme.dark ? #7fe787 : #176b2c) : line.kind == "removal" ? (AppTheme.dark ? #ff7b72 : #a12720) : line.kind == "hunk" ? (AppTheme.dark ? #79c0ff : #175b9e) : AppTheme.text; + wrap: no-wrap; overflow: clip; vertical-alignment: center; + } + } + } + } + EdgeBack { x: 0; y: 0; back => { root.back(); } } + } + + Rectangle { + x: root.safe-area-insets.left; + y: root.height - root.safe-area-insets.bottom - root.toolbar-height; + width: root.width - root.safe-area-insets.left - root.safe-area-insets.right; + height: root.toolbar-height; + background: AppTheme.toolbar; + TabButton { + x: 0; width: parent.width / 5; height: parent.height; kind: "home"; label: "Home"; active: root.tab == "home"; + clicked => { + if root.tab == "home" { root.home_scroll_request += 1; } + root.select_tab("home"); + } + } + TabButton { + x: parent.width / 5; width: parent.width / 5; height: parent.height; kind: "issues"; label: "Issues"; active: root.tab == "issues"; + clicked => { + if root.tab == "issues" { + if root.page == "servers" { root.servers_scroll_request += 1; } + if root.page == "repositories" { root.repositories_scroll_request += 1; } + if root.page == "issues" { root.issues_scroll_request += 1; } + if root.page == "issue" { root.issue_scroll_request += 1; } + } + root.select_tab("issues"); + } + } + TabButton { x: parent.width * 2 / 5; width: parent.width / 5; height: parent.height; kind: "repositories"; label: "Repos"; active: root.tab == "repositories"; clicked => { root.select_tab("repositories"); } } + TabButton { x: parent.width * 3 / 5; width: parent.width / 5; height: parent.height; kind: "pulls"; label: "Pulls"; active: root.tab == "pulls"; clicked => { root.select_tab("pulls"); } } + TabButton { x: parent.width * 4 / 5; width: parent.width / 5; height: parent.height; kind: "settings"; label: "Settings"; active: root.tab == "settings"; clicked => { root.select_tab("settings"); } } + } + + if root.loading && (root.tab == "issues" || root.tab == "repositories" || root.tab == "pulls"): Rectangle { + width: 100%; height: 100%; + background: AppTheme.loading-overlay; + TouchArea { } + Spinner { width: 38px; height: 38px; x: (parent.width - self.width) / 2; y: (parent.height - self.height) / 2; } + } + if root.filter_open: Rectangle { + width: 100%; height: 100%; background: #00000066; + TouchArea { clicked => { root.filter_open = false; } } + Rectangle { + x: 28px; y: (parent.height - 190px) / 2; width: parent.width - 56px; height: 190px; + background: AppTheme.surface; border-radius: 14px; + Text { x: 18px; y: 16px; width: parent.width - 36px; text: "Status"; font-size: 19px; font-weight: 650; color: AppTheme.text; } + VerticalLayout { + x: 18px; y: 54px; width: parent.width - 36px; height: 118px; spacing: 8px; + Button { text: "Open"; clicked => { root.choose_filter("open"); root.filter_open = false; } } + Button { text: "Closed"; clicked => { root.choose_filter("closed"); root.filter_open = false; } } + } + } + } + if root.error != "": Rectangle { + x: 14px; y: parent.height - root.safe-area-insets.bottom - 132px; width: parent.width - 28px; height: 66px; + background: #d93d35; border-radius: 12px; + Text { x: 14px; width: parent.width - 28px; text: root.error; color: white; font-size: 13px; wrap: word-wrap; vertical-alignment: center; } + TouchArea { clicked => { root.error = ""; } } + } +} diff --git a/crates/app/ui/components.slint b/crates/app/ui/components.slint new file mode 100644 index 0000000..c0a4411 --- /dev/null +++ b/crates/app/ui/components.slint @@ -0,0 +1,318 @@ +import { Palette } from "std-widgets.slint"; +import { CommentRow, FileRow, MarkdownBlock } from "types.slint"; + +export global IconTheme { + in-out property style: 0; +} + +export global AppTheme { + in-out property mode: 0; + private property system-background: Palette.background; + out property dark: mode == 2 || (mode == 0 && system-background.to-oklch().lightness < 0.5); + out property accent: dark ? #0a84ff : #0879e1; + out property accent-soft: dark ? #17324d : #e5f2fd; + out property page: dark ? #1c1c1e : #f2f2f7; + out property surface: dark ? #2c2c2e : #ffffff; + out property surface-muted: dark ? #3a3a3c : #f5f5f8; + out property header: dark ? #242426 : #f8f8fa; + out property toolbar: dark ? #242426 : #fbfbfc; + out property border: dark ? #48484a : #dedee3; + out property separator: dark ? #3a3a3c : #d8d8dd; + out property text: dark ? #f2f2f7 : #17171a; + out property text-secondary: dark ? #c7c7cc : #5f5f66; + out property text-tertiary: dark ? #8e8e93 : #85858c; + out property code-background: dark ? #242426 : #e9e9ed; + out property quote-background: dark ? #242426 : #f6f6f8; + out property loading-overlay: dark ? #00000088 : #ffffff88; + changed mode => { + Palette.color-scheme = mode == 1 ? ColorScheme.light : mode == 2 ? ColorScheme.dark : ColorScheme.unknown; + } +} + +export component AppIcon inherits Rectangle { + in property kind; + in property style: IconTheme.style; + in property icon-color: AppTheme.accent; + background: transparent; + + private property duotone: root.style == 1; + private property bold: root.style == 2; + private property issue-kind: root.kind == "issues" || root.kind == "issue-open" || root.kind == "issue-close" || root.kind == "issue-reopen" || root.kind == "issue-comment"; + private property repository-kind: root.kind == "repositories" || root.kind == "repo-create" || root.kind == "repo-rename" || root.kind == "repo-star" || root.kind == "repo-watch" || root.kind == "update"; + private property pull-kind: root.kind == "pulls" || root.kind == "pull-open" || root.kind == "pull-merge" || root.kind == "pull-close" || root.kind == "pull-reopen" || root.kind == "pull-comment" || root.kind == "pull-approve" || root.kind == "pull-reject"; + private property tag-kind: root.kind == "tag-push" || root.kind == "tag-delete"; + private property branch-kind: root.kind == "branch-create" || root.kind == "branch-delete"; + private property closed-shape: root.kind == "home" || root.issue-kind || root.repository-kind || root.kind == "favorite" || root.kind == "favorite-filled" || root.tag-kind || root.kind == "release"; + private property has-badge: root.kind == "branch-create" || root.kind == "branch-delete" || root.kind == "push" || root.kind == "repo-create" || root.kind == "repo-rename" || root.kind == "repo-star" || root.kind == "repo-watch" || root.kind == "update" || root.kind == "issue-open" || root.kind == "issue-close" || root.kind == "issue-reopen" || root.kind == "issue-comment" || root.kind == "pull-open" || root.kind == "pull-merge" || root.kind == "pull-close" || root.kind == "pull-reopen" || root.kind == "pull-comment" || root.kind == "pull-approve" || root.kind == "pull-reject" || root.kind == "tag-push" || root.kind == "tag-delete"; + private property positive: root.kind == "issue-close" || root.kind == "pull-merge" || root.kind == "pull-approve"; + private property destructive: root.kind == "branch-delete" || root.kind == "pull-close" || root.kind == "pull-reject" || root.kind == "tag-delete"; + private property badge-color: root.positive ? #20a866 : root.destructive ? #e45b4d : root.duotone ? #26bfd1 : root.icon-color; + private property shape: root.kind == "home" + ? "M3 10.5 L12 3 L21 10.5 V21 H15 V14 H9 V21 H3 Z" + : root.issue-kind + ? "M4 5 H20 V9 C18 9 18 15 20 15 V19 H4 V15 C6 15 6 9 4 9 Z" + : root.repository-kind + ? "M6 3 H18 C19.1 3 20 3.9 20 5 V21 H6 C4.9 21 4 20.1 4 19 V5 C4 3.9 4.9 3 6 3 Z" + : root.pull-kind + ? "M7 4 V20 M17 4 V11 C17 15 14 17 10 17 M7 4 H7 M17 4 H17" + : root.kind == "settings" + ? "M12 3 V6 M12 18 V21 M3 12 H6 M18 12 H21 M5.6 5.6 L7.8 7.8 M16.2 16.2 L18.4 18.4 M18.4 5.6 L16.2 7.8 M7.8 16.2 L5.6 18.4" + : root.kind == "back" + ? (root.bold ? "M16 4 L7 12 L16 20 Z" : "M16 4 L7 12 L16 20") + : root.kind == "filter" + ? (root.bold ? "M3 5 H21 L14 13 V20 L10 22 V13 Z" : "M4 6 H20 M4 12 H20 M4 18 H20") + : root.kind == "favorite" || root.kind == "favorite-filled" + ? "M12 3 L14.8 8.7 L21 9.6 L16.5 14 L17.6 20.3 L12 17.3 L6.4 20.3 L7.5 14 L3 9.6 L9.2 8.7 Z" + : root.kind == "disclosure" + ? (root.bold ? "M8 4 L17 12 L8 20 Z" : "M8 4 L17 12 L8 20") + : root.branch-kind + ? "M7 4 V20 M7 11 C11 11 14 9 14 5" + : root.kind == "push" + ? "M4 8 H13 M4 12 H13 M4 16 H13" + : root.tag-kind + ? "M3 5 V13 L11 21 L21 11 L13 3 H5 C3.9 3 3 3.9 3 5 Z" + : root.kind == "release" + ? "M4 8 L12 4 L20 8 V18 L12 22 L4 18 Z" + : "M12 4 V20 M4 12 H20"; + private property detail: root.kind == "home" + ? "M9 21 V14 H15 V21" + : root.issue-kind + ? "M12 8 V13 M12 16.5 V17" + : root.repository-kind + ? "M7 3 V21 M11 9 L8.5 12 L11 15 M15 9 L17.5 12 L15 15" + : root.pull-kind + ? "M7 4 A1.8 1.8 0 1 1 6.99 4 M7 20 A1.8 1.8 0 1 1 6.99 20 M17 4 A1.8 1.8 0 1 1 16.99 4" + : root.kind == "settings" + ? "M12 8 A4 4 0 1 1 11.99 8" + : root.kind == "filter" && !root.bold + ? "M9 4 V8 M15 10 V14 M8 16 V20" + : root.tag-kind + ? "M8 8 A1.5 1.5 0 1 1 7.99 8" + : root.kind == "release" + ? "M12 18 V8 M9 11 L12 8 L15 11 M7 6 V3 M17 6 V3" + : ""; + private property badge-detail: root.kind == "branch-create" || root.kind == "repo-create" || root.kind == "issue-open" || root.kind == "pull-open" + ? "M18 15.5 V20.5 M15.5 18 H20.5" + : root.kind == "issue-close" || root.kind == "pull-merge" || root.kind == "pull-approve" + ? "M15.4 18 L17.2 19.8 L20.7 16.2" + : root.kind == "branch-delete" || root.kind == "tag-delete" + ? "M15.3 18 H20.7" + : root.kind == "pull-close" || root.kind == "pull-reject" + ? "M16 16 L20 20 M20 16 L16 20" + : root.kind == "push" || root.kind == "tag-push" + ? "M18 20.5 V15.5 M15.7 17.8 L18 15.5 L20.3 17.8" + : root.kind == "repo-rename" + ? "M15.3 20.6 L16 18 L19.7 14.3 L21.7 16.3 L18 20 Z" + : root.kind == "repo-star" + ? "M18 14.5 L19 16.7 L21.4 17 L19.6 18.7 L20.1 21 L18 19.8 L15.9 21 L16.4 18.7 L14.6 17 L17 16.7 Z" + : root.kind == "repo-watch" + ? "M14.5 18 C16.3 15.8 19.7 15.8 21.5 18 C19.7 20.2 16.3 20.2 14.5 18 M18 17 A1 1 0 1 1 17.99 17" + : root.kind == "issue-reopen" || root.kind == "pull-reopen" + ? "M20.8 17 A3 3 0 1 0 20.2 20.3 M20.8 17 H18.5 M20.8 17 V19.3" + : root.kind == "issue-comment" || root.kind == "pull-comment" + ? "M15.5 17 H20.5 V20 H18 L16 21 V20 H15.5 Z M17 18.5 H17.1 M19 18.5 H19.1" + : root.kind == "update" + ? "M18 14.8 L18.8 17.2 L21.2 18 L18.8 18.8 L18 21.2 L17.2 18.8 L14.8 18 L17.2 17.2 Z" + : ""; + + Path { + width: 100%; height: 100%; viewbox-width: 24; viewbox-height: 24; + commands: root.shape; + fill: root.closed-shape ? (root.bold || root.kind == "favorite-filled" ? root.icon-color : root.duotone ? #26bfd133 : transparent) : transparent; + stroke: root.closed-shape && (root.bold || root.kind == "favorite-filled") ? transparent : root.icon-color; + stroke-width: root.bold ? 3px : root.duotone ? 2.2px : 1.8px; + stroke-line-cap: round; stroke-line-join: round; + } + if root.detail != "": Path { + width: 100%; height: 100%; viewbox-width: 24; viewbox-height: 24; + commands: root.detail; fill: transparent; + stroke: root.bold && root.closed-shape ? #ffffff : root.icon-color; + stroke-width: root.bold ? 2.4px : 1.8px; + stroke-line-cap: round; stroke-line-join: round; + } + if root.has-badge: Path { + width: 100%; height: 100%; viewbox-width: 24; viewbox-height: 24; + commands: "M18 13.5 A4.5 4.5 0 1 1 17.99 13.5 Z"; + fill: root.style == 0 ? AppTheme.surface : root.badge-color; + stroke: root.style == 0 ? root.badge-color : #ffffff; + stroke-width: root.style == 2 ? 1.5px : 1.2px; + } + if root.has-badge: Path { + width: 100%; height: 100%; viewbox-width: 24; viewbox-height: 24; + commands: root.badge-detail; fill: root.style == 0 ? transparent : #ffffff; + stroke: root.style == 0 ? root.badge-color : #ffffff; + stroke-width: root.bold ? 2px : 1.6px; + stroke-line-cap: round; stroke-line-join: round; + } +} + +export component IconStyleChoice inherits Rectangle { + in property value; + in property label; + callback clicked(); + background: IconTheme.style == root.value ? AppTheme.accent-soft : AppTheme.surface-muted; + border-color: IconTheme.style == root.value ? AppTheme.accent : AppTheme.border; + border-width: IconTheme.style == root.value ? 2px : 1px; + border-radius: 11px; + TouchArea { clicked => { root.clicked(); } } + AppIcon { x: (parent.width - 30px) / 2; y: 10px; width: 30px; height: 30px; kind: "repositories"; style: root.value; } + Text { y: 48px; width: 100%; text: root.label; font-size: 12px; font-weight: IconTheme.style == root.value ? 650 : 500; color: AppTheme.text; horizontal-alignment: center; } +} + +export component AppearanceChoice inherits Rectangle { + in property value; + in property label; + callback clicked(); + background: AppTheme.mode == root.value ? AppTheme.accent-soft : AppTheme.surface-muted; + border-color: AppTheme.mode == root.value ? AppTheme.accent : AppTheme.border; + border-width: AppTheme.mode == root.value ? 2px : 1px; + border-radius: 11px; + TouchArea { clicked => { root.clicked(); } } + Text { + width: 100%; height: 100%; text: root.label; + font-size: 14px; font-weight: AppTheme.mode == root.value ? 650 : 500; + color: AppTheme.mode == root.value ? AppTheme.accent : AppTheme.text; + horizontal-alignment: center; vertical-alignment: center; + } +} + +export component Header inherits Rectangle { + in property title; + in property can_back; + in property can_filter: false; + callback back(); + callback filter(); + height: 58px; + width: 100%; + background: AppTheme.header; + border-color: AppTheme.border; + border-width: 0px; + + if can_back: TouchArea { + x: 8px; width: 54px; + clicked => { root.back(); } + AppIcon { x: 7px; y: 17px; width: 24px; height: 24px; kind: "back"; } + } + Text { + x: root.can_back || root.can_filter ? 62px : 12px; + width: parent.width - 2 * self.x; + text: root.title; + horizontal-alignment: center; + vertical-alignment: center; + font-size: 18px; + font-weight: 600; + color: AppTheme.text; + overflow: elide; + } + if can_filter: TouchArea { + x: parent.width - 58px; width: 58px; + clicked => { root.filter(); } + AppIcon { x: 17px; y: 17px; width: 24px; height: 24px; kind: "filter"; } + } + Rectangle { y: parent.height - 1px; height: 1px; background: AppTheme.border; } +} + +export component EdgeBack inherits SwipeGestureHandler { + callback back(); + width: 24px; + height: 100%; + handle-swipe-right: true; + swiped => { root.back(); } +} + +export component EmptyState inherits VerticalLayout { + in property title; + in property detail; + alignment: center; + spacing: 8px; + Text { text: root.title; font-size: 21px; font-weight: 600; horizontal-alignment: center; color: AppTheme.text; } + Text { text: root.detail; font-size: 15px; horizontal-alignment: center; wrap: word-wrap; color: AppTheme.text-secondary; } +} + +export component MarkdownContent inherits VerticalLayout { + in property <[MarkdownBlock]> blocks; + spacing: 8px; + + for block in root.blocks: Rectangle { + height: block.kind == "rule" ? 9px : rendered.preferred-height + (block.kind == "code" || block.kind == "table" ? 16px : 0px); + background: block.kind == "code" || block.kind == "table" ? AppTheme.code-background : block.kind == "quote" ? AppTheme.quote-background : transparent; + border-radius: block.kind == "code" || block.kind == "table" ? 7px : 0px; + if block.kind == "quote": Rectangle { width: 3px; height: 100%; background: AppTheme.text-tertiary; border-radius: 2px; } + if block.kind == "rule": Rectangle { y: 4px; width: 100%; height: 1px; background: AppTheme.separator; } + rendered := StyledText { + x: block.kind == "code" || block.kind == "table" ? 8px : block.kind == "quote" ? 11px : 0px; + y: block.kind == "code" || block.kind == "table" ? 8px : 0px; + width: parent.width - self.x - (block.kind == "code" || block.kind == "table" ? 8px : 0px); + text: block.text; + default-color: AppTheme.text; + link-color: AppTheme.accent; + default-font-family: block.kind == "code" || block.kind == "table" ? "monospace" : ""; + default-font-size: block.kind == "heading1" ? 24px : block.kind == "heading2" ? 21px : block.kind == "heading3" ? 19px : block.kind == "heading4" ? 17px : 16px; + } + } +} + +export component CommentCard inherits Rectangle { + in property comment; + height: content.preferred-height + 24px; + background: AppTheme.surface; + border-radius: 10px; + content := VerticalLayout { + x: 12px; y: 10px; width: parent.width - 24px; + spacing: 5px; + Text { text: root.comment.author; font-size: 13px; font-weight: 600; color: AppTheme.text; } + MarkdownContent { width: parent.width; blocks: root.comment.body; } + Text { text: root.comment.meta; font-size: 11px; color: AppTheme.text-tertiary; } + } +} + +export component FileCard inherits Rectangle { + in property file; + callback clicked(); + height: 66px; + background: AppTheme.surface; + border-color: AppTheme.border; + border-width: 1px; + border-radius: 10px; + TouchArea { clicked => { root.clicked(); } } + Text { x: 13px; y: 10px; width: parent.width - 26px; text: root.file.path; font-size: 14px; font-weight: 600; color: AppTheme.text; overflow: elide; } + Text { x: 13px; y: 38px; width: parent.width - 26px; text: root.file.status; font-size: 12px; color: AppTheme.text-secondary; } +} + +export component TabButton inherits Rectangle { + in property kind; + in property label; + in property active; + private property icon-area-height: root.height * 3 / 4; + private property icon-size: min(42px, self.icon-area-height - 6px); + callback clicked(); + background: transparent; + TouchArea { clicked => { root.clicked(); } } + AppIcon { + x: (parent.width - root.icon-size) / 2; + y: (root.icon-area-height - root.icon-size) / 2; + width: root.icon-size; height: root.icon-size; + kind: root.kind; icon-color: root.active ? AppTheme.accent : AppTheme.text-tertiary; + } + Text { + y: root.icon-area-height; height: parent.height - self.y; width: 100%; text: root.label; + color: root.active ? AppTheme.accent : AppTheme.text-tertiary; font-size: 9px; + font-weight: root.active ? 600 : 400; + horizontal-alignment: center; vertical-alignment: center; + } +} + +export component PullToRefreshGesture inherits SwipeGestureHandler { + in property at_top; + in property refreshing; + callback refresh(); + enabled: root.at_top && !root.refreshing; + handle-swipe-down: true; + swiped => { root.refresh(); } + @children + if self.swiping: Rectangle { + x: (parent.width - 142px) / 2; y: 8px; width: 142px; height: 32px; + background: AppTheme.surface-muted; border-radius: 16px; + Text { text: "Release to refresh"; font-size: 12px; color: AppTheme.text-secondary; horizontal-alignment: center; vertical-alignment: center; } + } +} diff --git a/crates/app/ui/types.slint b/crates/app/ui/types.slint new file mode 100644 index 0000000..c7dd6fb --- /dev/null +++ b/crates/app/ui/types.slint @@ -0,0 +1,24 @@ +export struct ServerRow { name: string, url: string } +export struct RepositoryRow { + name: string, owner: string, description: string, meta: string, favorite: bool, +} +export struct LabelRow { name: string, background: color, foreground: color } +export struct IssueRow { + number: int, title: string, summary: string, meta: string, labels: [LabelRow], +} +export struct PullRow { + number: int, owner: string, repository: string, title: string, summary: string, meta: string, +} +export struct MarkdownBlock { text: styled-text, kind: string } +export struct CommentRow { author: string, body: [MarkdownBlock], meta: string } +export struct GraphLane { color: color, top: bool, bottom: bool, node: bool, connect_left: bool } +export struct CommitRow { + sha: string, title: string, meta: string, refs: string, lanes: [GraphLane], +} +export struct FileRow { path: string, status: string } +export struct ActivityRow { + icon: string, title: string, detail: string, meta: string, + target: string, owner: string, repository: string, number: int, sha: string, +} +export struct DiffLine { old_number: string, new_number: string, text: string, kind: string } +export struct HeatCell { week: int, day: int, level: int }