mirror of
https://github.com/th-ch/youtube-music.git
synced 2026-01-10 10:11:46 +00:00
Compare commits
1449 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| c94b22b82c | |||
| cab3cb49f0 | |||
| e42084f008 | |||
| adca273ec3 | |||
| 91dceb3c22 | |||
| 216e76f4a1 | |||
| 178bfa483f | |||
| 10ecf5d2fe | |||
| 7099b81296 | |||
| 1f15376b00 | |||
| 02b7a39753 | |||
| 6edc84a8bd | |||
| 11a0d39064 | |||
| d5a5ed35b6 | |||
| dbb9e95b32 | |||
| d4c8a4320d | |||
| 68d4f38e41 | |||
| 2204784e89 | |||
| c3b995b0a8 | |||
| ed0a344077 | |||
| 199d912823 | |||
| 089eff3152 | |||
| 306ee8dba5 | |||
| a4992bafb2 | |||
| 5b004eedff | |||
| 2a66076d31 | |||
| f48e46d29c | |||
| 20296f5463 | |||
| fe4c89c349 | |||
| 3640527c8c | |||
| 3326582a16 | |||
| 5dcb9fe9ba | |||
| 336fa1e6fd | |||
| 3679a109d6 | |||
| 5290ed3de2 | |||
| fe5195714f | |||
| 8eb846262d | |||
| e9a1c2a91f | |||
| 2d1f78b383 | |||
| 1899064fd3 | |||
| e0280e5fe2 | |||
| d577e0fba6 | |||
| 37577c2f7f | |||
| c880f0a4eb | |||
| 4875955914 | |||
| 5b28c780bd | |||
| 4db2674b15 | |||
| a454a0163f | |||
| 61eb28e780 | |||
| e165e64952 | |||
| f380822e11 | |||
| 6b1995145a | |||
| 9317e99f43 | |||
| 66d05d8683 | |||
| 545a3a4bb6 | |||
| 04a6d16dbe | |||
| 3c36477b1e | |||
| c5c191492e | |||
| b12e2f607c | |||
| 1d77ad6de4 | |||
| 25bd26d7f3 | |||
| d11d0abe73 | |||
| 8a643c465d | |||
| 233673b8d8 | |||
| 5a448fab31 | |||
| 42e8262cda | |||
| f64769b1d3 | |||
| e12998761b | |||
| 2e20fa83b8 | |||
| 5149757af3 | |||
| 655741f108 | |||
| 4e58571ad0 | |||
| 1e4a615b47 | |||
| dedcf0c9ff | |||
| a84a7d236a | |||
| e56b4b21f0 | |||
| 361f9e42bd | |||
| 918736d2ca | |||
| 8f3d5b08ac | |||
| 4ca327d801 | |||
| 8d0aa057ad | |||
| b7ffee089b | |||
| d72b994f66 | |||
| e6dafdb068 | |||
| 14886dd4bd | |||
| c9f2f88bac | |||
| 1eabbc0bbe | |||
| 32a572b35a | |||
| 59a5679cbb | |||
| ac51f798c3 | |||
| 7599cc694a | |||
| 53595654b1 | |||
| 7656c41dbc | |||
| ff0c5b87c9 | |||
| 506c95740a | |||
| 575a42e28a | |||
| dcdc6a825f | |||
| 0a41bb1cd6 | |||
| 72a4736dc9 | |||
| f2b1e6b6bf | |||
| bc0e28ad6d | |||
| 8ebae91c02 | |||
| 954ad90733 | |||
| 5af0643788 | |||
| be633ac1f2 | |||
| 0d047c1fd5 | |||
| e8b1aca629 | |||
| 5b9bacf390 | |||
| ccd16f4a5f | |||
| 02e519dab3 | |||
| 473ea78f12 | |||
| e7f366b770 | |||
| 66816ac42d | |||
| 08b985f2ab | |||
| 747bde2136 | |||
| eabc28b39f | |||
| 3537dc19ee | |||
| 6afeb60557 | |||
| 71115dedee | |||
| 8750b54f76 | |||
| 482a1c5073 | |||
| c8d516c40b | |||
| c1ad168c32 | |||
| 5f5be5d02f | |||
| 61ef56dccc | |||
| a73b5acc75 | |||
| 877573532c | |||
| 7b033b5caf | |||
| 8924ec29d3 | |||
| 23e688aaf8 | |||
| 9b3cbe8e01 | |||
| 67a89e8ed4 | |||
| 464a2b94ea | |||
| 9357a15116 | |||
| ee820bb01c | |||
| 6b81735811 | |||
| 8ce91b143a | |||
| 116dbad9bc | |||
| 977af3d617 | |||
| 6da8defc73 | |||
| 0e93a963e1 | |||
| 1e98b2e75a | |||
| 6f5f13a840 | |||
| 822bcedadf | |||
| 2b6aea82c3 | |||
| 4f4efb407e | |||
| 6159e0e652 | |||
| 3957e06174 | |||
| c78f823b9b | |||
| 1be3bb360e | |||
| ba2afd2652 | |||
| 5e283c9ea5 | |||
| ddb1c56111 | |||
| ebd167f3f2 | |||
| 178a62b9d3 | |||
| f98a2cf766 | |||
| fdbe6f7331 | |||
| 57c2cdc91e | |||
| 0f5074f8ab | |||
| 661396226d | |||
| 36f27fe2e6 | |||
| adf1ce4bc7 | |||
| 43b4b8df5e | |||
| 4a8440c281 | |||
| 32fe9fcffe | |||
| a9896845da | |||
| a59aa07334 | |||
| e07d7395e7 | |||
| 9bb6f32ece | |||
| ccb19a0dc9 | |||
| 64fb6c2597 | |||
| 73c3e355fe | |||
| fc7a504643 | |||
| 764dc0f895 | |||
| 9f33f49ec4 | |||
| 87ae6d29bb | |||
| 093c8e3ca6 | |||
| fec26a010d | |||
| 5d8aaccc55 | |||
| cda03078a9 | |||
| 9c139b96f4 | |||
| 9b2816c156 | |||
| b1b8847134 | |||
| bf9e698288 | |||
| 28e8a1c5dd | |||
| 18e0b1b863 | |||
| 02e2fb6a83 | |||
| 91bee4880e | |||
| 7de7303ebb | |||
| 363d869cff | |||
| 2512af80af | |||
| 887979932c | |||
| eeaaf2f158 | |||
| e91e995b95 | |||
| 49dd2ecac6 | |||
| 06f419abc4 | |||
| 2c29461e61 | |||
| a9b9e74477 | |||
| 30848b7c4a | |||
| 83023c19a6 | |||
| 45931a25b0 | |||
| 98d4ff83b1 | |||
| 35f1d75832 | |||
| cbbba6ec76 | |||
| 35383e4730 | |||
| b7029cfc60 | |||
| d8328e0ad5 | |||
| 7ccb72d399 | |||
| 5108f19ee5 | |||
| 041574570f | |||
| 9505195835 | |||
| b33f5ff94d | |||
| 097f488ba0 | |||
| 521d1d8ee7 | |||
| f38ce093f5 | |||
| 2e63985ed3 | |||
| a22d08e983 | |||
| d1b998aebd | |||
| 7f598b5856 | |||
| 373e27ac5b | |||
| dcd53a9234 | |||
| c59b11b63b | |||
| 4d8fd8718f | |||
| f0c4d1da36 | |||
| 79c669e7c1 | |||
| e8156fc0fe | |||
| 348c70dca4 | |||
| 3439dded3b | |||
| 2ee0101e97 | |||
| 6a037083dd | |||
| 1d1705e471 | |||
| 0f7fe74d40 | |||
| d9f24d2c4e | |||
| 7b6a7377a8 | |||
| bd8468a8c1 | |||
| aec088f95d | |||
| 4b12b43f57 | |||
| f47287de94 | |||
| 58317f4c10 | |||
| c0aae7b2ac | |||
| 76547ad602 | |||
| 8b128273c8 | |||
| 25d1127b21 | |||
| b3ab08b354 | |||
| 2ef8b4f14c | |||
| b39baf6d88 | |||
| 40f0b9b852 | |||
| 3fe8115f32 | |||
| 47d3b34e4b | |||
| d0889bb622 | |||
| d6a7cbfa6f | |||
| c6541b6897 | |||
| 865578037b | |||
| d4176eeb8a | |||
| b88bbbc680 | |||
| 67aaccce86 | |||
| b95b69bf50 | |||
| 894531fd93 | |||
| 8a20566e0f | |||
| 253325a3cf | |||
| ee6716a0eb | |||
| 8fe5450ace | |||
| 1bb36b38bc | |||
| 25cec993bc | |||
| c744619664 | |||
| 451d30517e | |||
| dbb345ba1f | |||
| c60edf9718 | |||
| bd4e3a91c8 | |||
| 0f8b586b75 | |||
| 262c17a5bd | |||
| 542cb916b5 | |||
| 2627ebd675 | |||
| 89ed7d2345 | |||
| 3c4abc1418 | |||
| de224444c2 | |||
| e9ae2d44c9 | |||
| 680f4143f5 | |||
| 23b553ea4b | |||
| e2a91022fd | |||
| d97aa1a8a0 | |||
| ede11307ef | |||
| b74c1a0207 | |||
| 104c1284f6 | |||
| 8af1b36551 | |||
| ce5421ffce | |||
| 98b1fd8787 | |||
| ed5f1ecde3 | |||
| efbd9922fd | |||
| 463bc2c976 | |||
| e71a70d25c | |||
| 4ae9a2820e | |||
| 3ac1cc9204 | |||
| 2938c93803 | |||
| 7e8d31172c | |||
| e0353a88ce | |||
| 635f3334a6 | |||
| 7800e106cb | |||
| e436e6eae0 | |||
| 0c24b70f23 | |||
| 2693a1598a | |||
| 7a87e90edf | |||
| d333fc1075 | |||
| 1f99db3217 | |||
| 4fa9762a50 | |||
| 1e5bea85b3 | |||
| 739518a6fd | |||
| 24b0ae2c6b | |||
| dae6fc9149 | |||
| 25958a7bb1 | |||
| 8b901789dd | |||
| 09a582192f | |||
| 8735107eb0 | |||
| 5b9e947b8f | |||
| 1f1efac466 | |||
| ee9c5a149b | |||
| 79151cb3aa | |||
| 328530ea2c | |||
| 9e809b002d | |||
| 200226f42d | |||
| 5d99a854e2 | |||
| cd4f0ccad7 | |||
| b572623442 | |||
| ef02fdcf45 | |||
| 25d5c16af0 | |||
| 6f49313f03 | |||
| 55c7456c69 | |||
| 78c435b3c4 | |||
| 5e43f38348 | |||
| 24000acda0 | |||
| 24becf0337 | |||
| 8c80922b6b | |||
| 813a089f0d | |||
| 197bead857 | |||
| 5f7a705394 | |||
| 646c0d79a3 | |||
| 5a1313397e | |||
| 4bc70ac2b8 | |||
| dc5b2f96be | |||
| d10b297d75 | |||
| 933b4cc8f0 | |||
| 4557aff9b6 | |||
| 3a42d700fe | |||
| 1a142a8a39 | |||
| 922b04cd54 | |||
| bdfae8ce24 | |||
| 08a537e509 | |||
| 51d8145f13 | |||
| 7e74f33030 | |||
| 0bb8d9bcd9 | |||
| 19a4cb901b | |||
| c497dff69b | |||
| cd0164b665 | |||
| ab35cd3049 | |||
| 13b2ff3a2e | |||
| e00c1b51c7 | |||
| 0a2a289939 | |||
| 919b6ba7cb | |||
| f0683177d8 | |||
| 13450580d0 | |||
| 2375067d19 | |||
| b2fe0f21cb | |||
| 3e0257ba07 | |||
| 4f078284f3 | |||
| e87fa12fdc | |||
| 354c44d717 | |||
| f55faa0a8a | |||
| eaf9d310aa | |||
| bbd10b657d | |||
| 8600b5558f | |||
| 9c7eb5dc26 | |||
| ac63a6a200 | |||
| 3389994ff9 | |||
| adaee80913 | |||
| 4e467d9308 | |||
| a85fc609cb | |||
| 96f69953f2 | |||
| 9095b46a15 | |||
| 4415927465 | |||
| e6b25119cd | |||
| 09e02aeac8 | |||
| f3de17112a | |||
| 91392c0c7e | |||
| 54683a233f | |||
| 8d12eeb033 | |||
| 0ba35890b1 | |||
| 4783ca5942 | |||
| 1517a60215 | |||
| 4ca3c8b7e2 | |||
| 93081c89c8 | |||
| 09255b626b | |||
| 33fe008b5c | |||
| e72ac3d9d0 | |||
| 14a926aa88 | |||
| 99311dba6d | |||
| 5e9f545e4e | |||
| 0cc8fdf564 | |||
| 27e0e7173a | |||
| 2710c62b82 | |||
| a50de65a66 | |||
| c21758f8e6 | |||
| 1a5f6c2a8f | |||
| d521a84f85 | |||
| aa29a0fa65 | |||
| a8469d7d8d | |||
| d09858cbec | |||
| 855f67bb1e | |||
| 8508620e53 | |||
| e9fbfe36cc | |||
| f158a7865a | |||
| 74860edc6e | |||
| 1712b70fb5 | |||
| 4a57cc5ee9 | |||
| 4db0f72864 | |||
| bfe624dc57 | |||
| 994fdaf436 | |||
| 9ac9146d78 | |||
| fbbfc540c2 | |||
| ac3f42d507 | |||
| 993655fdee | |||
| eff2f550c6 | |||
| aef03ab9fd | |||
| f822373c30 | |||
| 19313f9cc9 | |||
| c3b64b097f | |||
| 6668d735a0 | |||
| e2d801168e | |||
| 86f5223350 | |||
| 9ee6940856 | |||
| bffea06343 | |||
| e0ab14b4ea | |||
| 1cb5f628c8 | |||
| 1ac9704cf4 | |||
| 7ebcc51646 | |||
| f4ccde2734 | |||
| e6d7c5cdfc | |||
| 9e3f32a233 | |||
| 8ed813427f | |||
| 2db0d79af6 | |||
| 28ba662d6f | |||
| e041a83121 | |||
| 42185e59d5 | |||
| 975e9719ad | |||
| 31e51a67db | |||
| d5f829d8d0 | |||
| 0dbf0295b8 | |||
| daaf48f453 | |||
| 1d9e021681 | |||
| 6dd36c74e0 | |||
| b933218762 | |||
| 26f8814a97 | |||
| 236ba7536e | |||
| faaf996b16 | |||
| 5a637fd6e7 | |||
| aca1d30d2f | |||
| 5c3eecb3fd | |||
| 9da3ad2fb7 | |||
| d45d597136 | |||
| 2495d5da99 | |||
| 33aeafd19c | |||
| 374d0ce5e7 | |||
| 371805334b | |||
| 47dbeff0d0 | |||
| 17652b5b77 | |||
| 9608c2a7fc | |||
| 8abe2823d7 | |||
| dbc7f23ab8 | |||
| 357bd935e4 | |||
| f99ca53a6d | |||
| 8700c1a110 | |||
| c5e37b791c | |||
| 307f6387ab | |||
| 652a150a0a | |||
| 2c59badb46 | |||
| 69087bbf1f | |||
| af78f1596a | |||
| fca936a698 | |||
| 54b70f6b3e | |||
| 62f7d440fa | |||
| 752ccbf482 | |||
| a8bc53912d | |||
| ed700c2916 | |||
| 97695444af | |||
| 85e5e1814a | |||
| 88c84a50d0 | |||
| 0004fb3fc8 | |||
| 9cbaf5797b | |||
| 1df75ae82f | |||
| 4d86af5437 | |||
| ba0876fd8b | |||
| a3a3fca694 | |||
| de4396936d | |||
| 164575296f | |||
| 7e8cbfc4c0 | |||
| 679938ccf7 | |||
| 5a6d681bf4 | |||
| 62304b723e | |||
| c89bb4606f | |||
| 14c50e0d57 | |||
| f7e9cf9a29 | |||
| 4bb3f41828 | |||
| e4759ebe25 | |||
| ce33a92f02 | |||
| dd44c07450 | |||
| 15a5b7a820 | |||
| 41b7e095eb | |||
| f34a297fcf | |||
| 9b2c1a320b | |||
| 70ed6f8e6c | |||
| 5a2489f0bf | |||
| a7d035022a | |||
| b879a70b24 | |||
| ec4e9a1d47 | |||
| d9c52c0a7f | |||
| 81ecf18231 | |||
| b0b12a075d | |||
| 54c428083c | |||
| 60228a387a | |||
| 3e04baef00 | |||
| 573bcba1a0 | |||
| ae2fad5db3 | |||
| c8fc12569c | |||
| df8efe3fa4 | |||
| 251131b9b5 | |||
| 35fb61087a | |||
| cbec88ff47 | |||
| f4319616a6 | |||
| 1534b7a67f | |||
| cbfcc9d140 | |||
| 60d10a9222 | |||
| 17e090d5c5 | |||
| fa6b4fa83b | |||
| e4b5244d95 | |||
| 04dc5d6314 | |||
| 33ccb03f90 | |||
| 80011ed3aa | |||
| 3e43cf5959 | |||
| bbd590dde8 | |||
| 0975a951e4 | |||
| 8b78f227a7 | |||
| 5a93a04b61 | |||
| b971eb4191 | |||
| 403e825b8d | |||
| 9164eba88c | |||
| bb83bbac38 | |||
| 651a641b22 | |||
| 441b5fc8dd | |||
| 4fba9445d1 | |||
| 3fb5e01ca5 | |||
| 09b2b0d507 | |||
| a9be35481a | |||
| c871506a69 | |||
| 8e6790d366 | |||
| 46620c5ec9 | |||
| 5f090169da | |||
| 2205150b86 | |||
| 3c4bb8a8fc | |||
| 77d0e71529 | |||
| cf974e2d62 | |||
| ee03db4745 | |||
| efd2061058 | |||
| e7ed20f62f | |||
| 36d4c08a56 | |||
| 2a939e615c | |||
| 9825165286 | |||
| 55c934ac7c | |||
| c7715115ee | |||
| e93b5e8135 | |||
| fd6ba1eda1 | |||
| 34f5411aec | |||
| 2b74ec2ef8 | |||
| 14dd0e8e03 | |||
| b0156261b7 | |||
| 05d520f1c1 | |||
| ccd44c79e8 | |||
| 7be48ab05e | |||
| 2d40b410b5 | |||
| f54df86eec | |||
| 1be476de54 | |||
| 82fa8719a9 | |||
| 7600620c4a | |||
| c5217f3e1e | |||
| 706279852a | |||
| 3a6274504f | |||
| 3aa9398481 | |||
| 7cca435b1d | |||
| 241b5800d1 | |||
| 8abb8acdd3 | |||
| ef8226c091 | |||
| 7484e1bf9a | |||
| 2919fd54b7 | |||
| 9eeb1c986a | |||
| d37cd2418c | |||
| 8bd05f525d | |||
| 47b23b414c | |||
| 6f70d179c7 | |||
| 62a86e9267 | |||
| 6358a2d0b1 | |||
| 273633c2ce | |||
| 8b1209ef73 | |||
| 47505e9748 | |||
| 5178cc6bd8 | |||
| d9a27fff42 | |||
| 9e6560b814 | |||
| afdb19a742 | |||
| 0ae5b668f5 | |||
| 10533e28fa | |||
| 6189e67819 | |||
| f9ad505e40 | |||
| 9b011101ed | |||
| a6ed8bf3aa | |||
| 87acf4cf04 | |||
| b6fe2afd75 | |||
| 6d9bb8eb1c | |||
| 192fd0620f | |||
| 00d0b31980 | |||
| 5edb2131d2 | |||
| 4657aeca45 | |||
| 9f5651a8ba | |||
| 570dcfee29 | |||
| 2c130ce80d | |||
| 8457115105 | |||
| fbc02a494a | |||
| 7e2c254ecf | |||
| 11936a889e | |||
| f33970addd | |||
| 2c02b7193d | |||
| 3f80b598ae | |||
| 477068ed49 | |||
| b4083874ac | |||
| c5d0673b2f | |||
| 9ccc44474b | |||
| 98e341f122 | |||
| b23eba51dd | |||
| 980005a58c | |||
| aba58b1d34 | |||
| bb6a127d22 | |||
| ac6e30a6b6 | |||
| 3f23282eed | |||
| 199a77819c | |||
| 89a53b2854 | |||
| c57bf79b08 | |||
| eabb3392b4 | |||
| c78cc3a38d | |||
| 011ffd538b | |||
| f386576196 | |||
| bab3526f0f | |||
| 115923b422 | |||
| fefa8c8750 | |||
| 3aa80c0f01 | |||
| aea219994a | |||
| 74e22ccecd | |||
| db09cf5ffb | |||
| cc1d13f203 | |||
| 0cd1ce6a79 | |||
| 95254fc2ff | |||
| 205376aadf | |||
| 6f057bfbfc | |||
| bb39481666 | |||
| ddb9968195 | |||
| a309729fa4 | |||
| ba7e065ba6 | |||
| ee05893d4c | |||
| febc63edef | |||
| b3c05c8647 | |||
| cd8701d0e5 | |||
| 3b41edb62d | |||
| ab3b8495df | |||
| b04cd79bcf | |||
| c864e5764a | |||
| 1f71df6c41 | |||
| 0decda57ab | |||
| 5bfd3a4562 | |||
| d4af820ae8 | |||
| 3ec25b7779 | |||
| a9ee12b05e | |||
| a612d1c1fd | |||
| fb48d24e0d | |||
| 38d19d9ea7 | |||
| 4950abc399 | |||
| e3ad804dc4 | |||
| f2f15bc3cc | |||
| 4624a1022a | |||
| cc169da6d4 | |||
| c58ed21661 | |||
| c483733bc3 | |||
| 9738f2f6ae | |||
| ecdd8eb9f6 | |||
| 47e2052ce0 | |||
| a34eb31d9c | |||
| c03cab179e | |||
| 0d61cf906d | |||
| b3c1aa6b4b | |||
| 4904620ce6 | |||
| feaccb593d | |||
| 9ad2baea59 | |||
| df77086039 | |||
| ceb844473a | |||
| a0932b0dc4 | |||
| 0be37716ef | |||
| c07b05a7be | |||
| 94b1da9db0 | |||
| 5fa7a1273f | |||
| cf9088785b | |||
| adf3e3150e | |||
| dbeb63018e | |||
| 51a39d240c | |||
| 4061f8e0e6 | |||
| d32e60249a | |||
| b84a1e43af | |||
| f5c22b63aa | |||
| 41700799c7 | |||
| b15b421975 | |||
| cca4de8684 | |||
| 946c2790c4 | |||
| 3a033f1bab | |||
| 67a04a2840 | |||
| dd48e46854 | |||
| 99edb15c77 | |||
| 8503afeac7 | |||
| 0528637135 | |||
| bf20a2b3be | |||
| 0db55ce4f3 | |||
| 21347e9d0a | |||
| 4333a25c31 | |||
| 5b20e491bd | |||
| bc05f5849d | |||
| b1046bc28d | |||
| d5ab36f42e | |||
| 922d78dcee | |||
| de6506e6b4 | |||
| 9d136c8dd5 | |||
| 26de7f940e | |||
| 7c404ba2ea | |||
| 96d2a72bfa | |||
| 10f41bddad | |||
| 39d2f3ec80 | |||
| 512b446a3d | |||
| c9b96f0488 | |||
| c84ea257d5 | |||
| 6512f5ad2a | |||
| e5d0eced5d | |||
| f424ee5170 | |||
| ea0f6c401d | |||
| 5c5f51b3de | |||
| 7caf02ebba | |||
| f6a444b970 | |||
| d19a36b44f | |||
| aacb126fb5 | |||
| 5adf45cde2 | |||
| 0980aad060 | |||
| 069e5ac8b8 | |||
| b5f6762997 | |||
| c2abfe4b41 | |||
| 3964d03a3b | |||
| a82c4ce499 | |||
| 2f54fa19e6 | |||
| aacb4b3147 | |||
| 60980251f2 | |||
| f8e55f95df | |||
| bebd232af0 | |||
| 1a89fbe612 | |||
| e73584c2aa | |||
| 96a713074f | |||
| aedfa4ca06 | |||
| ac187f722b | |||
| cd87642384 | |||
| 4a736d3211 | |||
| e586940c57 | |||
| ab36a21583 | |||
| c0c1c3b626 | |||
| 918bd7fdb1 | |||
| 971d1a5776 | |||
| 1235d46e73 | |||
| 451b98833b | |||
| 48f9be9712 | |||
| fd8d59bada | |||
| 99ce0b7f9c | |||
| 88ace9ab35 | |||
| 63b0ea60e4 | |||
| b4ecf0f935 | |||
| c846f18086 | |||
| 8a851b06f9 | |||
| 5484dc8bd1 | |||
| 6c47ac36e3 | |||
| 3554496803 | |||
| b8a197615e | |||
| 2ed949920f | |||
| d76f4dade3 | |||
| aacd01ce7c | |||
| d501b016fc | |||
| b1503cfb87 | |||
| 8a004ae9dd | |||
| de709cc7c9 | |||
| 6b7c43925a | |||
| 5d5cc58f59 | |||
| 3e6bab7f15 | |||
| 7e17a8b73b | |||
| 129b798d8f | |||
| a3a411e197 | |||
| 6e6acd6f19 | |||
| 34cb79eeaf | |||
| 1c30a07031 | |||
| 5dd5f41ef5 | |||
| 45a3c11d51 | |||
| 8bd3b4d3f0 | |||
| 4e3cb5806d | |||
| 6563eb4ddd | |||
| 895386f6f8 | |||
| 3810955e56 | |||
| 59c521e53f | |||
| 25d266f8f9 | |||
| 0c3c380591 | |||
| a20cfa30a1 | |||
| fefe899393 | |||
| 55759e8d7a | |||
| ddb561937f | |||
| 198cb71a4c | |||
| c34b880752 | |||
| 76944e3e41 | |||
| 68cd76f2af | |||
| 81145b52b7 | |||
| 2a19dab061 | |||
| 6958d59d4f | |||
| 8a51dfad87 | |||
| 5bb4d9efbe | |||
| 927aa5f24b | |||
| d695bc93a1 | |||
| b05fb4ccbe | |||
| 299eb7e7d6 | |||
| ae26333224 | |||
| 35176469b0 | |||
| 4e74f9cbc5 | |||
| 4091b36f36 | |||
| b3f805fce6 | |||
| b129a3e8d8 | |||
| 64ea1fdb58 | |||
| 8fcf59ed0a | |||
| 9811ca63de | |||
| 9028f88299 | |||
| fd47766d93 | |||
| 26b12c7208 | |||
| 8da9b3454d | |||
| 205cbefc83 | |||
| 0e94c72eef | |||
| c055641351 | |||
| c0a3aa99de | |||
| 8a8976acef | |||
| e409165e1b | |||
| b278140796 | |||
| 397056a54d | |||
| edecd65419 | |||
| 4d2d0b7bd6 | |||
| 0ca4e34efd | |||
| 43f3226c3a | |||
| 0a6dbecc05 | |||
| f5aa179cd6 | |||
| 3140e91dda | |||
| 022f8ff65c | |||
| 5e63cc2e89 | |||
| 880ed99846 | |||
| 222e78c85b | |||
| 050d55c736 | |||
| 13ef8560ff | |||
| 78d990c079 | |||
| 4d3e2c09da | |||
| aa899d247a | |||
| ee0c512529 | |||
| 5f9b522307 | |||
| c207e29980 | |||
| df4d2d6b72 | |||
| c3dd20cabd | |||
| 7a6db95d1a | |||
| bc6825d63b | |||
| 5e79e9e0f2 | |||
| 5e303c2ba8 | |||
| 0bd9c16356 | |||
| f0f5d9da2f | |||
| f46c431f4c | |||
| 62410e9ee2 | |||
| 46f76f1408 | |||
| 5e071e16d8 | |||
| c0238588bd | |||
| 30002d660a | |||
| 48eeb6bca3 | |||
| e67699fed5 | |||
| 8aeae45965 | |||
| ce7491941b | |||
| 1dce03c4f2 | |||
| 62eae6d5d0 | |||
| 15b2b26b84 | |||
| 9664c17c47 | |||
| 8067dad2fa | |||
| 4dcaa510d9 | |||
| b6e918089d | |||
| 1c9e6b1bb8 | |||
| ebd304c252 | |||
| 36083c4173 | |||
| a084b060d8 | |||
| 432c79b606 | |||
| 0f1f0ee933 | |||
| 9b1a4b8d88 | |||
| 1a7a665915 | |||
| 623ecf7fb8 | |||
| 0dc9c6a1a9 | |||
| 72c5eaa5ff | |||
| 0f47b94b7d | |||
| 9abe15f1ad | |||
| 96afda92c8 | |||
| 5c6fd4a739 | |||
| 23b87a876d | |||
| 737fd05369 | |||
| c5bcd89f16 | |||
| 377e1be0b2 | |||
| a92049c0c9 | |||
| 27a2955bba | |||
| cc940e2020 | |||
| 203c80767d | |||
| f564039438 | |||
| d6566fb870 | |||
| 55ab17e789 | |||
| e0eeb720cd | |||
| 53ac7ff257 | |||
| a4e2f10afa | |||
| c9c9d766e6 | |||
| b82d161180 | |||
| 7cadacd8cf | |||
| fc1a7cda62 | |||
| da3bc5aeb7 | |||
| ee98344064 | |||
| da10eabf99 | |||
| ce3c63c386 | |||
| f8c3cff5a9 | |||
| 08f369f8bc | |||
| 1517230ede | |||
| 45aeadef86 | |||
| 5ec7c346ca | |||
| 4b68de1606 | |||
| 7710bcdacc | |||
| 4c19bcc983 | |||
| 2f50b1423a | |||
| 15768c9691 | |||
| 442eb75b8d | |||
| 8facd27125 | |||
| a302cb6ebf | |||
| 32422c7ba9 | |||
| ca131cb156 | |||
| 5f8262ede1 | |||
| 88db9d3693 | |||
| 0fa6f344f9 | |||
| d1642b3dfb | |||
| 7599fce5f5 | |||
| 9bd0dc9a7b | |||
| dad7cafebc | |||
| 4febb28201 | |||
| ced943bad3 | |||
| 202b8717a2 | |||
| eba7026b89 | |||
| 6b2adca1f4 | |||
| 0c9d3cc057 | |||
| 45ec11aaed | |||
| 4d47fa3fac | |||
| 6322d5bb09 | |||
| 2e8a579049 | |||
| 9db7e99d7f | |||
| ef73989995 | |||
| 61024084e9 | |||
| 1c71b2020f | |||
| 3b2b545388 | |||
| fafc7f94fd | |||
| 5fa88b5ce9 | |||
| a8fe4167db | |||
| 0e6b0ddc26 | |||
| af88db3865 | |||
| 0a1bef556c | |||
| 7030181dc1 | |||
| 98cb2f61ab | |||
| a601d0b3d2 | |||
| 8d18923065 | |||
| 4b6a7e0bd7 | |||
| 44f87853d6 | |||
| 4ff47bee03 | |||
| 7a2c28df21 | |||
| f7241d1e4a | |||
| 2b5daf3a75 | |||
| d7184cf75d | |||
| 1313d18d44 | |||
| 605e9481d9 | |||
| cf4c89f825 | |||
| f4f1d4d373 | |||
| 6557b40924 | |||
| e01a29c5d1 | |||
| d9a7e16d63 | |||
| d170199d85 | |||
| e650aae491 | |||
| 49058dbeab | |||
| 31d2d91566 | |||
| abf2a52b46 | |||
| d0ca10e1a1 | |||
| fe99a04174 | |||
| 916900ae6a | |||
| 826c9ba48e | |||
| 7783cc5f30 | |||
| 035d7f19ea | |||
| 294cf15d58 | |||
| 11fa0dcbc6 | |||
| 85b53db439 | |||
| 200df100f4 | |||
| ba202a8572 | |||
| daf05239a1 | |||
| b73b5735ec | |||
| 292248dd35 | |||
| 2e240541c8 | |||
| ab62ae3682 | |||
| 5c3f5d05d3 | |||
| c81af537b6 | |||
| 7295c73371 | |||
| 5a65e08a94 | |||
| 45e5bc7df5 | |||
| a81cb9515c | |||
| 88e5cc2728 | |||
| 1685648328 | |||
| 9334adacf6 | |||
| efbe557dce | |||
| 52b2625486 | |||
| aa0424db08 | |||
| 157619aa4f | |||
| ce637968b2 | |||
| b2a3ed7428 | |||
| ca856e4d88 | |||
| 2ef2536766 | |||
| 1b22633388 | |||
| db306ad4e0 | |||
| 34a44cb9c6 | |||
| 8c499a6e20 | |||
| 3b816a6fd9 | |||
| 35aba8e25a | |||
| 0eb6723f27 | |||
| 2250ec9372 | |||
| ef02b53284 | |||
| 57df7cf3f9 | |||
| f0cd540726 | |||
| 7e919395eb | |||
| facc7252c2 | |||
| 09b8943f69 | |||
| aa84ffc7e1 | |||
| 10162c948b | |||
| e8c1cbdd94 | |||
| 5459f623ec | |||
| 2a2cba3539 | |||
| 1ce84607e6 | |||
| 075c54d39f | |||
| aabb826cb8 | |||
| 3791b1ae1c | |||
| d4b87d098b | |||
| 57e9c13d13 | |||
| e48e570e69 | |||
| b7d4d5b022 | |||
| dc8b0173e9 | |||
| 840729126e | |||
| ed90b97a92 | |||
| bbe4f07651 | |||
| 66df83a86d | |||
| dfedd8f091 | |||
| 58ec6eebfb | |||
| 3ba4130bc6 | |||
| 7dbbf38e78 | |||
| 51e04162a7 | |||
| d3325e2490 | |||
| d2708ee32f | |||
| 781a09c717 | |||
| 11605293dd | |||
| 3f0b946190 | |||
| 74972f053a | |||
| d85190ace1 | |||
| 5755b4ac7f | |||
| 3c24f2edcd | |||
| 6828520853 | |||
| 2ba7dddb95 | |||
| 57a8922d04 | |||
| 640098860a | |||
| 53384d9f3b | |||
| 89d8d98a35 | |||
| 7b78ba6761 | |||
| 3926a9a0c0 | |||
| 0708cd5a38 | |||
| e20e9ca771 | |||
| be1038bafd | |||
| ebc087963b | |||
| 020bdc0811 | |||
| a0d1ad6a47 | |||
| 62e5679791 | |||
| 35568bd299 | |||
| 1e40b377af | |||
| 1073de1b45 | |||
| 1d5788acaf | |||
| 8fb446588d | |||
| 6cec34b2ac | |||
| 763d3e8f74 | |||
| 7da0a913f1 | |||
| ca04c4561b | |||
| 7d8fbf49a8 | |||
| 75e15b948d | |||
| 125b69fd75 | |||
| a68d6b64dd | |||
| a60d4264dc | |||
| 9e2c6b1afa | |||
| 14965a93e9 | |||
| f62664b6a5 | |||
| 60cb7f32f1 | |||
| 008b3ad710 | |||
| 8cae64f496 | |||
| 5cdc1bc762 | |||
| 15c455105b | |||
| 14407a98c9 | |||
| 0d004d5caf | |||
| 4b75a2405c | |||
| 1f7e28b6fb | |||
| c41b2ce861 | |||
| 7f02afc5a6 | |||
| 496b3ffc1b | |||
| e9a395f67a | |||
| 0660f0b7ce | |||
| 3ac09b9dc1 | |||
| fe4904a4af | |||
| d8c8bd17ec | |||
| e9d4d5ba14 | |||
| 5b2e69588f | |||
| c1591402a0 | |||
| e2e9c03895 | |||
| deac4ef56b | |||
| 7c39e658ce | |||
| 6b026f57bc | |||
| dc07cbda6f | |||
| 1cf43fcd42 | |||
| e2cf550bed | |||
| 2917da1138 | |||
| b74eeb5688 | |||
| 0b084a6441 | |||
| 865efa1b12 | |||
| 6a248e5336 | |||
| eb9c256a5d | |||
| 4bd54dcb2d | |||
| 17b035d317 | |||
| 28bcd1fefc | |||
| 59bb1d9124 | |||
| d9255c1cec | |||
| 4ab4bb4cb3 | |||
| a6c8b887e3 | |||
| 1db0abf32d | |||
| ff899b8720 | |||
| 18004c4441 | |||
| ce1cde72bd | |||
| 453f4d92c9 | |||
| 37740e78b4 | |||
| 8ace123179 | |||
| bcdb9de41a | |||
| 9fdb6eb7e5 | |||
| 88cd1d2390 | |||
| 943bcd322d | |||
| 7774128d7e | |||
| b9b9e2ba00 | |||
| 0ce4f20ec5 | |||
| 51b87312c4 | |||
| 9ffd7af8a7 | |||
| 4a453a4f3d | |||
| dc8a472cdb | |||
| d2eabaa4bb | |||
| 39c8ca66d1 | |||
| 806098a5ef | |||
| 5f6cfd9558 | |||
| b4b7ad824b | |||
| 7b5d602f63 | |||
| 7eeeb89457 | |||
| 6e8447b5d1 | |||
| a6445bacf0 | |||
| bd9b4f1b1a | |||
| 9a816b3f07 | |||
| 4dcac23688 | |||
| 97ef6ff997 | |||
| 244a656671 | |||
| f8a2829adb | |||
| 24daadbef8 | |||
| bf2ac88847 | |||
| e42423b100 | |||
| de0c02efaf | |||
| b8290417f8 | |||
| b92205a228 | |||
| 5f642007ba | |||
| ee40d278d4 | |||
| 02d2e8ea92 | |||
| 70715e5e8a | |||
| 5ae4f564b7 | |||
| 123eabd77a | |||
| fd3438a20d | |||
| c8554a12f6 | |||
| 4a687ade9c | |||
| f77aa372cc | |||
| 0b9eef94c4 | |||
| 71b2f69f98 | |||
| 4b61c5307e | |||
| a617b91263 | |||
| fc79bdd0f3 | |||
| e5e1e547d5 | |||
| edac9b0c20 | |||
| dfaf3cf95a | |||
| bae90ce8f3 | |||
| 188e56ce30 | |||
| 19b48b123f | |||
| 549961f297 | |||
| ba7bc68ac3 | |||
| bbfe272d41 | |||
| 8a3e0a31ca | |||
| 8fbda97885 | |||
| 1856deb0f5 | |||
| fec7c5c130 | |||
| 936b4b28bb | |||
| f3092d0778 | |||
| fc1adfae6c | |||
| e279aaed64 | |||
| 4d346a9471 | |||
| cfc504da34 | |||
| 0919a4b9b7 | |||
| f46ad2ea0e | |||
| 252719bc71 | |||
| 45f49361ea | |||
| c4a74c6c7e | |||
| 05f197948d | |||
| 5a1d230538 | |||
| a7ad260a00 | |||
| ef068cccd9 | |||
| 166067920d | |||
| 8227853cf9 | |||
| 324a539b89 | |||
| ce7557353c | |||
| 7b7923fe9b | |||
| 105d5c78e7 | |||
| b25183a8f5 | |||
| adde33d1f5 | |||
| ad325ccb10 | |||
| 2e7ea6969c | |||
| 7401cf69ad | |||
| 7f71c36dc0 | |||
| a3104fda4b | |||
| 44c42310f1 | |||
| a22a8ac5c9 | |||
| aa5c3bac4e | |||
| 30b3beee18 | |||
| b059e43fb1 | |||
| 3b04d0ba19 | |||
| 959f99beae | |||
| ed402933d3 | |||
| ef8bb95884 | |||
| 1b79d2e429 | |||
| ec786748be | |||
| 06f1c7effe | |||
| d78da237fc | |||
| 4c0cce89ee | |||
| 888ced8fd1 | |||
| e1690720b3 | |||
| bbff0a6bc2 | |||
| 5db759150c | |||
| ae239f6700 | |||
| 1d26d10e57 | |||
| da70a4ce7e | |||
| 75ae9f4fad | |||
| 8f7933c111 | |||
| 29a0dedcce | |||
| 4d62993177 | |||
| 8714f33fa2 | |||
| 5dacd50ff6 | |||
| 8d06dcc7b6 | |||
| b8f6dd2584 | |||
| 0650205b86 | |||
| 3e8a0ec49a | |||
| 04d7b32d3f | |||
| eaaf170cc8 | |||
| 09450fb8c7 | |||
| ac0b78eefb | |||
| 90103d9853 | |||
| bf27c73f1d | |||
| 845c9365be | |||
| 91cf5f5c25 | |||
| 783a892e26 | |||
| 41d8f86962 | |||
| 252349579e | |||
| 99b1cfbde4 | |||
| 3f70d912d7 | |||
| bf33c4e7b4 | |||
| 3152842a30 | |||
| d84416b27c | |||
| cc38978bd3 | |||
| 7a76079ff4 | |||
| 2fe28cf126 | |||
| 3ffbfbe0e3 | |||
| 4fad456619 | |||
| 7591f13505 | |||
| 11d06c50a5 | |||
| e0a3489640 | |||
| e55a1d3076 | |||
| 563d431c00 | |||
| 3a1b77ebd8 | |||
| 3f8030a9c5 | |||
| e12e67af0e | |||
| 3ab4cd5d05 | |||
| 738adbed98 | |||
| 365a078600 | |||
| 04fc43e18b | |||
| 54273baec7 | |||
| 51e62ef47b | |||
| a330ebcda7 | |||
| a023fff2d0 | |||
| abb25ea6fb | |||
| ef49bcdb5f | |||
| b4f1b112d6 | |||
| f24ec0ae9d | |||
| ebb51fe37b | |||
| e8ee18f903 | |||
| a593de705c | |||
| 03dd024704 | |||
| 528c3535dd | |||
| 0e0f80a2d0 | |||
| 6b67fb136a | |||
| 9fe1c14869 | |||
| 8a96dddf54 | |||
| 230422c98b | |||
| d16ffc531f | |||
| f614199ea5 | |||
| 55a1c2e9e3 | |||
| bee1f77812 | |||
| fdf982ada5 | |||
| ff02fc7855 | |||
| 01ed289400 | |||
| aedb2db655 | |||
| 10a54b9de0 | |||
| ccd029c040 | |||
| 3a431841b7 | |||
| deceae8354 | |||
| c8628670cf | |||
| ffe53d5596 | |||
| a4f4ecb569 | |||
| 2097f42efb | |||
| 9c59f56aac | |||
| dfcc4107b7 | |||
| ef71abfff1 | |||
| bc916f3a6e | |||
| c7ff0dcbf6 | |||
| 7242f9bfd0 | |||
| bb2e865880 | |||
| 6ab3cf9ac9 | |||
| b77f5c9ecc | |||
| b470dbd6b9 | |||
| 1f96b6b44d | |||
| de0b228ae8 | |||
| f35d192650 | |||
| 794d00ce9e | |||
| 739e7a448b | |||
| 7fa8a454b6 | |||
| 5cd1d9abe8 | |||
| e0e17cac99 | |||
| 840039330f | |||
| 734409dc3f | |||
| 34564c8c55 | |||
| afe6accab8 | |||
| b6e7e75ae8 | |||
| 06dc0e80f0 | |||
| 47cccbce7c | |||
| 269352af97 | |||
| fa62f79dce | |||
| 9f88b37f41 | |||
| 55ae9eac1e | |||
| 05564d4a58 | |||
| 59426c56db | |||
| 18cd4c0c9a | |||
| a0e2a33e28 | |||
| 7bdb46e161 | |||
| f560b62de0 | |||
| adc1f6822b | |||
| 2da29fcfa7 | |||
| c5d0314db6 | |||
| 8c052faedd | |||
| 37067ff950 | |||
| 6366dc026e | |||
| 6e52178074 | |||
| 47f38cc690 | |||
| fdd6d9929f | |||
| 1707261f49 | |||
| 6712fced6d | |||
| 6dabfaa9ba | |||
| a41db79c35 | |||
| 87786d9aef | |||
| 22f5866050 | |||
| 04894fbcf5 | |||
| c17c624ba4 | |||
| bfe7249df8 | |||
| 13c570efe9 | |||
| b299846f0f | |||
| 59e9289d27 | |||
| 8dc29caa1b | |||
| 7fedf88654 | |||
| 5da0202425 | |||
| 6288d0b171 | |||
| 4248d20e8e | |||
| 0b413492ad | |||
| dc73561c8a | |||
| 949a2f6428 | |||
| bceaa05197 | |||
| 776cdac30d | |||
| 4333891cca | |||
| 8a89bbccf7 | |||
| fa4c69d228 | |||
| c25def8901 | |||
| 284a59b721 | |||
| 5fcba8619a | |||
| f3cd759276 | |||
| 9d3981e361 | |||
| 787326948b | |||
| 779251933c | |||
| 1efe835c69 | |||
| 5702978227 | |||
| fa3d742838 | |||
| c460cc2296 | |||
| 4e4af5e830 | |||
| 9a4e98063b | |||
| 8bfe04bb50 | |||
| 6774d54f5e | |||
| 9705f8489d | |||
| a7229cbe14 | |||
| 7577aba45e | |||
| d78fbe476e | |||
| bfe4b2bba7 | |||
| 7625a3aa52 | |||
| 30c8dcf730 | |||
| 00a3e8d35e | |||
| 4d01cdfa6c | |||
| f924b6c8e3 | |||
| 926d98174c | |||
| 41b3972f54 | |||
| 467f29e363 | |||
| 9cc13c3757 | |||
| f8ccb86156 | |||
| b316aa2301 | |||
| 5c49b28664 | |||
| dedf96afd3 | |||
| 3bb5bc2ca1 | |||
| c79fdd9887 | |||
| d7b821727d | |||
| 21c45faf20 | |||
| 92cab89d17 |
@ -1,3 +1 @@
|
||||
.eslintrc.js
|
||||
rollup.main.config.ts
|
||||
rollup.preload.config.ts
|
||||
|
||||
13
.eslintrc.js
13
.eslintrc.js
@ -7,7 +7,7 @@ module.exports = {
|
||||
'plugin:@typescript-eslint/recommended',
|
||||
'plugin:@typescript-eslint/recommended-requiring-type-checking',
|
||||
],
|
||||
plugins: ['@typescript-eslint', 'import'],
|
||||
plugins: ['prettier', '@typescript-eslint', 'import'],
|
||||
parser: '@typescript-eslint/parser',
|
||||
parserOptions: {
|
||||
project: './tsconfig.json',
|
||||
@ -26,6 +26,7 @@ module.exports = {
|
||||
'import/newline-after-import': 'error',
|
||||
'import/no-default-export': 'off',
|
||||
'import/no-duplicates': 'error',
|
||||
'import/no-unresolved': ['error', { ignore: ['^virtual:', '\\?inline$', '\\?raw$', '\\?asset&asarUnpack'] }],
|
||||
'import/order': [
|
||||
'error',
|
||||
{
|
||||
@ -66,4 +67,14 @@ module.exports = {
|
||||
es6: true,
|
||||
},
|
||||
ignorePatterns: ['dist', 'node_modules'],
|
||||
root: true,
|
||||
settings: {
|
||||
'import/parsers': {
|
||||
'@typescript-eslint/parser': ['.ts']
|
||||
},
|
||||
'import/resolver': {
|
||||
typescript: {},
|
||||
exports: {},
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
30
.github/ISSUE_TEMPLATE/bug_report.yml
vendored
30
.github/ISSUE_TEMPLATE/bug_report.yml
vendored
@ -12,16 +12,24 @@ body:
|
||||
required: true
|
||||
- label: I have searched the [issue tracker](https://github.com/th-ch/youtube-music/issues) for a bug report that matches the one I want to file, without success.
|
||||
required: true
|
||||
- label: I understand that **th-ch/youtube-music has NO affiliation with Google or YouTube**
|
||||
required: true
|
||||
- type: input
|
||||
attributes:
|
||||
label: YouTube Music (Application) Version
|
||||
description: |
|
||||
What version of YouTube Music Application are you using?
|
||||
What version of the YouTube Music Application are you using?
|
||||
|
||||
Note: Please check if this issue is reproducible with the latest stable release.
|
||||
placeholder: 2.0.0
|
||||
validations:
|
||||
required: true
|
||||
- type: checkboxes
|
||||
attributes:
|
||||
label: Checklists
|
||||
options:
|
||||
- label: I use the portable version of the YouTube Music Application.
|
||||
- label: I can reproduce this issue in the [official version of (WEB) YTM](https://music.youtube.com).
|
||||
- type: dropdown
|
||||
attributes:
|
||||
label: What operating system are you using?
|
||||
@ -36,13 +44,13 @@ body:
|
||||
- type: input
|
||||
attributes:
|
||||
label: Operating System Version
|
||||
description: What operating system version are you using? On Windows, click Start button > Settings > System > About. On macOS, click the Apple Menu > About This Mac. On Linux, use lsb_release or uname -a.
|
||||
description: What operating system version are you using? On Windows, click the Start button > Settings > System > About. On macOS, click the Apple Menu > About This Mac. On Linux, use lsb_release or uname -a.
|
||||
placeholder: "e.g. Windows 10 version 1909, macOS Catalina 10.15.7, or Ubuntu 20.04"
|
||||
validations:
|
||||
required: true
|
||||
- type: dropdown
|
||||
attributes:
|
||||
label: What arch are you using?
|
||||
label: What CPU architecture are you using?
|
||||
options:
|
||||
- x64
|
||||
- ia32
|
||||
@ -55,10 +63,17 @@ body:
|
||||
label: Last Known Working YouTube Music (Application) version
|
||||
description: (If applicable) What is the last version of YouTube Music this worked in?
|
||||
placeholder: 1.20.0
|
||||
- type: textarea
|
||||
attributes:
|
||||
label: Reproduction steps
|
||||
description: Provide steps to reproduce the issue.
|
||||
placeholder: 1. Enable the X plugin.
|
||||
validations:
|
||||
required: true
|
||||
- type: textarea
|
||||
attributes:
|
||||
label: Expected Behavior
|
||||
description: A clear and concise description of what you expected to happen. (Add a replication step if applicable)
|
||||
description: A clear and concise description of what you expected to happen.
|
||||
validations:
|
||||
required: true
|
||||
- type: textarea
|
||||
@ -67,6 +82,13 @@ body:
|
||||
description: A clear description of what actually happens.
|
||||
validations:
|
||||
required: true
|
||||
- type: textarea
|
||||
attributes:
|
||||
label: Enabled plugins
|
||||
description: Provide the list of plugins you enabled.
|
||||
placeholder: 1. Album Color Theme
|
||||
validations:
|
||||
required: true
|
||||
- type: textarea
|
||||
attributes:
|
||||
label: Additional Information
|
||||
|
||||
4
.github/ISSUE_TEMPLATE/feature_request.yml
vendored
4
.github/ISSUE_TEMPLATE/feature_request.yml
vendored
@ -15,7 +15,7 @@ body:
|
||||
- type: textarea
|
||||
attributes:
|
||||
label: Problem Description
|
||||
description: Please add a clear and concise description of the problem you are seeking to solve with this feature request.
|
||||
description: A clear and concise description of the problem you are seeking to solve with this feature request.
|
||||
validations:
|
||||
required: true
|
||||
- type: textarea
|
||||
@ -33,6 +33,6 @@ body:
|
||||
- type: textarea
|
||||
attributes:
|
||||
label: Additional Information
|
||||
description: Add any other context about the problem here.
|
||||
description: Any other context about the problem.
|
||||
validations:
|
||||
required: false
|
||||
|
||||
112
.github/workflows/build.yml
vendored
112
.github/workflows/build.yml
vendored
@ -20,59 +20,64 @@ jobs:
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
|
||||
- name: Install pnpm
|
||||
uses: pnpm/action-setup@v2
|
||||
with:
|
||||
version: 9
|
||||
run_install: false
|
||||
|
||||
- name: Setup NodeJS
|
||||
uses: actions/setup-node@v3
|
||||
if: startsWith(matrix.os, 'macOS') != true
|
||||
uses: actions/setup-node@v4
|
||||
with:
|
||||
node-version: ${{ env.NODE_VERSION }}
|
||||
cache: 'pnpm'
|
||||
|
||||
- name: Setup NodeJS for macOS
|
||||
if: startsWith(matrix.os, 'macOS')
|
||||
uses: actions/setup-node@v4
|
||||
with:
|
||||
node-version: ${{ env.NODE_VERSION }}
|
||||
cache: 'npm'
|
||||
|
||||
- name: Install dependencies
|
||||
run: npm ci
|
||||
run: pnpm install --frozen-lockfile
|
||||
|
||||
# Only vite build without release if it is a fork, or it is a pull-request
|
||||
- name: Vite Build
|
||||
if: github.repository == 'th-ch/youtube-music' && github.event_name == 'pull_request'
|
||||
run: |
|
||||
pnpm build
|
||||
|
||||
# Build and release if it's the main repository and is not pull-request
|
||||
- name: Build and release on Mac
|
||||
if: startsWith(matrix.os, 'macOS') && (github.repository == 'th-ch/youtube-music' && github.event_name != 'pull_request')
|
||||
env:
|
||||
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
run: |
|
||||
pnpm release:mac
|
||||
|
||||
- name: Build and release on Linux
|
||||
if: startsWith(matrix.os, 'ubuntu') && (github.repository == 'th-ch/youtube-music' && github.event_name != 'pull_request')
|
||||
env:
|
||||
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
run: |
|
||||
sudo snap install snapcraft --classic
|
||||
pnpm release:linux
|
||||
|
||||
- name: Build and release on Windows
|
||||
if: startsWith(matrix.os, 'windows') && (github.repository == 'th-ch/youtube-music' && github.event_name != 'pull_request')
|
||||
env:
|
||||
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
run: |
|
||||
pnpm release:win
|
||||
|
||||
- name: Test
|
||||
uses: GabrielBB/xvfb-action@v1
|
||||
uses: coactions/setup-xvfb@v1
|
||||
env:
|
||||
PLAYWRIGHT_SKIP_BROWSER_DOWNLOAD: 1
|
||||
with:
|
||||
run: npm run test
|
||||
run: pnpm test:debug
|
||||
|
||||
# Build and release if it's the main repository
|
||||
- name: Build and release on Mac
|
||||
if: startsWith(matrix.os, 'macOS') && github.repository == 'th-ch/youtube-music'
|
||||
env:
|
||||
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
run: |
|
||||
npm run release:mac
|
||||
|
||||
- name: Build and release on Linux
|
||||
if: startsWith(matrix.os, 'ubuntu') && github.repository == 'th-ch/youtube-music'
|
||||
env:
|
||||
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
run: |
|
||||
npm run release:linux
|
||||
|
||||
- name: Build and release on Windows
|
||||
if: startsWith(matrix.os, 'windows') && github.repository == 'th-ch/youtube-music'
|
||||
env:
|
||||
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
run: |
|
||||
npm run release:win
|
||||
|
||||
# Only build without release if it is a fork
|
||||
- name: Build on Mac
|
||||
if: startsWith(matrix.os, 'macOS') && github.repository != 'th-ch/youtube-music'
|
||||
run: |
|
||||
npm run build:mac
|
||||
|
||||
- name: Build on Linux
|
||||
if: startsWith(matrix.os, 'ubuntu') && github.repository != 'th-ch/youtube-music'
|
||||
run: |
|
||||
npm run build:linux
|
||||
|
||||
- name: Build on Windows
|
||||
if: startsWith(matrix.os, 'windows') && github.repository != 'th-ch/youtube-music'
|
||||
run: |
|
||||
npm run build:win
|
||||
|
||||
release:
|
||||
runs-on: ubuntu-latest
|
||||
@ -84,14 +89,27 @@ jobs:
|
||||
with:
|
||||
fetch-depth: 0
|
||||
|
||||
- name: Install pnpm
|
||||
uses: pnpm/action-setup@v2
|
||||
with:
|
||||
version: 9
|
||||
run_install: false
|
||||
|
||||
- name: Setup NodeJS
|
||||
uses: actions/setup-node@v3
|
||||
if: startsWith(matrix.os, 'macOS') != true
|
||||
uses: actions/setup-node@v4
|
||||
with:
|
||||
node-version: ${{ env.NODE_VERSION }}
|
||||
cache: 'pnpm'
|
||||
|
||||
- name: Setup NodeJS for macOS
|
||||
if: startsWith(matrix.os, 'macOS')
|
||||
uses: actions/setup-node@v4
|
||||
with:
|
||||
node-version: ${{ env.NODE_VERSION }}
|
||||
cache: 'npm'
|
||||
|
||||
- name: Install dependencies
|
||||
run: npm ci
|
||||
run: pnpm install --frozen-lockfile
|
||||
|
||||
- name: Get version
|
||||
run: |
|
||||
@ -129,10 +147,12 @@ jobs:
|
||||
|
||||
Thanks to all contributors! 🏅
|
||||
|
||||
(Note for Windows: `YouTube-Music-Web-Setup-${{ env.VERSION_TAG }}.exe` is an installer, and `YouTube-Music-${{ env.VERSION_TAG }}.exe` is a portable version)
|
||||
|
||||
- name: Update changelog
|
||||
if: ${{ env.VERSION_HASH == '' }}
|
||||
run: |
|
||||
npm run changelog
|
||||
pnpm changelog
|
||||
|
||||
- name: Commit changelog
|
||||
if: ${{ env.VERSION_HASH == '' }}
|
||||
|
||||
2
.github/workflows/dependency-review.yml
vendored
2
.github/workflows/dependency-review.yml
vendored
@ -17,4 +17,4 @@ jobs:
|
||||
- name: "Checkout Repository"
|
||||
uses: actions/checkout@v4
|
||||
- name: "Dependency Review"
|
||||
uses: actions/dependency-review-action@v3
|
||||
uses: actions/dependency-review-action@v4
|
||||
|
||||
6
.github/workflows/winget-submission.yml
vendored
6
.github/workflows/winget-submission.yml
vendored
@ -15,12 +15,16 @@ jobs:
|
||||
name: Publish winget package
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Set winget version env
|
||||
env:
|
||||
TAG_NAME: ${{ inputs.tag_name || github.event.release.tag_name }}
|
||||
run: echo "WINGET_TAG_NAME=$(echo ${TAG_NAME#v})" >> $GITHUB_ENV
|
||||
- name: Submit package to Windows Package Manager Community Repository
|
||||
uses: vedantmgoyal2009/winget-releaser@v2
|
||||
with:
|
||||
identifier: th-ch.YouTubeMusic
|
||||
installers-regex: '^YouTube-Music-Web-Setup-[\d\.]+\.exe$'
|
||||
version: ${{ inputs.tag_name || github.event.release.tag_name }}
|
||||
version: ${{ env.WINGET_TAG_NAME }}
|
||||
release-tag: ${{ inputs.tag_name || github.event.release.tag_name }}
|
||||
token: ${{ secrets.WINGET_ACC_TOKEN }}
|
||||
fork-user: youtube-music-winget
|
||||
|
||||
1
.gitignore
vendored
1
.gitignore
vendored
@ -12,3 +12,4 @@ electron-builder.yml
|
||||
!.yarn/releases
|
||||
!.yarn/sdks
|
||||
!.yarn/versions
|
||||
.vite-inspect
|
||||
|
||||
5
.prettierrc
Normal file
5
.prettierrc
Normal file
@ -0,0 +1,5 @@
|
||||
{
|
||||
"tabWidth": 2,
|
||||
"useTabs": false,
|
||||
"singleQuote": true
|
||||
}
|
||||
@ -1,31 +1,176 @@
|
||||
# YouTube Music
|
||||
|
||||
<div align="center">
|
||||
|
||||
# YouTube Music
|
||||
|
||||
[](https://github.com/th-ch/youtube-music/releases/)
|
||||
[](https://github.com/th-ch/youtube-music/blob/master/LICENSE)
|
||||
[](https://github.com/th-ch/youtube-music/blob/master/.eslintrc.js)
|
||||
[](https://GitHub.com/th-ch/youtube-music/releases/)
|
||||
[](https://snyk.io/test/github/th-ch/youtube-music)
|
||||
[](https://GitHub.com/th-ch/youtube-music/releases/)
|
||||
[](https://aur.archlinux.org/packages/youtube-music-bin)
|
||||
[](https://snyk.io/test/github/th-ch/youtube-music)
|
||||
|
||||
</div>
|
||||
|
||||

|
||||
|
||||
|
||||
<div align="center">
|
||||
<a href="https://github.com/th-ch/youtube-music/releases/latest">
|
||||
<img src="web/youtube-music.svg" width="400" height="100">
|
||||
<img src="web/youtube-music.svg" width="400" height="100" alt="YouTube Music SVG">
|
||||
</a>
|
||||
</div>
|
||||
|
||||
Read this in other languages: [🇰🇷](./docs/readme/README-ko.md), [🇮🇸](./docs/readme/README-is.md), [🇨🇱 🇪🇸](./docs/readme/README-es.md), [🇷🇺](./docs/readme/README-ru.md)
|
||||
|
||||
**Electron wrapper around YouTube Music featuring:**
|
||||
|
||||
- Native look & feel, aims at keeping the original interface
|
||||
- Framework for custom plugins: change YouTube Music to your needs (style, content, features), enable/disable plugins in
|
||||
one click
|
||||
|
||||
## Demo Image
|
||||
|
||||
| Player Screen (album color theme & ambient light) |
|
||||
|:---------------------------------------------------------------------------------------------------------:|
|
||||
||
|
||||
|
||||
## Content
|
||||
|
||||
- [Features](#features)
|
||||
- [Available plugins](#available-plugins)
|
||||
- [Translation](#translation)
|
||||
- [Download](#download)
|
||||
- [Arch Linux](#arch-linux)
|
||||
- [MacOS](#macos)
|
||||
- [Windows](#windows)
|
||||
- [How to install without a network connection? (in Windows)](#how-to-install-without-a-network-connection-in-windows)
|
||||
- [Themes](#themes)
|
||||
- [Dev](#dev)
|
||||
- [Build your own plugins](#build-your-own-plugins)
|
||||
- [Creating a plugin](#creating-a-plugin)
|
||||
- [Common use cases](#common-use-cases)
|
||||
- [Build](#build)
|
||||
- [Production Preview](#production-preview)
|
||||
- [Tests](#tests)
|
||||
- [License](#license)
|
||||
- [FAQ](#faq)
|
||||
|
||||
## Features:
|
||||
|
||||
- **Auto confirm when paused** (Always Enabled): disable
|
||||
the ["Continue Watching?"](https://user-images.githubusercontent.com/61631665/129977894-01c60740-7ec6-4bf0-9a2c-25da24491b0e.png)
|
||||
popup that pause music after a certain time
|
||||
|
||||
- And more ...
|
||||
|
||||
## Available plugins:
|
||||
|
||||
- **Ad Blocker**: Block all ads and tracking out of the box
|
||||
|
||||
- **Album Actions**: Adds Undislike, Dislike, Like, and Unlike buttons to apply this to all songs in a playlist or album
|
||||
|
||||
- **Album Color Theme**: Applies a dynamic theme and visual effects based on the album color palette
|
||||
|
||||
- **Ambient Mode**: Applies a lighting effect by casting gentle colors from the video, into your screen’s background
|
||||
|
||||
- **Audio Compressor**: Apply compression to audio (lowers the volume of the loudest parts of the signal and raises the
|
||||
volume of the softest parts)
|
||||
|
||||
- **Blur Navigation Bar**: makes navigation bar transparent and blurry
|
||||
|
||||
- **Bypass Age Restrictions**: bypass YouTube's age verification
|
||||
|
||||
- **Captions Selector**: Enable captions
|
||||
|
||||
- **Compact Sidebar**: Always set the sidebar in compact mode
|
||||
|
||||
- **Crossfade**: Crossfade between songs
|
||||
|
||||
- **Disable Autoplay**: Makes every song start in "paused" mode
|
||||
|
||||
- **[Discord](https://discord.com/) Rich Presence**: Show your friends what you listen to
|
||||
with [Rich Presence](https://user-images.githubusercontent.com/28219076/104362104-a7a0b980-5513-11eb-9744-bb89eabe0016.png)
|
||||
|
||||
- **Downloader**: downloads
|
||||
MP3 [directly from the interface](https://user-images.githubusercontent.com/61631665/129977677-83a7d067-c192-45e1-98ae-b5a4927393be.png) [(youtube-dl)](https://github.com/ytdl-org/youtube-dl)
|
||||
|
||||
- **Exponential Volume**: Makes the volume
|
||||
slider [exponential](https://greasyfork.org/en/scripts/397686-youtube-music-fix-volume-ratio/) so it's easier to
|
||||
select lower volumes
|
||||
|
||||
- **In-App Menu**: [gives bars a fancy, dark look](https://user-images.githubusercontent.com/78568641/112215894-923dbf00-8c29-11eb-95c3-3ce15db27eca.png)
|
||||
|
||||
> (see [this post](https://github.com/th-ch/youtube-music/issues/410#issuecomment-952060709) if you have problem
|
||||
accessing the menu after enabling this plugin and hide-menu option)
|
||||
|
||||
- **Scrobbler**: Adds scrobbling support for [Last.fm](https://www.last.fm/) and [ListenBrainz](https://listenbrainz.org/)
|
||||
|
||||
- **Lumia Stream**: Adds [Lumia Stream](https://lumiastream.com/) support
|
||||
|
||||
- **Lyrics Genius**: Adds lyrics support for most songs
|
||||
|
||||
- **Music Together**: Share a playlist with others. When the host plays a song, everyone else will hear the same song
|
||||
|
||||
- **Navigation**: Next/Back navigation arrows directly integrated in the interface, like in your favorite browser
|
||||
|
||||
- **No Google Login**: Remove Google login buttons and links from the interface
|
||||
|
||||
- **Notifications**: Display a notification when a song starts
|
||||
playing ([interactive notifications](https://user-images.githubusercontent.com/78568641/114102651-63ce0e00-98d0-11eb-9dfe-c5a02bb54f9c.png)
|
||||
are available on windows)
|
||||
|
||||
- **Picture-in-picture**: allows to switch the app to picture-in-picture mode
|
||||
|
||||
- **Playback Speed**: Listen fast, listen
|
||||
slow! [Adds a slider that controls song speed](https://user-images.githubusercontent.com/61631665/129976003-e55db5ba-bf42-448c-a059-26a009775e68.png)
|
||||
|
||||
- **Precise Volume**: Control the volume precisely using mousewheel/hotkeys, with a custom hud and customizable volume
|
||||
steps
|
||||
|
||||
- **Shortcuts (& MPRIS)**: Allows setting global hotkeys for playback (play/pause/next/previous) +
|
||||
disable [media osd](https://user-images.githubusercontent.com/84923831/128601225-afa38c1f-dea8-4209-9f72-0f84c1dd8b54.png)
|
||||
by overriding media keys + enable Ctrl/CMD + F to search + enable linux mpris support for
|
||||
mediakeys + [custom hotkeys](https://github.com/Araxeus/youtube-music/blob/1e591d6a3df98449bcda6e63baab249b28026148/providers/song-controls.js#L13-L50)
|
||||
for [advanced users](https://github.com/th-ch/youtube-music/issues/106#issuecomment-952156902)
|
||||
|
||||
- **Skip Disliked Song**: Skips disliked songs
|
||||
|
||||
- **Skip Silences**: Automatically skip silenced sections
|
||||
|
||||
- [**SponsorBlock**](https://github.com/ajayyy/SponsorBlock): Automatically Skips non-music parts like intro/outro or
|
||||
parts of music videos where the song isn't playing
|
||||
|
||||
- **Synced Lyrics**: Provides synced lyrics to songs, using providers like [LRClib](https://lrclib.net).
|
||||
|
||||
- **Taskbar Media Control**: Control playback from
|
||||
your [Windows taskbar](https://user-images.githubusercontent.com/78568641/111916130-24a35e80-8a82-11eb-80c8-5021c1aa27f4.png)
|
||||
|
||||
- **TouchBar**: Custom TouchBar layout for macOS
|
||||
|
||||
- **Tuna OBS**: Integration with [OBS](https://obsproject.com/)'s
|
||||
plugin [Tuna](https://obsproject.com/forum/resources/tuna.843/)
|
||||
|
||||
- **Video Quality Changer**: Allows changing the video quality with
|
||||
a [button](https://user-images.githubusercontent.com/78568641/138574366-70324a5e-2d64-4f6a-acdd-dc2a2b9cecc5.png) on
|
||||
the video overlay
|
||||
|
||||
- **Video Toggle**: Adds
|
||||
a [button](https://user-images.githubusercontent.com/28893833/173663950-63e6610e-a532-49b7-9afa-54cb57ddfc15.png) to
|
||||
switch between Video/Song mode. can also optionally remove the whole video tab
|
||||
|
||||
- **Visualizer**: Different music visualizers
|
||||
|
||||
|
||||
## Translation
|
||||
|
||||
You can help with translation on [Hosted Weblate](https://hosted.weblate.org/projects/youtube-music/).
|
||||
|
||||
<a href="https://hosted.weblate.org/engage/youtube-music/">
|
||||
<img src="https://hosted.weblate.org/widget/youtube-music/i18n/multi-auto.svg" alt="translation status" />
|
||||
<img src="https://hosted.weblate.org/widget/youtube-music/i18n/287x66-black.png" alt="translation status 2" />
|
||||
</a>
|
||||
|
||||
## Download
|
||||
|
||||
You can check out the [latest release](https://github.com/th-ch/youtube-music/releases/latest) to quickly find the
|
||||
@ -33,12 +178,18 @@ latest version.
|
||||
|
||||
### Arch Linux
|
||||
|
||||
Install the `youtube-music-bin` package from the AUR. For AUR installation instructions, take a look at
|
||||
Install the [`youtube-music-bin`](https://aur.archlinux.org/packages/youtube-music-bin) package from the AUR. For AUR installation instructions, take a look at
|
||||
this [wiki page](https://wiki.archlinux.org/index.php/Arch_User_Repository#Installing_packages).
|
||||
|
||||
### MacOS
|
||||
### macOS
|
||||
|
||||
If you get an error "is damaged and can’t be opened." when launching the app, run the following in the Terminal:
|
||||
You can install the app using Homebrew (see the [cask definition](https://github.com/th-ch/homebrew-youtube-music)):
|
||||
|
||||
```bash
|
||||
brew install th-ch/youtube-music/youtube-music
|
||||
```
|
||||
|
||||
If you install the app manually and get an error "is damaged and can’t be opened." when launching the app, run the following in the Terminal:
|
||||
|
||||
```bash
|
||||
xattr -cr /Applications/YouTube\ Music.app
|
||||
@ -75,98 +226,6 @@ winget install th-ch.YouTubeMusic
|
||||
- Place them in the **same directory**.
|
||||
- Run the installer.
|
||||
|
||||
## Available plugins:
|
||||
|
||||
- **Ad Blocker**: Block all ads and tracking out of the box
|
||||
|
||||
- **Audio Compressor**: Apply compression to audio (lowers the volume of the loudest parts of the signal and raises the
|
||||
volume of the softest parts)
|
||||
|
||||
- **Blur Nav Bar**: makes navigation bar transparent and blurry
|
||||
|
||||
- **Bypass age restrictions**: bypass YouTube's age verification
|
||||
|
||||
- **Captions selector**: Enable captions
|
||||
|
||||
- **Compact sidebar**: Always set the sidebar in compact mode
|
||||
|
||||
- **Crossfade**: Crossfade between songs
|
||||
|
||||
- **Disable Autoplay**: Makes every song start in "paused" mode
|
||||
|
||||
- [**Discord**](https://discord.com/): Show your friends what you listen to
|
||||
with [Rich Presence](https://user-images.githubusercontent.com/28219076/104362104-a7a0b980-5513-11eb-9744-bb89eabe0016.png)
|
||||
|
||||
- **Downloader**: downloads
|
||||
MP3 [directly from the interface](https://user-images.githubusercontent.com/61631665/129977677-83a7d067-c192-45e1-98ae-b5a4927393be.png) [(youtube-dl)](https://github.com/ytdl-org/youtube-dl)
|
||||
|
||||
- **Exponential Volume**: Makes the volume
|
||||
slider [exponential](https://greasyfork.org/en/scripts/397686-youtube-music-fix-volume-ratio/) so it's easier to
|
||||
select lower volumes.
|
||||
|
||||
- **In-App Menu**: [gives bars a fancy, dark look](https://user-images.githubusercontent.com/78568641/112215894-923dbf00-8c29-11eb-95c3-3ce15db27eca.png)
|
||||
|
||||
> (see [this post](https://github.com/th-ch/youtube-music/issues/410#issuecomment-952060709) if you have problem
|
||||
accessing the menu after enabling this plugin and hide-menu option)
|
||||
|
||||
- [**Last.fm**](https://www.last.fm/): Scrobbles support
|
||||
|
||||
- **Lyrics Genius**: Adds lyrics support for most songs
|
||||
|
||||
- **Navigation**: Next/Back navigation arrows directly integrated in the interface, like in your favorite browser
|
||||
|
||||
- **No Google Login**: Remove Google login buttons and links from the interface
|
||||
|
||||
- **Notifications**: Display a notification when a song starts
|
||||
playing ([interactive notifications](https://user-images.githubusercontent.com/78568641/114102651-63ce0e00-98d0-11eb-9dfe-c5a02bb54f9c.png)
|
||||
are available on windows)
|
||||
|
||||
- **Picture in picture**: allows to switch the app to picture-in-picture mode
|
||||
|
||||
- **Playback Speed**: Listen fast, listen
|
||||
slow! [Adds a slider that controls song speed](https://user-images.githubusercontent.com/61631665/129976003-e55db5ba-bf42-448c-a059-26a009775e68.png)
|
||||
|
||||
- **Precise Volume**: Control the volume precisely using mousewheel/hotkeys, with a custom hud and customizable volume
|
||||
steps
|
||||
|
||||
- **Quality Changer**: Allows changing the video quality with
|
||||
a [button](https://user-images.githubusercontent.com/78568641/138574366-70324a5e-2d64-4f6a-acdd-dc2a2b9cecc5.png) on
|
||||
the video overlay
|
||||
|
||||
- **Shortcuts**: Allows setting global hotkeys for playback (play/pause/next/previous) +
|
||||
disable [media osd](https://user-images.githubusercontent.com/84923831/128601225-afa38c1f-dea8-4209-9f72-0f84c1dd8b54.png)
|
||||
by overriding media keys + enable Ctrl/CMD + F to search + enable linux mpris support for
|
||||
mediakeys + [custom hotkeys](https://github.com/Araxeus/youtube-music/blob/1e591d6a3df98449bcda6e63baab249b28026148/providers/song-controls.js#L13-L50)
|
||||
for [advanced users](https://github.com/th-ch/youtube-music/issues/106#issuecomment-952156902)
|
||||
|
||||
- **Skip-Silences** - Automatically skip silenced sections
|
||||
|
||||
- [**SponsorBlock**](https://github.com/ajayyy/SponsorBlock): Automatically Skips non-music parts like intro/outro or
|
||||
parts of music videos where the song isn't playing
|
||||
|
||||
- **Taskbar Media Control**: Control playback from
|
||||
your [Windows taskbar](https://user-images.githubusercontent.com/78568641/111916130-24a35e80-8a82-11eb-80c8-5021c1aa27f4.png)
|
||||
|
||||
- **Touchbar**: Custom TouchBar layout for macOS
|
||||
|
||||
- **Tuna-OBS**: Integration with [OBS](https://obsproject.com/)'s
|
||||
plugin [Tuna](https://obsproject.com/forum/resources/tuna.843/)
|
||||
|
||||
- **Video Toggle**: Adds
|
||||
a [button](https://user-images.githubusercontent.com/28893833/173663950-63e6610e-a532-49b7-9afa-54cb57ddfc15.png) to
|
||||
switch between Video/Song mode. can also optionally remove the whole video tab
|
||||
|
||||
- **Visualizer**: Different music visualizers
|
||||
|
||||
---
|
||||
|
||||
- **Auto confirm when paused** (Always Enabled): disable
|
||||
the ["Continue Watching?"](https://user-images.githubusercontent.com/61631665/129977894-01c60740-7ec6-4bf0-9a2c-25da24491b0e.png)
|
||||
popup that pause music after a certain time
|
||||
|
||||
> If `Hide Menu` option is on - you can show the menu with the <kbd>alt</kbd> key (or <kbd>\`</kbd> [backtick] if using
|
||||
> the in-app-menu plugin)
|
||||
|
||||
## Themes
|
||||
|
||||
You can load CSS files to change the look of the application (Options > Visual Tweaks > Themes).
|
||||
@ -178,8 +237,8 @@ Some predefined themes are available in https://github.com/kerichdev/themes-for-
|
||||
```bash
|
||||
git clone https://github.com/th-ch/youtube-music
|
||||
cd youtube-music
|
||||
npm ci
|
||||
npm run start
|
||||
pnpm install --frozen-lockfile
|
||||
pnpm dev
|
||||
```
|
||||
|
||||
## Build your own plugins
|
||||
@ -191,49 +250,72 @@ Using plugins, you can:
|
||||
|
||||
### Creating a plugin
|
||||
|
||||
Create a folder in `plugins/YOUR-PLUGIN-NAME`:
|
||||
|
||||
- if you need to manipulate the BrowserWindow, create a file with the following template:
|
||||
Create a folder in `src/plugins/YOUR-PLUGIN-NAME`:
|
||||
|
||||
- `index.ts`: the main file of the plugin
|
||||
```typescript
|
||||
// file: back.ts
|
||||
export default (win: Electron.BrowserWindow, config: ConfigType<'YOUR-PLUGIN-NAME'>) => {
|
||||
// something
|
||||
};
|
||||
```
|
||||
import style from './style.css?inline'; // import style as inline
|
||||
|
||||
then, register the plugin in `index.ts`:
|
||||
import { createPlugin } from '@/utils';
|
||||
|
||||
```typescript
|
||||
import yourPlugin from './plugins/YOUR-PLUGIN-NAME/back';
|
||||
export default createPlugin({
|
||||
name: 'Plugin Label',
|
||||
restartNeeded: true, // if value is true, ytmusic show restart dialog
|
||||
config: {
|
||||
enabled: false,
|
||||
}, // your custom config
|
||||
stylesheets: [style], // your custom style,
|
||||
menu: async ({ getConfig, setConfig }) => {
|
||||
// All *Config methods are wrapped Promise<T>
|
||||
const config = await getConfig();
|
||||
return [
|
||||
{
|
||||
label: 'menu',
|
||||
submenu: [1, 2, 3].map((value) => ({
|
||||
label: `value ${value}`,
|
||||
type: 'radio',
|
||||
checked: config.value === value,
|
||||
click() {
|
||||
setConfig({ value });
|
||||
},
|
||||
})),
|
||||
},
|
||||
];
|
||||
},
|
||||
backend: {
|
||||
start({ window, ipc }) {
|
||||
window.maximize();
|
||||
|
||||
// ...
|
||||
|
||||
const mainPlugins = {
|
||||
// ...
|
||||
'YOUR-PLUGIN-NAME': yourPlugin,
|
||||
};
|
||||
```
|
||||
|
||||
- if you need to change the front, create a file with the following template:
|
||||
|
||||
```typescript
|
||||
// file: front.ts
|
||||
export default (config: ConfigType<'YOUR-PLUGIN-NAME'>) => {
|
||||
// This function will be called as a preload script
|
||||
// So you can use front features like `document.querySelector`
|
||||
};
|
||||
```
|
||||
|
||||
then, register the plugin in `preload.ts`:
|
||||
|
||||
```typescript
|
||||
import yourPlugin from './plugins/YOUR-PLUGIN-NAME/front';
|
||||
|
||||
const rendererPlugins: PluginMapper<'renderer'> = {
|
||||
// ...
|
||||
'YOUR-PLUGIN-NAME': yourPlugin,
|
||||
};
|
||||
// you can communicate with renderer plugin
|
||||
ipc.handle('some-event', () => {
|
||||
return 'hello';
|
||||
});
|
||||
},
|
||||
// it fired when config changed
|
||||
onConfigChange(newConfig) { /* ... */ },
|
||||
// it fired when plugin disabled
|
||||
stop(context) { /* ... */ },
|
||||
},
|
||||
renderer: {
|
||||
async start(context) {
|
||||
console.log(await context.ipc.invoke('some-event'));
|
||||
},
|
||||
// Only renderer available hook
|
||||
onPlayerApiReady(api: YoutubePlayer, context: RendererContext) {
|
||||
// set plugin config easily
|
||||
context.setConfig({ myConfig: api.getVolume() });
|
||||
},
|
||||
onConfigChange(newConfig) { /* ... */ },
|
||||
stop(_context) { /* ... */ },
|
||||
},
|
||||
preload: {
|
||||
async start({ getConfig }) {
|
||||
const config = await getConfig();
|
||||
},
|
||||
onConfigChange(newConfig) {},
|
||||
stop(_context) {},
|
||||
},
|
||||
});
|
||||
```
|
||||
|
||||
### Common use cases
|
||||
@ -241,45 +323,70 @@ const rendererPlugins: PluginMapper<'renderer'> = {
|
||||
- injecting custom CSS: create a `style.css` file in the same folder then:
|
||||
|
||||
```typescript
|
||||
import path from 'node:path';
|
||||
import { injectCSS } from '../utils';
|
||||
// index.ts
|
||||
import style from './style.css?inline'; // import style as inline
|
||||
|
||||
// back.ts
|
||||
export default (win: Electron.BrowserWindow) => {
|
||||
injectCSS(win.webContents, path.join(__dirname, 'style.css'));
|
||||
};
|
||||
import { createPlugin } from '@/utils';
|
||||
|
||||
export default createPlugin({
|
||||
name: 'Plugin Label',
|
||||
restartNeeded: true, // if value is true, ytmusic will show a restart dialog
|
||||
config: {
|
||||
enabled: false,
|
||||
}, // your custom config
|
||||
stylesheets: [style], // your custom style
|
||||
renderer() {} // define renderer hook
|
||||
});
|
||||
```
|
||||
|
||||
- changing the HTML:
|
||||
- If you want to change the HTML:
|
||||
|
||||
```typescript
|
||||
// front.ts
|
||||
export default () => {
|
||||
// Remove the login button
|
||||
document.querySelector(".sign-in-link.ytmusic-nav-bar").remove();
|
||||
};
|
||||
import { createPlugin } from '@/utils';
|
||||
|
||||
export default createPlugin({
|
||||
name: 'Plugin Label',
|
||||
restartNeeded: true, // if value is true, ytmusic will show the restart dialog
|
||||
config: {
|
||||
enabled: false,
|
||||
}, // your custom config
|
||||
renderer() {
|
||||
// Remove the login button
|
||||
document.querySelector(".sign-in-link.ytmusic-nav-bar").remove();
|
||||
} // define renderer hook
|
||||
});
|
||||
```
|
||||
|
||||
- communicating between the front and back: can be done using the ipcMain module from electron. See `utils.js` file and
|
||||
example in `navigation` plugin.
|
||||
- communicating between the front and back: can be done using the ipcMain module from electron. See `index.ts` file and
|
||||
example in `sponsorblock` plugin.
|
||||
|
||||
## Build
|
||||
|
||||
1. Clone the repo
|
||||
2. Run `npm i` to install dependencies
|
||||
3. Run `npm run build:OS`
|
||||
2. Follow [this guide](https://pnpm.io/installation) to install `pnpm`
|
||||
3. Run `pnpm install --frozen-lockfile` to install dependencies
|
||||
4. Run `pnpm build:OS`
|
||||
|
||||
- `npm run dist:win` - Windows
|
||||
- `npm run dist:linux` - Linux
|
||||
- `npm run dist:mac` - MacOS
|
||||
- `pnpm dist:win` - Windows
|
||||
- `pnpm dist:linux` - Linux (amd64)
|
||||
- `pnpm dist:linux:deb-arm64` - Linux (arm64 for Debian)
|
||||
- `pnpm dist:linux:rpm-arm64` - Linux (arm64 for Fedora)
|
||||
- `pnpm dist:mac` - macOS (amd64)
|
||||
- `pnpm dist:mac:arm64` - macOS (arm64)
|
||||
|
||||
Builds the app for macOS, Linux, and Windows,
|
||||
using [electron-builder](https://github.com/electron-userland/electron-builder).
|
||||
|
||||
## Production Preview
|
||||
|
||||
```bash
|
||||
pnpm start
|
||||
```
|
||||
|
||||
## Tests
|
||||
|
||||
```bash
|
||||
npm run test
|
||||
pnpm test
|
||||
```
|
||||
|
||||
Uses [Playwright](https://playwright.dev/) to test the app.
|
||||
@ -287,3 +394,10 @@ Uses [Playwright](https://playwright.dev/) to test the app.
|
||||
## License
|
||||
|
||||
MIT © [th-ch](https://github.com/th-ch/youtube-music)
|
||||
|
||||
## FAQ
|
||||
|
||||
### Why apps menu isn't showing up?
|
||||
|
||||
If `Hide Menu` option is on - you can show the menu with the <kbd>alt</kbd> key (or <kbd>\`</kbd> [backtick] if using
|
||||
the in-app-menu plugin)
|
||||
50
assets/error.html
Normal file
50
assets/error.html
Normal file
@ -0,0 +1,50 @@
|
||||
<!doctype html>
|
||||
<html>
|
||||
<head>
|
||||
<meta charset="utf-8" />
|
||||
<title>Cannot load YouTube Music</title>
|
||||
<style>
|
||||
body {
|
||||
background: #000;
|
||||
}
|
||||
|
||||
.container {
|
||||
margin: 0;
|
||||
font-family: Roboto, Arial, sans-serif;
|
||||
font-size: 20px;
|
||||
font-weight: 500;
|
||||
color: rgba(255, 255, 255, 0.5);
|
||||
position: absolute;
|
||||
top: 50%;
|
||||
left: 50%;
|
||||
margin-right: -50%;
|
||||
transform: translate(-50%, -50%);
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.button {
|
||||
background: #065fd4;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
color: white;
|
||||
font: inherit;
|
||||
text-transform: uppercase;
|
||||
text-decoration: none;
|
||||
border-radius: 2px;
|
||||
font-size: 16px;
|
||||
font-weight: normal;
|
||||
text-align: center;
|
||||
padding: 8px 22px;
|
||||
display: inline-block;
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
|
||||
<body>
|
||||
<div class="container">
|
||||
<p>Cannot load YouTube Music… Internet disconnected?</p>
|
||||
<a class="button" href="#" onclick="reload()">Retry</a>
|
||||
</div>
|
||||
</body>
|
||||
</html>
|
||||
Binary file not shown.
BIN
assets/youtube-music-tray-paused.png
Normal file
BIN
assets/youtube-music-tray-paused.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 2.1 KiB |
706
changelog.md
706
changelog.md
@ -2,8 +2,714 @@
|
||||
|
||||
All notable changes to this project will be documented in this file. Dates are displayed in UTC.
|
||||
|
||||
#### [v3.5.2](https://github.com/th-ch/youtube-music/compare/v3.5.1...v3.5.2)
|
||||
|
||||
- chore(deps): update typescript-eslint monorepo to v8.4.0 [`#2401`](https://github.com/th-ch/youtube-music/pull/2401)
|
||||
- chore(deps): update dependency @total-typescript/ts-reset to v0.6.1 [`#2396`](https://github.com/th-ch/youtube-music/pull/2396)
|
||||
- chore(deps): update dependency electron to v31.5.0 [`#2397`](https://github.com/th-ch/youtube-music/pull/2397)
|
||||
- chore(deps): update dependency eslint-import-resolver-typescript to v3.6.3 [`#2376`](https://github.com/th-ch/youtube-music/pull/2376)
|
||||
- chore(deps): update dependency discord-api-types to v0.37.100 [`#2394`](https://github.com/th-ch/youtube-music/pull/2394)
|
||||
- fix(deps): update dependency electron-updater to v6.3.4 [`#2395`](https://github.com/th-ch/youtube-music/pull/2395)
|
||||
- chore(deps): update dependency @babel/runtime to v7.25.6 [`#2388`](https://github.com/th-ch/youtube-music/pull/2388)
|
||||
- chore(deps): update dependency vite-plugin-inspect to v0.8.7 [`#2389`](https://github.com/th-ch/youtube-music/pull/2389)
|
||||
- chore(deps): update dependency discord-api-types to v0.37.99 [`#2374`](https://github.com/th-ch/youtube-music/pull/2374)
|
||||
- chore(deps): update dependency vite to v5.4.3 [`#2377`](https://github.com/th-ch/youtube-music/pull/2377)
|
||||
- fix: incorrect regex when splitting artistName [`#2378`](https://github.com/th-ch/youtube-music/pull/2378)
|
||||
- chore(deps): update dependency @babel/runtime to v7.25.4 [`#2373`](https://github.com/th-ch/youtube-music/pull/2373)
|
||||
- synced-lyrics: make the lyrics search more reliable [`#2343`](https://github.com/th-ch/youtube-music/pull/2343)
|
||||
- fix(deps): update dependency solid-js to v1.8.22 [`#2354`](https://github.com/th-ch/youtube-music/pull/2354)
|
||||
- chore(deps): update typescript-eslint monorepo to v8.3.0 [`#2350`](https://github.com/th-ch/youtube-music/pull/2350)
|
||||
- fix(deps): update dependency electron-debug to v4.0.1 [`#2349`](https://github.com/th-ch/youtube-music/pull/2349)
|
||||
- chore(deps): update dependency electron to v31.4.0 [`#2356`](https://github.com/th-ch/youtube-music/pull/2356)
|
||||
- fix: hide native-controls on linux when in-app-menu is used [`#2366`](https://github.com/th-ch/youtube-music/pull/2366)
|
||||
- fix: detect the upgrade btn using the icon [`#2364`](https://github.com/th-ch/youtube-music/pull/2364)
|
||||
- fix: exclude build-id files from rpm [`#2361`](https://github.com/th-ch/youtube-music/pull/2361)
|
||||
- fix(deps): update dependency i18next to v23.12.3 [`#2352`](https://github.com/th-ch/youtube-music/pull/2352)
|
||||
- fix(deps): update dependency @floating-ui/dom to v1.6.10 [`#2340`](https://github.com/th-ch/youtube-music/pull/2340)
|
||||
- fix(deps): update dependency electron-updater to v6.3.3 [`#2347`](https://github.com/th-ch/youtube-music/pull/2347)
|
||||
- fix(deps): update dependency solid-js to v1.8.20 [`#2345`](https://github.com/th-ch/youtube-music/pull/2345)
|
||||
- chore(deps): update dependency vite to v5.4.0 [`#2342`](https://github.com/th-ch/youtube-music/pull/2342)
|
||||
- chore(deps): update typescript-eslint monorepo to v8.0.1 [`#2335`](https://github.com/th-ch/youtube-music/pull/2335)
|
||||
- fix(deps): update dependency @floating-ui/dom to v1.6.9 [`#2337`](https://github.com/th-ch/youtube-music/pull/2337)
|
||||
- chore(deps): update playwright monorepo to v1.46.0 [`#2336`](https://github.com/th-ch/youtube-music/pull/2336)
|
||||
- chore(README): Translation README to Russian and adding Synced Lyrics to main README [`#2338`](https://github.com/th-ch/youtube-music/pull/2338)
|
||||
- chore(deps): update dependency rollup to v4.20.0 [`#2326`](https://github.com/th-ch/youtube-music/pull/2326)
|
||||
- fix(synced-lyric): fix timestamp [`#2323`](https://github.com/th-ch/youtube-music/issues/2323) [`#2379`](https://github.com/th-ch/youtube-music/issues/2379)
|
||||
- Revert "fix(MPRIS): Prevents player to start with invalid MPRIS interface (#1996)" [`#2225`](https://github.com/th-ch/youtube-music/issues/2225)
|
||||
- fix(adblocker/inplayer): fix Response.prototype.json [`#2310`](https://github.com/th-ch/youtube-music/issues/2310)
|
||||
- chore(deps): update dependency eslint-plugin-import to v2.30.0 [`f48e46d`](https://github.com/th-ch/youtube-music/commit/f48e46d29cf09c76c5172fd56d2d0f705616e4e3)
|
||||
- Revert "chore(deps): update dependency electron-builder to v25" [`089eff3`](https://github.com/th-ch/youtube-music/commit/089eff3152903c8b55ad3e5571b944062a647e27)
|
||||
- chore(deps): update dependency electron-builder to v25 [`fe4c89c`](https://github.com/th-ch/youtube-music/commit/fe4c89c349bb9f4f54d95c2018943095ccfdab0c)
|
||||
|
||||
#### [v3.5.1](https://github.com/th-ch/youtube-music/compare/v3.5.0...v3.5.1)
|
||||
|
||||
> 1 August 2024
|
||||
|
||||
- fix(deps): update dependency youtubei.js to v10.3.0 [`#2306`](https://github.com/th-ch/youtube-music/pull/2306)
|
||||
- fix: Window gets stuck offscreen in some instances [`#2303`](https://github.com/th-ch/youtube-music/pull/2303)
|
||||
- fix: Incorrect window size on multi-monitor scaled displays [`#2302`](https://github.com/th-ch/youtube-music/pull/2302)
|
||||
- chore(deps): update dependency rollup to v4.19.2 [`#2304`](https://github.com/th-ch/youtube-music/pull/2304)
|
||||
- chore(deps): update typescript-eslint monorepo to v8 (major) [`#2297`](https://github.com/th-ch/youtube-music/pull/2297)
|
||||
- fix(ambient-mode): fix ambient-mode not working for videos after restart [`#2294`](https://github.com/th-ch/youtube-music/pull/2294)
|
||||
- fix(deps): update dependency @xhayper/discord-rpc to v1.2.0 [`#2291`](https://github.com/th-ch/youtube-music/pull/2291)
|
||||
- fix(synced-lyrics): fix lyric load [`#2295`](https://github.com/th-ch/youtube-music/issues/2295)
|
||||
- fix(ambient-mode): fix ambient-mode not working for videos after restart (#2294) [`#1641`](https://github.com/th-ch/youtube-music/issues/1641)
|
||||
- fix(synced-lyrics): fix i18n [`8750b54`](https://github.com/th-ch/youtube-music/commit/8750b54f766c735ff039c6be454427f17d4737e2)
|
||||
- ts-fix: disambiguate ElectronStore typings [`8775735`](https://github.com/th-ch/youtube-music/commit/877573532c1b68af861a3fdc44d093f3097d36ab)
|
||||
- chore(i18n): Translated using Weblate (Hungarian) [`3537dc1`](https://github.com/th-ch/youtube-music/commit/3537dc19eecce7f7deb2478942f70d3c7b72148d)
|
||||
|
||||
#### [v3.5.0](https://github.com/th-ch/youtube-music/compare/v3.4.1...v3.5.0)
|
||||
|
||||
> 31 July 2024
|
||||
|
||||
- plugin: Synced Lyrics [`#2207`](https://github.com/th-ch/youtube-music/pull/2207)
|
||||
- chore(deps): update dependency electron to v31.3.1 [`#2290`](https://github.com/th-ch/youtube-music/pull/2290)
|
||||
- chore(deps): update typescript-eslint monorepo to v7.18.0 [`#2292`](https://github.com/th-ch/youtube-music/pull/2292)
|
||||
- fix(deps): update dependency youtubei.js to v10.2.0 [`#2285`](https://github.com/th-ch/youtube-music/pull/2285)
|
||||
- chore(deps): update dependency electron to v31.3.0 [`#2282`](https://github.com/th-ch/youtube-music/pull/2282)
|
||||
- chore(deps): update typescript-eslint monorepo to v7.17.0 [`#2283`](https://github.com/th-ch/youtube-music/pull/2283)
|
||||
- fix(deps): update dependency solid-js to v1.8.19 [`#2280`](https://github.com/th-ch/youtube-music/pull/2280)
|
||||
- fix(deps): update dependency @xhayper/discord-rpc to v1.1.4 [`#2279`](https://github.com/th-ch/youtube-music/pull/2279)
|
||||
- chore(deps): update dependency @babel/runtime to v7.25.0 [`#2281`](https://github.com/th-ch/youtube-music/pull/2281)
|
||||
- fix(deps): update dependency @floating-ui/dom to v1.6.8 [`#2278`](https://github.com/th-ch/youtube-music/pull/2278)
|
||||
- Fix: Incorrect window size on scaled displays [`#2258`](https://github.com/th-ch/youtube-music/pull/2258)
|
||||
- chore(deps): update dependency vite-plugin-resolve to v2.5.2 [`#2276`](https://github.com/th-ch/youtube-music/pull/2276)
|
||||
- chore(deps): update playwright monorepo to v1.45.3 [`#2277`](https://github.com/th-ch/youtube-music/pull/2277)
|
||||
- fix(deps): update dependency deepmerge-ts to v7.1.0 [`#2263`](https://github.com/th-ch/youtube-music/pull/2263)
|
||||
- chore(deps): update dependency typescript to v5.5.4 [`#2274`](https://github.com/th-ch/youtube-music/pull/2274)
|
||||
- chore(deps): update dependency vite to v5.3.5 [`#2275`](https://github.com/th-ch/youtube-music/pull/2275)
|
||||
- fix(deps): update dependency i18next to v23.12.2 [`#2260`](https://github.com/th-ch/youtube-music/pull/2260)
|
||||
- chore(deps): update dependency discord-api-types to v0.37.93 [`#2273`](https://github.com/th-ch/youtube-music/pull/2273)
|
||||
- chore(deps): update dependency rollup to v4.19.1 [`#2261`](https://github.com/th-ch/youtube-music/pull/2261)
|
||||
- fix(deps): update dependency custom-electron-prompt to v1.5.8 [`#2262`](https://github.com/th-ch/youtube-music/pull/2262)
|
||||
- feat(adblocker): add new option AdSpeedup [`#2235`](https://github.com/th-ch/youtube-music/pull/2235)
|
||||
- fix: disable multi-plane format for software video [`#2254`](https://github.com/th-ch/youtube-music/pull/2254)
|
||||
- chore(deps): update dependency eslint-plugin-prettier to v5.2.1 [`#2253`](https://github.com/th-ch/youtube-music/pull/2253)
|
||||
- chore(deps): update dependency vite to v5.3.4 [`#2243`](https://github.com/th-ch/youtube-music/pull/2243)
|
||||
- chore(deps): update typescript-eslint monorepo to v7.16.1 [`#2239`](https://github.com/th-ch/youtube-music/pull/2239)
|
||||
- chore(deps): update playwright monorepo to v1.45.2 [`#2244`](https://github.com/th-ch/youtube-music/pull/2244)
|
||||
- chore(deps): update dependency vite-plugin-inspect to v0.8.5 [`#2252`](https://github.com/th-ch/youtube-music/pull/2252)
|
||||
- fix(deps): update dependency semver to v7.6.3 [`#2250`](https://github.com/th-ch/youtube-music/pull/2250)
|
||||
- chore(deps): update dependency electron to v31.2.1 [`#2241`](https://github.com/th-ch/youtube-music/pull/2241)
|
||||
- chore(i18n): Translated using Weblate (Catalan) [`4a8440c`](https://github.com/th-ch/youtube-music/commit/4a8440c281c341977ab3687982cec8cbc5af6cf7)
|
||||
- Update changelog for v3.4.1 [`18e0b1b`](https://github.com/th-ch/youtube-music/commit/18e0b1b86341b13f1cbc713bfbd7b5d7a45ee392)
|
||||
- fix(synced-lyrics): fix type error [`9357a15`](https://github.com/th-ch/youtube-music/commit/9357a15116a8526d22ba6142c0a02f31688743f2)
|
||||
|
||||
#### [v3.4.1](https://github.com/th-ch/youtube-music/compare/v3.4.0...v3.4.1)
|
||||
|
||||
> 15 July 2024
|
||||
|
||||
- fix(mpris): fix mpris position [`#2225`](https://github.com/th-ch/youtube-music/issues/2225)
|
||||
- fix(deb): fix depends [`#1983`](https://github.com/th-ch/youtube-music/issues/1983)
|
||||
- fix: fix touchbar icon [`#2183`](https://github.com/th-ch/youtube-music/issues/2183)
|
||||
- fix: fix "Starting page" [`#1822`](https://github.com/th-ch/youtube-music/issues/1822)
|
||||
- fix: fix album actions [`#2202`](https://github.com/th-ch/youtube-music/issues/2202)
|
||||
- fix: fix playback slider [`#2045`](https://github.com/th-ch/youtube-music/issues/2045)
|
||||
- chore(i18n): Translated using Weblate (Spanish) [`91bee48`](https://github.com/th-ch/youtube-music/commit/91bee4880ed2c6fdd887814a2620877d89bea311)
|
||||
- Bump version to 3.4.1 [`02e2fb6`](https://github.com/th-ch/youtube-music/commit/02e2fb6a83844f439f760e72cdcb935b86000df2)
|
||||
|
||||
#### [v3.4.0](https://github.com/th-ch/youtube-music/compare/v3.3.12...v3.4.0)
|
||||
|
||||
> 14 July 2024
|
||||
|
||||
- fix(deps): update dependency i18next to v23.12.1 [`#2230`](https://github.com/th-ch/youtube-music/pull/2230)
|
||||
- feat(downloader): New option to download on finish [`#1964`](https://github.com/th-ch/youtube-music/pull/1964)
|
||||
- chore(deps): update typescript-eslint monorepo to v8.0.0-alpha.42 [`#2228`](https://github.com/th-ch/youtube-music/pull/2228)
|
||||
- chore(deps): update dependency eslint to v9.7.0 [`#2226`](https://github.com/th-ch/youtube-music/pull/2226)
|
||||
- chore(deps): update dependency @babel/runtime to v7.24.8 [`#2221`](https://github.com/th-ch/youtube-music/pull/2221)
|
||||
- chore(deps): update dependency node-gyp to v10.2.0 [`#2216`](https://github.com/th-ch/youtube-music/pull/2216)
|
||||
- chore(deps): update dependency ws to v8.18.0 [`#2217`](https://github.com/th-ch/youtube-music/pull/2217)
|
||||
- chore(deps): update dependency glob to v11 [`#2219`](https://github.com/th-ch/youtube-music/pull/2219)
|
||||
- chore(deps): update dependency esbuild to v0.23.0 [`#2215`](https://github.com/th-ch/youtube-music/pull/2215)
|
||||
- chore(deps): update dependency electron to v31.2.0 [`#2214`](https://github.com/th-ch/youtube-music/pull/2214)
|
||||
- fix(deps): update dependency youtubei.js to v10.1.0 [`#2218`](https://github.com/th-ch/youtube-music/pull/2218)
|
||||
- chore(deps): update playwright monorepo to v1.45.1 [`#2212`](https://github.com/th-ch/youtube-music/pull/2212)
|
||||
- chore(deps): update typescript-eslint monorepo to v8.0.0-alpha.41 [`#2213`](https://github.com/th-ch/youtube-music/pull/2213)
|
||||
- chore(deps): update dependency rollup to v4.18.1 [`#2210`](https://github.com/th-ch/youtube-music/pull/2210)
|
||||
- chore(deps): update dependency eslint to v9.6.0 [`#2192`](https://github.com/th-ch/youtube-music/pull/2192)
|
||||
- chore(deps): update dependency vite to v5.3.3 [`#2211`](https://github.com/th-ch/youtube-music/pull/2211)
|
||||
- chore(deps): update dependency glob to v10.4.5 [`#2205`](https://github.com/th-ch/youtube-music/pull/2205)
|
||||
- chore(deps): update dependency discord-api-types to v0.37.92 [`#2204`](https://github.com/th-ch/youtube-music/pull/2204)
|
||||
- fix(deps): update dependency solid-js to v1.8.18 [`#2189`](https://github.com/th-ch/youtube-music/pull/2189)
|
||||
- chore(deps): update dependency typescript to v5.5.3 [`#2206`](https://github.com/th-ch/youtube-music/pull/2206)
|
||||
- chore(deps): update dependency electron to v31.1.0 [`#2190`](https://github.com/th-ch/youtube-music/pull/2190)
|
||||
- chore(deps): update typescript-eslint monorepo to v8.0.0-alpha.40 [`#2193`](https://github.com/th-ch/youtube-music/pull/2193)
|
||||
- fix(deps): update dependency @floating-ui/dom to v1.6.7 [`#2196`](https://github.com/th-ch/youtube-music/pull/2196)
|
||||
- chore(deps): update dependency vite to v5.3.2 [`#2188`](https://github.com/th-ch/youtube-music/pull/2188)
|
||||
- chore(deps): update dependency discord-api-types to v0.37.91 [`#2187`](https://github.com/th-ch/youtube-music/pull/2187)
|
||||
- chore(deps): update typescript-eslint monorepo to v8.0.0-alpha.34 [`#2184`](https://github.com/th-ch/youtube-music/pull/2184)
|
||||
- fix(deps): update dependency @floating-ui/dom to v1.6.6 [`#2182`](https://github.com/th-ch/youtube-music/pull/2182)
|
||||
- chore(deps): update playwright monorepo to v1.45.0 [`#2181`](https://github.com/th-ch/youtube-music/pull/2181)
|
||||
- fix(deps): update dependency ts-morph to v23 [`#2180`](https://github.com/th-ch/youtube-music/pull/2180)
|
||||
- chore(deps): update dependency electron-vite to v2.3.0 [`#2178`](https://github.com/th-ch/youtube-music/pull/2178)
|
||||
- fix(deps): update dependency conf to v13.0.1 [`#2175`](https://github.com/th-ch/youtube-music/pull/2175)
|
||||
- chore(deps): update dependency glob to v10.4.2 [`#2168`](https://github.com/th-ch/youtube-music/pull/2168)
|
||||
- chore(deps): update dependency discord-api-types to v0.37.90 [`#2167`](https://github.com/th-ch/youtube-music/pull/2167)
|
||||
- chore(deps): update dependency typescript to v5.5.2 [`#2173`](https://github.com/th-ch/youtube-music/pull/2173)
|
||||
- chore(deps): update dependency electron to v31.0.2 [`#2170`](https://github.com/th-ch/youtube-music/pull/2170)
|
||||
- chore(deps): update dependency ws to v8.17.1 [`#2164`](https://github.com/th-ch/youtube-music/pull/2164)
|
||||
- chore(deps): update dependency eslint to v9.5.0 [`#2162`](https://github.com/th-ch/youtube-music/pull/2162)
|
||||
- fix(deps): update dependency youtubei.js to v10 [`#2136`](https://github.com/th-ch/youtube-music/pull/2136)
|
||||
- chore(deps): update dependency discord-api-types to v0.37.89 [`#2153`](https://github.com/th-ch/youtube-music/pull/2153)
|
||||
- chore(deps): update dependency vite to v5.3.1 [`#2154`](https://github.com/th-ch/youtube-music/pull/2154)
|
||||
- fix(deps): update dependency electron-store to v10 [`#2157`](https://github.com/th-ch/youtube-music/pull/2157)
|
||||
- fix(deps): update dependency conf to v13 [`#2156`](https://github.com/th-ch/youtube-music/pull/2156)
|
||||
- chore(deps): update dependency electron to v31.0.1 [`#2148`](https://github.com/th-ch/youtube-music/pull/2148)
|
||||
- chore(deps): update dependency discord-api-types to v0.37.88 [`#2138`](https://github.com/th-ch/youtube-music/pull/2138)
|
||||
- chore(deps): update typescript-eslint monorepo to v8.0.0-alpha.30 [`#2139`](https://github.com/th-ch/youtube-music/pull/2139)
|
||||
- chore(deps): update dependency electron to v31 [`#2141`](https://github.com/th-ch/youtube-music/pull/2141)
|
||||
- chore(deps): update dependency esbuild to v0.21.5 [`#2135`](https://github.com/th-ch/youtube-music/pull/2135)
|
||||
- chore(deps): update typescript-eslint monorepo to v8.0.0-alpha.29 [`#2132`](https://github.com/th-ch/youtube-music/pull/2132)
|
||||
- fix: rollback eslint version to v8 [`45931a2`](https://github.com/th-ch/youtube-music/commit/45931a25b08ab8a406f9e102486585311fd14bf9)
|
||||
- chore(i18n): Translated using Weblate (Filipino) [`8a20566`](https://github.com/th-ch/youtube-music/commit/8a20566e0f2736f72d46282188ada69df1d7076a)
|
||||
- chore(i18n): Translated using Weblate (Slovenian) [`40f0b9b`](https://github.com/th-ch/youtube-music/commit/40f0b9b852dcd9146e1c1e6c741b5baaf55ac079)
|
||||
|
||||
#### [v3.3.12](https://github.com/th-ch/youtube-music/compare/v3.3.11...v3.3.12)
|
||||
|
||||
> 8 June 2024
|
||||
|
||||
- hotfix: Revert "chore(deps): update dependencies `@cliqz/adblocker-electron`, `@cliqz/adblocker-electron-preload`" [`3c4abc1`](https://github.com/th-ch/youtube-music/commit/3c4abc14187e51f7e47c1ae71b3513f6d8c9912a)
|
||||
- Update changelog for v3.3.11 [`de22444`](https://github.com/th-ch/youtube-music/commit/de224444c2a6d9030aa22a3b263ceacbc4b41914)
|
||||
- Bump version to 3.3.12 [`89ed7d2`](https://github.com/th-ch/youtube-music/commit/89ed7d2345001fea59514944f4c1d56d2b7bd888)
|
||||
|
||||
#### [v3.3.11](https://github.com/th-ch/youtube-music/compare/v3.3.10...v3.3.11)
|
||||
|
||||
> 8 June 2024
|
||||
|
||||
- Revert "fix(deps): update dependency @cliqz/adblocker-electron to v1.27.10" [`#2129`](https://github.com/th-ch/youtube-music/pull/2129)
|
||||
- chore(deps): update dependency vite to v5.2.13 [`#2127`](https://github.com/th-ch/youtube-music/pull/2127)
|
||||
- chore(deps): update dependency electron to v30.1.0 [`#2126`](https://github.com/th-ch/youtube-music/pull/2126)
|
||||
- fix(deps): update dependency deepmerge-ts to v7.0.3 [`#2125`](https://github.com/th-ch/youtube-music/pull/2125)
|
||||
- chore(deps): update dependency @babel/runtime to v7.24.7 [`#2124`](https://github.com/th-ch/youtube-music/pull/2124)
|
||||
- chore(deps): update typescript-eslint monorepo to v8.0.0-alpha.28 [`#2121`](https://github.com/th-ch/youtube-music/pull/2121)
|
||||
- fix(deps): update dependency electron-updater to v6.2.1 [`#2120`](https://github.com/th-ch/youtube-music/pull/2120)
|
||||
- chore(deps): update dependency discord-api-types to v0.37.87 [`#2119`](https://github.com/th-ch/youtube-music/pull/2119)
|
||||
- fix(deps): update dependency deepmerge-ts to v7.0.2 [`#2118`](https://github.com/th-ch/youtube-music/pull/2118)
|
||||
- chore(deps): update typescript-eslint monorepo to v8.0.0-alpha.25 [`#2114`](https://github.com/th-ch/youtube-music/pull/2114)
|
||||
- fix(menu): fix menubar items doesn't rendered [`#2113`](https://github.com/th-ch/youtube-music/issues/2113)
|
||||
- chore(i18n): Translated using Weblate (Nepali) [`4ae9a28`](https://github.com/th-ch/youtube-music/commit/4ae9a2820e9d453635094956264dd8b42c4997f7)
|
||||
- chore(i18n): Translated using Weblate (Nepali) [`7e8d311`](https://github.com/th-ch/youtube-music/commit/7e8d31172ceb175ba07f307d248fc1246265a4c0)
|
||||
- fix(deps): update dependency @cliqz/adblocker-electron to v1.27.10 [`d97aa1a`](https://github.com/th-ch/youtube-music/commit/d97aa1a8a003f15eea63c8cb2dabd0f215e885f1)
|
||||
|
||||
#### [v3.3.10](https://github.com/th-ch/youtube-music/compare/v3.3.9...v3.3.10)
|
||||
|
||||
> 2 June 2024
|
||||
|
||||
- fix(adblocker): fix blank screen [`#2103`](https://github.com/th-ch/youtube-music/issues/2103) [`#2105`](https://github.com/th-ch/youtube-music/issues/2105)
|
||||
- chore(i18n): Translated using Weblate (Hungarian) [`25958a7`](https://github.com/th-ch/youtube-music/commit/25958a7bb1fea20e59676e7821f3dd8819602b68)
|
||||
- fix(deps): bump deps [`4fa9762`](https://github.com/th-ch/youtube-music/commit/4fa9762a506544ce453894ce2df13033225e6c7d)
|
||||
- fix(deps): bump `@typescript-eslint/eslint-plugin` version to 8.0.0-alpha.24 [`1e5bea8`](https://github.com/th-ch/youtube-music/commit/1e5bea85b31da5de868d9eff8758e5d2d888c2c8)
|
||||
|
||||
#### [v3.3.9](https://github.com/th-ch/youtube-music/compare/v3.3.8...v3.3.9)
|
||||
|
||||
> 1 June 2024
|
||||
|
||||
- chore(deps): update dependency eslint to v9.4.0 [`#2106`](https://github.com/th-ch/youtube-music/pull/2106)
|
||||
- fix(adblocker): fix In-Player adblocker [`#1817`](https://github.com/th-ch/youtube-music/issues/1817)
|
||||
- feat(adblocker): improve In-Player adblocker [`5b9e947`](https://github.com/th-ch/youtube-music/commit/5b9e947b8feebb57d9a2122ae7b7ab2ff7c37c06)
|
||||
- chore(i18n): Translated using Weblate (French) [`9e809b0`](https://github.com/th-ch/youtube-music/commit/9e809b002d10f6ec0202a7d56d3d0b73f8093012)
|
||||
- chore(i18n): Translated using Weblate (Malay) [`79151cb`](https://github.com/th-ch/youtube-music/commit/79151cb3aa6c087b8d8bb500322f505797b822bd)
|
||||
|
||||
#### [v3.3.8](https://github.com/th-ch/youtube-music/compare/v3.3.7...v3.3.8)
|
||||
|
||||
> 1 June 2024
|
||||
|
||||
- fix(adblocker): fix blank screen [`#1942`](https://github.com/th-ch/youtube-music/issues/1942) [`#2100`](https://github.com/th-ch/youtube-music/issues/2100) [`#2103`](https://github.com/th-ch/youtube-music/issues/2103)
|
||||
- Update changelog for v3.3.7 [`b572623`](https://github.com/th-ch/youtube-music/commit/b572623442fc8b45b593dc0c91623fbf814115b4)
|
||||
- Bump version to 3.3.8 [`5d99a85`](https://github.com/th-ch/youtube-music/commit/5d99a854e2f29bdb6682beeffa4e6b9b8be0f60f)
|
||||
|
||||
#### [v3.3.7](https://github.com/th-ch/youtube-music/compare/v3.3.6...v3.3.7)
|
||||
|
||||
> 1 June 2024
|
||||
|
||||
- chore(deps): update dependency electron to v30.0.9 [`#2098`](https://github.com/th-ch/youtube-music/pull/2098)
|
||||
- Revert "fix(deps): update dependency @cliqz/adblocker-electron to v1.27.6" [`#2101`](https://github.com/th-ch/youtube-music/pull/2101)
|
||||
- fix(deps): update dependency @cliqz/adblocker-electron to v1.27.6 [`#2096`](https://github.com/th-ch/youtube-music/pull/2096)
|
||||
- chore(deps): update dependency discord-api-types to v0.37.86 [`#2092`](https://github.com/th-ch/youtube-music/pull/2092)
|
||||
- chore(deps): update dependency vite to v5.2.12 [`#2094`](https://github.com/th-ch/youtube-music/pull/2094)
|
||||
- chore(deps): update dependency @typescript-eslint/eslint-plugin to v7.11.0 [`#2093`](https://github.com/th-ch/youtube-music/pull/2093)
|
||||
- chore(docs): Added README-es.md and linked to README.md [`#2090`](https://github.com/th-ch/youtube-music/pull/2090)
|
||||
- fix(deps): update dependency deepmerge-ts to v7 [`#2085`](https://github.com/th-ch/youtube-music/pull/2085)
|
||||
- chore(deps): update dependency builtin-modules to v4 [`#2084`](https://github.com/th-ch/youtube-music/pull/2084)
|
||||
- fix(deps): update dependency electron-debug to v4 [`#2086`](https://github.com/th-ch/youtube-music/pull/2086)
|
||||
- fix(deps): update dependency electron-store to v9 [`#2087`](https://github.com/th-ch/youtube-music/pull/2087)
|
||||
- fix(deps): update dependency conf to v12 [`#1463`](https://github.com/th-ch/youtube-music/pull/1463)
|
||||
- fix(deps): update dependency youtubei.js to v9.4.0 [`#2083`](https://github.com/th-ch/youtube-music/pull/2083)
|
||||
- chore(deps): update playwright monorepo to v1.44.1 [`#2082`](https://github.com/th-ch/youtube-music/pull/2082)
|
||||
- chore(deps): update dependency ws to v8.17.0 [`#2081`](https://github.com/th-ch/youtube-music/pull/2081)
|
||||
- chore(deps): update dependency glob to v10.4.1 [`#2080`](https://github.com/th-ch/youtube-music/pull/2080)
|
||||
- chore(deps): update dependency eslint to v9.3.0 [`#2079`](https://github.com/th-ch/youtube-music/pull/2079)
|
||||
- fix(deps): update dependency peerjs to v1.5.4 [`#2075`](https://github.com/th-ch/youtube-music/pull/2075)
|
||||
- chore(deps): update dependency esbuild to v0.21.4 [`#2078`](https://github.com/th-ch/youtube-music/pull/2078)
|
||||
- fix(deps): update dependency semver to v7.6.2 [`#2076`](https://github.com/th-ch/youtube-music/pull/2076)
|
||||
- chore(deps): update dependency electron-vite to v2.2.0 [`#2077`](https://github.com/th-ch/youtube-music/pull/2077)
|
||||
- fix(deps): update dependency i18next to v23.11.5 [`#2074`](https://github.com/th-ch/youtube-music/pull/2074)
|
||||
- fix(deps): update dependency @cliqz/adblocker-electron to v1.27.3 [`#2071`](https://github.com/th-ch/youtube-music/pull/2071)
|
||||
- chore(deps): update dependency vite to v5.2.11 [`#2070`](https://github.com/th-ch/youtube-music/pull/2070)
|
||||
- fix(deps): update dependency @floating-ui/dom to v1.6.5 [`#2073`](https://github.com/th-ch/youtube-music/pull/2073)
|
||||
- fix(deps): update dependency @cliqz/adblocker-electron-preload to v1.27.3 [`#2072`](https://github.com/th-ch/youtube-music/pull/2072)
|
||||
- chore(deps): update pnpm to v9 [`#1980`](https://github.com/th-ch/youtube-music/pull/1980)
|
||||
- chore(deps): update dependency electron to v30.0.8 [`#2068`](https://github.com/th-ch/youtube-music/pull/2068)
|
||||
- chore(deps-dev): bump ejs from 3.1.9 to 3.1.10 [`#2023`](https://github.com/th-ch/youtube-music/pull/2023)
|
||||
- chore(deps): update dependency utf-8-validate to v6.0.4 [`#2069`](https://github.com/th-ch/youtube-music/pull/2069)
|
||||
- fix(MPRIS): Prevents player to start with invalid MPRIS interface [`#1996`](https://github.com/th-ch/youtube-music/pull/1996)
|
||||
- fix(deps): update dependency solid-js to v1.8.17 [`#2002`](https://github.com/th-ch/youtube-music/pull/2002)
|
||||
- chore(deps): update dependency @typescript-eslint/eslint-plugin to v7.10.0 [`#2000`](https://github.com/th-ch/youtube-music/pull/2000)
|
||||
- chore(deps): update dependency discord-api-types to v0.37.85 [`#1998`](https://github.com/th-ch/youtube-music/pull/1998)
|
||||
- fix(deps): update dependency serve to v14.2.3 [`#1997`](https://github.com/th-ch/youtube-music/pull/1997)
|
||||
- chore(deps): update dependency rollup to v4.18.0 [`#1990`](https://github.com/th-ch/youtube-music/pull/1990)
|
||||
- feat: Enable arm64 for deb and rpm [`#2033`](https://github.com/th-ch/youtube-music/pull/2033)
|
||||
- chore (README-is.md): Replace viðbót with tengiforrit [`#2004`](https://github.com/th-ch/youtube-music/pull/2004)
|
||||
- chore(docs): readme file translated to french [`#2049`](https://github.com/th-ch/youtube-music/pull/2049)
|
||||
- chore(deps): update dependency @babel/runtime to v7.24.6 [`#2039`](https://github.com/th-ch/youtube-music/pull/2039)
|
||||
- Fix substract `margin-top` in fullscreen mode [`#2015`](https://github.com/th-ch/youtube-music/pull/2015)
|
||||
- chore(deps): update pnpm to v8.15.7 [`#1970`](https://github.com/th-ch/youtube-music/pull/1970)
|
||||
- fix(renderer): fix macos traffic lights gap [`#2035`](https://github.com/th-ch/youtube-music/issues/2035)
|
||||
- Fix substract `margin-top` in fullscreen mode [`#2013`](https://github.com/th-ch/youtube-music/issues/2013)
|
||||
- chore(i18n): Translated using Weblate (Hungarian) [`f3de171`](https://github.com/th-ch/youtube-music/commit/f3de17112af787437362f31b5c4e2d4149ba1436)
|
||||
- feat(menu): add theme list in menu [`933b4cc`](https://github.com/th-ch/youtube-music/commit/933b4cc8f062b3442afd4516a40eb2938db98fc6)
|
||||
- chore(i18n): Translated using Weblate (Filipino) [`91392c0`](https://github.com/th-ch/youtube-music/commit/91392c0c7efaf3b33da4be4aaa7946af7108d676)
|
||||
|
||||
#### [v3.3.6](https://github.com/th-ch/youtube-music/compare/v3.3.5...v3.3.6)
|
||||
|
||||
> 13 April 2024
|
||||
|
||||
- fix: add AdGuard as blocklist sources [`#1966`](https://github.com/th-ch/youtube-music/pull/1966)
|
||||
- chore(deps): update dependency rollup to v4.14.2 [`#1968`](https://github.com/th-ch/youtube-music/pull/1968)
|
||||
- fix(deps): update dependency youtubei.js to v9.3.0 [`#1967`](https://github.com/th-ch/youtube-music/pull/1967)
|
||||
- chore(deps): update playwright monorepo to v1.43.1 [`#1969`](https://github.com/th-ch/youtube-music/pull/1969)
|
||||
- chore(deps): update dependency electron to v29.3.0 [`#1961`](https://github.com/th-ch/youtube-music/pull/1961)
|
||||
- fix(mpris): use global regex to replace minus in the video ID [`#1963`](https://github.com/th-ch/youtube-music/pull/1963)
|
||||
- fix(deps): update dependency @cliqz/adblocker-electron-preload to v1.27.1 [`#1954`](https://github.com/th-ch/youtube-music/pull/1954)
|
||||
- chore(deps): update dependency typescript to v5.4.5 [`#1958`](https://github.com/th-ch/youtube-music/pull/1958)
|
||||
- fix(deps): update dependency youtubei.js to v9.2.1 [`#1957`](https://github.com/th-ch/youtube-music/pull/1957)
|
||||
- fix(deps): update dependency i18next to v23.11.1 [`#1956`](https://github.com/th-ch/youtube-music/pull/1956)
|
||||
- fix(deps): update dependency @cliqz/adblocker-electron to v1.27.1 [`#1953`](https://github.com/th-ch/youtube-music/pull/1953)
|
||||
- chore(deps): update dependency @typescript-eslint/eslint-plugin to v7.6.0 [`#1947`](https://github.com/th-ch/youtube-music/pull/1947)
|
||||
- fix(deps): update dependency i18next to v23.11.0 [`#1946`](https://github.com/th-ch/youtube-music/pull/1946)
|
||||
- chore(deps): update dependency node-gyp to v10.1.0 [`#1941`](https://github.com/th-ch/youtube-music/pull/1941)
|
||||
- chore(deps): update dependency eslint to v9 [`#1940`](https://github.com/th-ch/youtube-music/pull/1940)
|
||||
- chore(deps): update dependency rollup to v4.14.1 [`#1944`](https://github.com/th-ch/youtube-music/pull/1944)
|
||||
- chore(deps): update dependency node-gyp to v10.1.0 [`#1937`](https://github.com/th-ch/youtube-music/pull/1937)
|
||||
- chore(deps): update dependency typescript to v5.4.4 [`#1936`](https://github.com/th-ch/youtube-music/pull/1936)
|
||||
- chore(deps): update playwright monorepo to v1.43.0 [`#1938`](https://github.com/th-ch/youtube-music/pull/1938)
|
||||
- chore(deps): bump undici from 5.28.3 to 5.28.4 [`#1935`](https://github.com/th-ch/youtube-music/pull/1935)
|
||||
- chore(deps): update dependency vite to v5.2.8 [`#1930`](https://github.com/th-ch/youtube-music/pull/1930)
|
||||
- chore(deps): update dependency discord-api-types to v0.37.79 [`#1933`](https://github.com/th-ch/youtube-music/pull/1933)
|
||||
- chore(deps): update dependency node-gyp to v10.1.0 [`#1910`](https://github.com/th-ch/youtube-music/pull/1910)
|
||||
- chore(deps): update dependency node-gyp to v10.1.0 [`#1908`](https://github.com/th-ch/youtube-music/pull/1908)
|
||||
- fix(deps): update dependency @cliqz/adblocker-electron to v1.27.0 [`#1906`](https://github.com/th-ch/youtube-music/pull/1906)
|
||||
- fix(deps): update dependency @cliqz/adblocker-electron-preload to v1.27.0 [`#1907`](https://github.com/th-ch/youtube-music/pull/1907)
|
||||
- chore(deps): update dependency rollup to v4.13.2 [`#1901`](https://github.com/th-ch/youtube-music/pull/1901)
|
||||
- chore(deps): update dependency glob to v10.3.12 [`#1900`](https://github.com/th-ch/youtube-music/pull/1900)
|
||||
- chore(deps): update dependency vite to v5.2.7 [`#1905`](https://github.com/th-ch/youtube-music/pull/1905)
|
||||
- fix(deps): update dependency node-html-parser to v6.1.13 [`#1903`](https://github.com/th-ch/youtube-music/pull/1903)
|
||||
- chore(deps): update dependency discord-api-types to v0.37.77 [`#1899`](https://github.com/th-ch/youtube-music/pull/1899)
|
||||
- chore(deps): update dependency electron to v29.1.6 [`#1898`](https://github.com/th-ch/youtube-music/pull/1898)
|
||||
- Improve video title filters [`#1667`](https://github.com/th-ch/youtube-music/pull/1667)
|
||||
- chore(deps): update dependency rollup to v4.13.1 [`#1896`](https://github.com/th-ch/youtube-music/pull/1896)
|
||||
- chore(deps): update dependency node-gyp to v10.1.0 [`#1890`](https://github.com/th-ch/youtube-music/pull/1890)
|
||||
- chore(deps): update dependency node-gyp to v10.1.0 [`#1889`](https://github.com/th-ch/youtube-music/pull/1889)
|
||||
- fix: fix `switch-repeat` [`#1810`](https://github.com/th-ch/youtube-music/issues/1810)
|
||||
- i18n Translation to Dutch/nl [`0dbf029`](https://github.com/th-ch/youtube-music/commit/0dbf0295b805f9883522ee00983b338060fbddbe)
|
||||
- fix: rollback electron-builder version to 24.9.4 [`4a57cc5`](https://github.com/th-ch/youtube-music/commit/4a57cc5ee9ab2ad6835cff75b8b3aead75d9e564)
|
||||
- chore: update electron-builder to 25.0.0-alpha.6 [`aef03ab`](https://github.com/th-ch/youtube-music/commit/aef03ab9fd440fe19c41e315cffab27e976c723d)
|
||||
|
||||
#### [v3.3.5](https://github.com/th-ch/youtube-music/compare/v3.3.4...v3.3.5)
|
||||
|
||||
> 26 March 2024
|
||||
|
||||
- chore(deps): update dependency node-gyp to v10.1.0 [`#1885`](https://github.com/th-ch/youtube-music/pull/1885)
|
||||
- chore(deps): update dependency @typescript-eslint/eslint-plugin to v7.4.0 [`#1886`](https://github.com/th-ch/youtube-music/pull/1886)
|
||||
- chore(deps): update dependency vite to v5.2.6 [`#1883`](https://github.com/th-ch/youtube-music/pull/1883)
|
||||
- fix(style): resolve #1887 [`#1887`](https://github.com/th-ch/youtube-music/issues/1887)
|
||||
- chore(i18n): Translated using Weblate (Swedish) [`69087bb`](https://github.com/th-ch/youtube-music/commit/69087bbf1fac1ba58e992146deb1d6f1706b1e3c)
|
||||
- chore(i18n): Translated using Weblate (French) [`af78f15`](https://github.com/th-ch/youtube-music/commit/af78f1596ab8db2fa7069fdb1c4f078099ce4446)
|
||||
- Update changelog for v3.3.4 [`62f7d44`](https://github.com/th-ch/youtube-music/commit/62f7d440fab5bdbe9f49a3a5f8c32e7aaf2f28f6)
|
||||
|
||||
#### [v3.3.4](https://github.com/th-ch/youtube-music/compare/v3.3.3...v3.3.4)
|
||||
|
||||
> 24 March 2024
|
||||
|
||||
- Update changelog for v3.3.3 [`9769544`](https://github.com/th-ch/youtube-music/commit/97695444affbacb71dd73ae7107d4c987e285a37)
|
||||
- fix(style): fix fullscreen style and in-app-menu [`ed700c2`](https://github.com/th-ch/youtube-music/commit/ed700c2916cc7e6ccd2010d0c552364af116eb4f)
|
||||
- fix(style): fix miniplayer style [`a8bc539`](https://github.com/th-ch/youtube-music/commit/a8bc53912d1f4137008ecb2d9d5d9d9eb06ee2a8)
|
||||
|
||||
#### [v3.3.3](https://github.com/th-ch/youtube-music/compare/v3.3.2...v3.3.3)
|
||||
|
||||
> 24 March 2024
|
||||
|
||||
- chore(deps): update dependency electron to v29.1.5 [`#1876`](https://github.com/th-ch/youtube-music/pull/1876)
|
||||
- chore(deps): update dependency typescript to v5.4.3 [`#1877`](https://github.com/th-ch/youtube-music/pull/1877)
|
||||
- chore(deps): update dependency discord-api-types to v0.37.76 [`#1878`](https://github.com/th-ch/youtube-music/pull/1878)
|
||||
- chore(deps): update dependency vite to v5.2.4 [`#1881`](https://github.com/th-ch/youtube-music/pull/1881)
|
||||
- Ambient Plugin cleanup [`#1880`](https://github.com/th-ch/youtube-music/pull/1880)
|
||||
- chore(deps): update dependency vite to v5.2.2 [`#1875`](https://github.com/th-ch/youtube-music/pull/1875)
|
||||
- fix(deps): update dependency solid-js to v1.8.16 [`#1873`](https://github.com/th-ch/youtube-music/pull/1873)
|
||||
- chore(deps): update dependency @typescript-eslint/eslint-plugin to v7.3.1 [`#1868`](https://github.com/th-ch/youtube-music/pull/1868)
|
||||
- chore(deps): update dependency discord-api-types to v0.37.75 [`#1867`](https://github.com/th-ch/youtube-music/pull/1867)
|
||||
- chore(deps): update pnpm to v8.15.5 [`#1865`](https://github.com/th-ch/youtube-music/pull/1865)
|
||||
- fix: Fix Miniplayer image size [`#1863`](https://github.com/th-ch/youtube-music/pull/1863)
|
||||
- fix(style): fixed image/video alignment when toggle is active [`#1862`](https://github.com/th-ch/youtube-music/pull/1862)
|
||||
- chore: Update README-is.md [`#1858`](https://github.com/th-ch/youtube-music/pull/1858)
|
||||
- chore(deps): update dependency vite-plugin-solid to v2.10.2 [`#1859`](https://github.com/th-ch/youtube-music/pull/1859)
|
||||
- fix: Ambient Mode intialization improvement [`#1857`](https://github.com/th-ch/youtube-music/pull/1857)
|
||||
- chore(deps): bump follow-redirects from 1.15.5 to 1.15.6 [`#1856`](https://github.com/th-ch/youtube-music/pull/1856)
|
||||
- chore(README): Nicer Readme 2.0 [`#1833`](https://github.com/th-ch/youtube-music/pull/1833)
|
||||
- chore(deps): update dependency discord-api-types to v0.37.74 [`#1854`](https://github.com/th-ch/youtube-music/pull/1854)
|
||||
- chore(deps): update dependency esbuild to v0.20.2 [`#1855`](https://github.com/th-ch/youtube-music/pull/1855)
|
||||
- Improve ambient mode [`#1853`](https://github.com/th-ch/youtube-music/pull/1853)
|
||||
- chore(deps): update dependency electron to v29.1.4 [`#1852`](https://github.com/th-ch/youtube-music/pull/1852)
|
||||
- chore(deps): update dependency electron to v29.1.3 [`#1851`](https://github.com/th-ch/youtube-music/pull/1851)
|
||||
- chore(deps): update dependency rollup to v4.13.0 [`#1850`](https://github.com/th-ch/youtube-music/pull/1850)
|
||||
- fix(deps): update dependency electron-store to v8.2.0 [`#1843`](https://github.com/th-ch/youtube-music/pull/1843)
|
||||
- chore(deps): update dependency electron to v29.1.1 [`#1841`](https://github.com/th-ch/youtube-music/pull/1841)
|
||||
- fix(deps): update dependency i18next to v23.10.1 [`#1842`](https://github.com/th-ch/youtube-music/pull/1842)
|
||||
- chore(deps): update dependency @typescript-eslint/eslint-plugin to v7.2.0 [`#1848`](https://github.com/th-ch/youtube-music/pull/1848)
|
||||
- chore(deps): update dependency vite to v5.1.6 [`#1847`](https://github.com/th-ch/youtube-music/pull/1847)
|
||||
- fix(deps): update dependency async-mutex to v0.5.0 [`#1849`](https://github.com/th-ch/youtube-music/pull/1849)
|
||||
- fix(deps): update dependency ts-morph to v22 [`#1846`](https://github.com/th-ch/youtube-music/pull/1846)
|
||||
- chore(deps): update dependency discord-api-types to v0.37.73 [`#1840`](https://github.com/th-ch/youtube-music/pull/1840)
|
||||
- chore(deps): update dependency rollup to v4.12.1 [`#1837`](https://github.com/th-ch/youtube-music/pull/1837)
|
||||
- chore: Changed a single word (README-is.md) [`#1836`](https://github.com/th-ch/youtube-music/pull/1836)
|
||||
- chore(deps): update dependency typescript to v5.4.2 [`#1838`](https://github.com/th-ch/youtube-music/pull/1838)
|
||||
- chore(deps): update dependency electron-vite to v2.1.0 [`#1823`](https://github.com/th-ch/youtube-music/pull/1823)
|
||||
- chore(deps): update dependency @typescript-eslint/eslint-plugin to v7.1.1 [`#1829`](https://github.com/th-ch/youtube-music/pull/1829)
|
||||
- chore(deps): update dependency vite to v5.1.5 [`#1831`](https://github.com/th-ch/youtube-music/pull/1831)
|
||||
- Revert "chore(deps): update dependency electron-builder to v24.13.3" [`#1818`](https://github.com/th-ch/youtube-music/pull/1818)
|
||||
- chore(deps): update dependency electron-builder to v24.13.3 [`#1774`](https://github.com/th-ch/youtube-music/pull/1774)
|
||||
- chore(deps): update playwright monorepo to v1.42.1 [`#1816`](https://github.com/th-ch/youtube-music/pull/1816)
|
||||
- fix: Add scale ratio for tray icons [`#1811`](https://github.com/th-ch/youtube-music/pull/1811)
|
||||
- Icelandic translation of the readme file [`#1806`](https://github.com/th-ch/youtube-music/pull/1806)
|
||||
- chore(deps): update dependency electron to v29.1.0 [`#1808`](https://github.com/th-ch/youtube-music/pull/1808)
|
||||
- chore(deps): update playwright monorepo to v1.42.0 [`#1805`](https://github.com/th-ch/youtube-music/pull/1805)
|
||||
- chore(deps): update dependency eslint to v8.57.0 [`#1793`](https://github.com/th-ch/youtube-music/pull/1793)
|
||||
- chore(deps): update dependency @typescript-eslint/eslint-plugin to v7.1.0 [`#1800`](https://github.com/th-ch/youtube-music/pull/1800)
|
||||
- chore(deps): update dependency discord-api-types to v0.37.71 [`#1799`](https://github.com/th-ch/youtube-music/pull/1799)
|
||||
- chore(deps): update pnpm to v8.15.4 [`#1795`](https://github.com/th-ch/youtube-music/pull/1795)
|
||||
- chore(deps): update dependency @types/semver to v7.5.8 [`#1797`](https://github.com/th-ch/youtube-music/pull/1797)
|
||||
- fix: center the pause icon [`#1786`](https://github.com/th-ch/youtube-music/pull/1786)
|
||||
- fix(deps): update dependency @cliqz/adblocker-electron to v1.26.16 [`#1788`](https://github.com/th-ch/youtube-music/pull/1788)
|
||||
- fix(deps): update dependency @cliqz/adblocker-electron-preload to v1.26.16 [`#1789`](https://github.com/th-ch/youtube-music/pull/1789)
|
||||
- fix(deps): update dependency youtubei.js to v9.1.0 [`#1790`](https://github.com/th-ch/youtube-music/pull/1790)
|
||||
- fix(deps): update dependency i18next to v23.10.0 [`#1785`](https://github.com/th-ch/youtube-music/pull/1785)
|
||||
- chore(deps): update dependency electron to v29 [`#1773`](https://github.com/th-ch/youtube-music/pull/1773)
|
||||
- chore(deps): update dependency vite to v5.1.4 [`#1778`](https://github.com/th-ch/youtube-music/pull/1778)
|
||||
- chore(deps): bump ip from 2.0.0 to 2.0.1 [`#1777`](https://github.com/th-ch/youtube-music/pull/1777)
|
||||
- fix: add support for Wayland [`#1864`](https://github.com/th-ch/youtube-music/issues/1864)
|
||||
- fix(style): fix navigation bar items are not working [`#1381`](https://github.com/th-ch/youtube-music/issues/1381) [`#1396`](https://github.com/th-ch/youtube-music/issues/1396) [`#1649`](https://github.com/th-ch/youtube-music/issues/1649)
|
||||
- fix(ytm-bugs): fixed a `scrollbar-color` bug that affected Chromium 121 and later [`#1737`](https://github.com/th-ch/youtube-music/issues/1737)
|
||||
- chore(i18n): Translated using Weblate (Icelandic) [`82fa871`](https://github.com/th-ch/youtube-music/commit/82fa8719a96abdfaaa8548a0077f4db2164ec09b)
|
||||
- chore(i18n): Translated using Weblate (Romanian) [`c871506`](https://github.com/th-ch/youtube-music/commit/c871506a69180308ab4fc587b6e8a33f193087e8)
|
||||
- chore(i18n): Translated using Weblate (Thai) [`a7d0350`](https://github.com/th-ch/youtube-music/commit/a7d035022a229f0b245694d1fc7a484befe1c269)
|
||||
|
||||
#### [v3.3.2](https://github.com/th-ch/youtube-music/compare/v3.3.1...v3.3.2)
|
||||
|
||||
> 20 February 2024
|
||||
|
||||
- fix: fix bugs in MPRIS, and improve MPRIS [`#1760`](https://github.com/th-ch/youtube-music/pull/1760)
|
||||
- fix(deps): update dependency electron-updater to v6.1.8 [`#1770`](https://github.com/th-ch/youtube-music/pull/1770)
|
||||
- chore(deps): update dependency electron-builder to v24.12.0 [`#1771`](https://github.com/th-ch/youtube-music/pull/1771)
|
||||
- feat(scrobblers): use `BrowserWindow` instead of `shell.openExternal` [`#1758`](https://github.com/th-ch/youtube-music/pull/1758)
|
||||
- chore(deps): update dependency @typescript-eslint/eslint-plugin to v7.0.2 [`#1763`](https://github.com/th-ch/youtube-music/pull/1763)
|
||||
- chore(deps): update dependency esbuild to v0.20.1 [`#1759`](https://github.com/th-ch/youtube-music/pull/1759)
|
||||
- fix(deps): update dependency i18next to v23.9.0 [`#1754`](https://github.com/th-ch/youtube-music/pull/1754)
|
||||
- fix: fixed an issue that caused infinite loops when using Music Together [`#1752`](https://github.com/th-ch/youtube-music/issues/1752)
|
||||
- chore(deps): rollback dependency electron-builder to v24.9.1 [`8bd05f5`](https://github.com/th-ch/youtube-music/commit/8bd05f525df98671f0a516b159cccab302b7ae99)
|
||||
- chore(deps): update dependency electron-builder to v24.13.1 [`47b23b4`](https://github.com/th-ch/youtube-music/commit/47b23b414c8feb25c4d9a23d6adb7cbf1ac818fb)
|
||||
- chore(i18n): Translated using Weblate (German) [`47505e9`](https://github.com/th-ch/youtube-music/commit/47505e97482f9e953ee451b968d0950585616ffa)
|
||||
|
||||
#### [v3.3.1](https://github.com/th-ch/youtube-music/compare/v3.3.0...v3.3.1)
|
||||
|
||||
> 18 February 2024
|
||||
|
||||
- Update changelog for v3.3.0 [`6d9bb8e`](https://github.com/th-ch/youtube-music/commit/6d9bb8eb1cc2d892a5552ffb1f7c20859aa80f67)
|
||||
- hotfix: in-app-menu position issue [`87acf4c`](https://github.com/th-ch/youtube-music/commit/87acf4cf042ba32a000a4aeaec5c17c93501d333)
|
||||
- release 3.3.1 (HOTFIX) [`a6ed8bf`](https://github.com/th-ch/youtube-music/commit/a6ed8bf3aa20ca8e950e85d88f981ccf9edc7498)
|
||||
|
||||
#### [v3.3.0](https://github.com/th-ch/youtube-music/compare/v3.2.2...v3.3.0)
|
||||
|
||||
> 18 February 2024
|
||||
|
||||
- fix(deps): update dependency i18next to v23.8.3 [`#1751`](https://github.com/th-ch/youtube-music/pull/1751)
|
||||
- import fixed ./constants [`#1748`](https://github.com/th-ch/youtube-music/pull/1748)
|
||||
- chore(deps): update dependency rollup to v4.12.0 [`#1743`](https://github.com/th-ch/youtube-music/pull/1743)
|
||||
- chore(deps): bump undici from 5.28.2 to 5.28.3 [`#1747`](https://github.com/th-ch/youtube-music/pull/1747)
|
||||
- chore(deps): update dependency vite to v5.1.3 [`#1742`](https://github.com/th-ch/youtube-music/pull/1742)
|
||||
- chore(deps): update dependency vite-plugin-solid to v2.10.1 [`#1734`](https://github.com/th-ch/youtube-music/pull/1734)
|
||||
- chore(deps): update dependency discord-api-types to v0.37.70 [`#1740`](https://github.com/th-ch/youtube-music/pull/1740)
|
||||
- chore(deps): update dependency electron to v28.2.3 [`#1736`](https://github.com/th-ch/youtube-music/pull/1736)
|
||||
- chore(deps): update pnpm to v8.15.3 [`#1739`](https://github.com/th-ch/youtube-music/pull/1739)
|
||||
- chore(deps): update dependency rollup to v4.11.0 [`#1738`](https://github.com/th-ch/youtube-music/pull/1738)
|
||||
- fix(deps): update dependency solid-js to v1.8.15 [`#1735`](https://github.com/th-ch/youtube-music/pull/1735)
|
||||
- chore(deps): update dependency vite to v5.1.2 [`#1733`](https://github.com/th-ch/youtube-music/pull/1733)
|
||||
- chore(deps): update dependency vite-plugin-solid to v2.10.0 [`#1732`](https://github.com/th-ch/youtube-music/pull/1732)
|
||||
- chore(deps): update pnpm to v8.15.2 [`#1729`](https://github.com/th-ch/youtube-music/pull/1729)
|
||||
- Update Copyright - 2024 [`#1730`](https://github.com/th-ch/youtube-music/pull/1730)
|
||||
- chore(deps): update dependency @typescript-eslint/eslint-plugin to v7 [`#1728`](https://github.com/th-ch/youtube-music/pull/1728)
|
||||
- fix(deps): update dependency @floating-ui/dom to v1.6.3 [`#1727`](https://github.com/th-ch/youtube-music/pull/1727)
|
||||
- chore(deps): update dependency electron to v28.2.2 [`#1717`](https://github.com/th-ch/youtube-music/pull/1717)
|
||||
- chore(deps): update dependency vite to v5.1.1 [`#1718`](https://github.com/th-ch/youtube-music/pull/1718)
|
||||
- chore(deps): update dependency @types/semver to v7.5.7 [`#1724`](https://github.com/th-ch/youtube-music/pull/1724)
|
||||
- fix(deps): update dependency @floating-ui/dom to v1.6.2 [`#1725`](https://github.com/th-ch/youtube-music/pull/1725)
|
||||
- chore(deps): update dependency rollup to v4.10.0 [`#1719`](https://github.com/th-ch/youtube-music/pull/1719)
|
||||
- fix(deps): update dependency solid-js to v1.8.14 [`#1713`](https://github.com/th-ch/youtube-music/pull/1713)
|
||||
- chore(deps): update dependency @typescript-eslint/eslint-plugin to v6.21.0 [`#1711`](https://github.com/th-ch/youtube-music/pull/1711)
|
||||
- fix(deps): update dependency semver to v7.6.0 [`#1712`](https://github.com/th-ch/youtube-music/pull/1712)
|
||||
- refactor(in-app-menu): refactor `in-app-menu` plugin [`#1710`](https://github.com/th-ch/youtube-music/pull/1710)
|
||||
- chore(deps): update playwright monorepo to v1.41.2 [`#1706`](https://github.com/th-ch/youtube-music/pull/1706)
|
||||
- chore(deps): update dependency electron to v29.0.0-beta.5 [`#1707`](https://github.com/th-ch/youtube-music/pull/1707)
|
||||
- feat(album-color-theme): support album color theme in all pages [`#1685`](https://github.com/th-ch/youtube-music/pull/1685)
|
||||
- fix(deps): update dependency youtubei.js to v9.0.2 [`#1704`](https://github.com/th-ch/youtube-music/pull/1704)
|
||||
- fix(deps): update dependency i18next to v23.8.2 [`#1702`](https://github.com/th-ch/youtube-music/pull/1702)
|
||||
- feat: Support disabling scrobbling for non-music content [`#1665`](https://github.com/th-ch/youtube-music/pull/1665)
|
||||
- fix(deps): update dependency youtubei.js to v9 [`#1682`](https://github.com/th-ch/youtube-music/pull/1682)
|
||||
- chore(deps): update dependency electron to v29.0.0-beta.4 [`#1698`](https://github.com/th-ch/youtube-music/pull/1698)
|
||||
- fix(deps): update dependency i18next to v23.8.1 [`#1694`](https://github.com/th-ch/youtube-music/pull/1694)
|
||||
- chore(deps): update dependency @typescript-eslint/eslint-plugin to v6.20.0 [`#1700`](https://github.com/th-ch/youtube-music/pull/1700)
|
||||
- chore(deps): update pnpm to v8.15.1 [`#1699`](https://github.com/th-ch/youtube-music/pull/1699)
|
||||
- chore(deps): update dependency esbuild to v0.20.0 [`#1691`](https://github.com/th-ch/youtube-music/pull/1691)
|
||||
- chore(deps): update pnpm to v8.15.0 [`#1692`](https://github.com/th-ch/youtube-music/pull/1692)
|
||||
- fix(deps): update dependency i18next to v23.7.20 [`#1684`](https://github.com/th-ch/youtube-music/pull/1684)
|
||||
- chore(deps): update dependency electron to v29.0.0-beta.3 [`#1683`](https://github.com/th-ch/youtube-music/pull/1683)
|
||||
- chore(deps): update dependency electron to v29.0.0-beta.2 [`#1681`](https://github.com/th-ch/youtube-music/pull/1681)
|
||||
- chore(deps): update dependency rollup to v4.9.6 [`#1663`](https://github.com/th-ch/youtube-music/pull/1663)
|
||||
- chore(deps): update dependency electron to v29.0.0-beta.1 [`#1670`](https://github.com/th-ch/youtube-music/pull/1670)
|
||||
- fix(deps): update dependency i18next to v23.7.19 [`#1680`](https://github.com/th-ch/youtube-music/pull/1680)
|
||||
- chore(deps): update dependency @typescript-eslint/eslint-plugin to v6.19.1 [`#1669`](https://github.com/th-ch/youtube-music/pull/1669)
|
||||
- chore(deps): update pnpm to v8.14.3 [`#1668`](https://github.com/th-ch/youtube-music/pull/1668)
|
||||
- chore(deps): update dependency vite-plugin-inspect to v0.8.3 [`#1672`](https://github.com/th-ch/youtube-music/pull/1672)
|
||||
- chore(deps): update dependency esbuild to v0.19.12 [`#1673`](https://github.com/th-ch/youtube-music/pull/1673)
|
||||
- fix(deps): update dependency @electron/remote to v2.1.2 [`#1676`](https://github.com/th-ch/youtube-music/pull/1676)
|
||||
- chore: Update issue templates [`#1661`](https://github.com/th-ch/youtube-music/pull/1661)
|
||||
- chore(deps): update playwright monorepo to v1.41.1 [`#1660`](https://github.com/th-ch/youtube-music/pull/1660)
|
||||
- fix(deps): update dependency i18next to v23.7.18 [`#1662`](https://github.com/th-ch/youtube-music/pull/1662)
|
||||
- chore(deps): update actions/dependency-review-action action to v4 [`#1654`](https://github.com/th-ch/youtube-music/pull/1654)
|
||||
- chore(deps): update dependency electron to v29.0.0-alpha.11 [`#1656`](https://github.com/th-ch/youtube-music/pull/1656)
|
||||
- chore(deps): update dependency vite to v5.0.12 [security] [`#1659`](https://github.com/th-ch/youtube-music/pull/1659)
|
||||
- fix(deps): update dependency async-mutex to v0.4.1 [`#1653`](https://github.com/th-ch/youtube-music/pull/1653)
|
||||
- chore(deps): update playwright monorepo to v1.41.0 [`#1651`](https://github.com/th-ch/youtube-music/pull/1651)
|
||||
- feat: Better Scrobbler Plugin [`#1640`](https://github.com/th-ch/youtube-music/pull/1640)
|
||||
- chore(deps): update dependency electron to v29.0.0-alpha.10 [`#1645`](https://github.com/th-ch/youtube-music/pull/1645)
|
||||
- chore(deps): update dependency @typescript-eslint/eslint-plugin to v6.19.0 [`#1643`](https://github.com/th-ch/youtube-music/pull/1643)
|
||||
- chore(README): Fix plugins names and add plugins in/to Readme (in menu too) [`#1624`](https://github.com/th-ch/youtube-music/pull/1624)
|
||||
- fix(album-actions): Fixed album actions [`#1639`](https://github.com/th-ch/youtube-music/pull/1639)
|
||||
- chore(deps): update playwright monorepo to v1.41.0-beta-1705101589000 [`#1638`](https://github.com/th-ch/youtube-music/pull/1638)
|
||||
- fix(#1543): fix song control doesn't work [`#1637`](https://github.com/th-ch/youtube-music/pull/1637)
|
||||
- chore(deps): update playwright monorepo to v1.41.0-beta-1705092460000 [`#1635`](https://github.com/th-ch/youtube-music/pull/1635)
|
||||
- chore(deps): update dependency rollup to v4.9.5 [`#1629`](https://github.com/th-ch/youtube-music/pull/1629)
|
||||
- chore(deps): update dependency electron to v29.0.0-alpha.9 [`#1627`](https://github.com/th-ch/youtube-music/pull/1627)
|
||||
- chore(deps): update dependency electron to v29.0.0-alpha.8 [`#1608`](https://github.com/th-ch/youtube-music/pull/1608)
|
||||
- fix(deps): update dependency @cliqz/adblocker-electron to v1.26.15 [`#1615`](https://github.com/th-ch/youtube-music/pull/1615)
|
||||
- chore(deps): update dependency rollup to v4.9.4 [`#1591`](https://github.com/th-ch/youtube-music/pull/1591)
|
||||
- fix(deps): update dependency @cliqz/adblocker-electron-preload to v1.26.15 [`#1616`](https://github.com/th-ch/youtube-music/pull/1616)
|
||||
- chore(deps): update pnpm to v8.14.1 [`#1619`](https://github.com/th-ch/youtube-music/pull/1619)
|
||||
- chore(deps): update dependency eslint-plugin-prettier to v5.1.3 [`#1618`](https://github.com/th-ch/youtube-music/pull/1618)
|
||||
- chore(deps): update dependency @typescript-eslint/eslint-plugin to v6.18.1 [`#1612`](https://github.com/th-ch/youtube-music/pull/1612)
|
||||
- fix(deps): update dependency youtubei.js to v8.2.0 [`#1614`](https://github.com/th-ch/youtube-music/pull/1614)
|
||||
- chore(deps): update dependency electron-vite to v2.0.0 [`#1609`](https://github.com/th-ch/youtube-music/pull/1609)
|
||||
- chore(deps): update dependency @typescript-eslint/eslint-plugin to v6.18.0 [`#1603`](https://github.com/th-ch/youtube-music/pull/1603)
|
||||
- chore(deps): update dependency electron-vite to v2.0.0-beta.4 [`#1602`](https://github.com/th-ch/youtube-music/pull/1602)
|
||||
- fix: fix upgrade button [`#1199`](https://github.com/th-ch/youtube-music/issues/1199)
|
||||
- fix(mpris): fix mpris invalid position [`#1726`](https://github.com/th-ch/youtube-music/issues/1726)
|
||||
- fix: discord RPC (fix #1664) [`#1664`](https://github.com/th-ch/youtube-music/issues/1664)
|
||||
- fix: remove sign-in button (fix #1199) [`#1199`](https://github.com/th-ch/youtube-music/issues/1199)
|
||||
- Fix #1617 [`#1617`](https://github.com/th-ch/youtube-music/issues/1617)
|
||||
- fix(crossfade): fix #1633 [`#1633`](https://github.com/th-ch/youtube-music/issues/1633)
|
||||
- fix: fix #1621 [`#1621`](https://github.com/th-ch/youtube-music/issues/1621)
|
||||
- fix(tuna-obs): partially fix #1596 [`#1596`](https://github.com/th-ch/youtube-music/issues/1596)
|
||||
- fix(discord): fix hide duration button [`#1644`](https://github.com/th-ch/youtube-music/issues/1644)
|
||||
- fix(in-app-menu): fix invalid `margin-top` [`#1597`](https://github.com/th-ch/youtube-music/issues/1597)
|
||||
- fix(README): fix `plugins` path [`#1598`](https://github.com/th-ch/youtube-music/issues/1598)
|
||||
- chore(i18n): Translated using Weblate (Vietnamese) [`0528637`](https://github.com/th-ch/youtube-music/commit/05286371353e8b4c36a5b9fe9011ae5dfdc7ee82)
|
||||
- chore: update pnpm-lock [`fd8d59b`](https://github.com/th-ch/youtube-music/commit/fd8d59bada56dab4e156d22394fe0c5efec5abc4)
|
||||
- fix(in-app-menu): fix app crash in production [`febc63e`](https://github.com/th-ch/youtube-music/commit/febc63edef375bd82db48b7fb460ec5a601ab872)
|
||||
|
||||
#### [v3.2.2](https://github.com/th-ch/youtube-music/compare/v3.2.1...v3.2.2)
|
||||
|
||||
> 5 January 2024
|
||||
|
||||
- feat(tray): Add song info and paused icon [`#1592`](https://github.com/th-ch/youtube-music/pull/1592)
|
||||
- fix(skip-silences): fix audio distorted [`#1141`](https://github.com/th-ch/youtube-music/issues/1141)
|
||||
- chore(deps): update dependency rollup to v4.9.3 [`0c3c380`](https://github.com/th-ch/youtube-music/commit/0c3c3805918adf2a185a7f1dc67ea3af8135863d)
|
||||
- chore(i18n): Translated using Weblate (Turkish) [`64ea1fd`](https://github.com/th-ch/youtube-music/commit/64ea1fdb58fdf2766ae3284ac1a51bfac8894b36)
|
||||
- fix(music-together): typing [`895386f`](https://github.com/th-ch/youtube-music/commit/895386f6f8c649f77ea15c88f6fb7ecc5b775554)
|
||||
|
||||
#### [v3.2.1](https://github.com/th-ch/youtube-music/compare/v3.2.0...v3.2.1)
|
||||
|
||||
> 1 January 2024
|
||||
|
||||
- fix: fix #1574 [`#1574`](https://github.com/th-ch/youtube-music/issues/1574)
|
||||
- fix: fix #1575 [`#1575`](https://github.com/th-ch/youtube-music/issues/1575)
|
||||
- chore(i18n): Translated using Weblate [`f5aa179`](https://github.com/th-ch/youtube-music/commit/f5aa179cd639eb4b8f70f1264b5b459ebcc16695)
|
||||
- chore(i18n): Translated using Weblate (English) [`e409165`](https://github.com/th-ch/youtube-music/commit/e409165e1bed85f3d1aea3a565e7b9e462b1e05b)
|
||||
- chore(i18n): Translated using Weblate (Czech) [`0ca4e34`](https://github.com/th-ch/youtube-music/commit/0ca4e34efd86e877314e5a245f266065b4cf0013)
|
||||
|
||||
#### [v3.2.0](https://github.com/th-ch/youtube-music/compare/v3.1.1...v3.2.0)
|
||||
|
||||
> 1 January 2024
|
||||
|
||||
- feat(album-color-theme): improve `Album Color Theme` style [`#1571`](https://github.com/th-ch/youtube-music/pull/1571)
|
||||
- feat(menu): add more detail in Menu [`#1570`](https://github.com/th-ch/youtube-music/pull/1570)
|
||||
- feat(music-together): Add new plugin `Music Together` [`#1562`](https://github.com/th-ch/youtube-music/pull/1562)
|
||||
- chore(deps): update dependency rollup to v4.9.2 [`#1567`](https://github.com/th-ch/youtube-music/pull/1567)
|
||||
- fix(deps): update dependency i18next to v23.7.13 [`#1569`](https://github.com/th-ch/youtube-music/pull/1569)
|
||||
- feat: Add new plugin `Album actions` [`#1515`](https://github.com/th-ch/youtube-music/pull/1515)
|
||||
- fix(deps): update dependency i18next to v23.7.12 [`#1564`](https://github.com/th-ch/youtube-music/pull/1564)
|
||||
- fix: Only apply scale factor on Windows [`#1565`](https://github.com/th-ch/youtube-music/pull/1565)
|
||||
- chore(deps): update dependency @typescript-eslint/eslint-plugin to v6.16.0 [`#1556`](https://github.com/th-ch/youtube-music/pull/1556)
|
||||
- chore(deps): update pnpm to v8.13.1 [`#1557`](https://github.com/th-ch/youtube-music/pull/1557)
|
||||
- chore(deps): update dependency ws to v8.16.0 [`#1559`](https://github.com/th-ch/youtube-music/pull/1559)
|
||||
- fix(deps): update dependency youtubei.js to v8.1.0 [`#1560`](https://github.com/th-ch/youtube-music/pull/1560)
|
||||
- fix(deps): update dependency node-html-parser to v6.1.12 [`#1554`](https://github.com/th-ch/youtube-music/pull/1554)
|
||||
- Revert "fix(deps): update dependency @xhayper/discord-rpc to v1.1.2" [`#1552`](https://github.com/th-ch/youtube-music/pull/1552)
|
||||
- feat(ambient-mode): support ambient mode on `Song section` [`#1555`](https://github.com/th-ch/youtube-music/issues/1555)
|
||||
- fix: fixed an issue with the download button disappearing [`#1551`](https://github.com/th-ch/youtube-music/issues/1551)
|
||||
- fix: fix `homebrew cask` [`#1514`](https://github.com/th-ch/youtube-music/issues/1514)
|
||||
- fix: pnpm build error [`13ef856`](https://github.com/th-ch/youtube-music/commit/13ef8560ff43353030537403be7da82542ba535e)
|
||||
- chore(i18n): Translated using Weblate (Czech) [`0dc9c6a`](https://github.com/th-ch/youtube-music/commit/0dc9c6a1a90bce6505614617b827e816cbaaf875)
|
||||
- chore(deps): update dependency @typescript-eslint/eslint-plugin to v6.15.0 [`c5bcd89`](https://github.com/th-ch/youtube-music/commit/c5bcd89f164b51d7380486a8ae35edd0caeea842)
|
||||
|
||||
#### [v3.1.1](https://github.com/th-ch/youtube-music/compare/v3.1.0...v3.1.1)
|
||||
|
||||
> 18 December 2023
|
||||
|
||||
- fix: fix renderer plugin load timing [`#1522`](https://github.com/th-ch/youtube-music/issues/1522)
|
||||
- chore(i18n): Translated using Weblate (Lithuanian) [`fc1a7cd`](https://github.com/th-ch/youtube-music/commit/fc1a7cda62b6e33e5f5d57a5a6e0adef6a32bf9a)
|
||||
- chore(i18n): Translated using Weblate (Chinese (Simplified)) [`eba7026`](https://github.com/th-ch/youtube-music/commit/eba7026b89bbfdd3ac07cf728a66ba9bdd274ec0)
|
||||
- chore(deps): update dependency rollup to v4.8.0 [`a601d0b`](https://github.com/th-ch/youtube-music/commit/a601d0b3d2dee0fabad79a18e1a7dd0ca84ccf01)
|
||||
|
||||
#### [v3.1.0](https://github.com/th-ch/youtube-music/compare/v3.0.2...v3.1.0)
|
||||
|
||||
> 11 December 2023
|
||||
|
||||
- chore(deps): update dependency electron to v28 [`#1498`](https://github.com/th-ch/youtube-music/pull/1498)
|
||||
- Enable/Disable Navigation without restart [`#1507`](https://github.com/th-ch/youtube-music/pull/1507)
|
||||
- Turkish(tr)_lang_file [`#1513`](https://github.com/th-ch/youtube-music/pull/1513)
|
||||
- Skip Disliked Songs [`#1505`](https://github.com/th-ch/youtube-music/pull/1505)
|
||||
- chore(deps): update dependency @typescript-eslint/eslint-plugin to v6.13.2 [`#1452`](https://github.com/th-ch/youtube-music/pull/1452)
|
||||
- fix: Homebrew latest release url parsing [`#1496`](https://github.com/th-ch/youtube-music/pull/1496)
|
||||
- fix: in-player adblocker inject timing issue [`#1478`](https://github.com/th-ch/youtube-music/issues/1478)
|
||||
- fix(package.json): fix RPM version `libuuid` issue [`#1508`](https://github.com/th-ch/youtube-music/issues/1508)
|
||||
- Translated using Weblate (Polish) [`7b78ba6`](https://github.com/th-ch/youtube-music/commit/7b78ba67613f14be65a45751efeb06431b405a91)
|
||||
- Translated using Weblate (French) [`ebc0879`](https://github.com/th-ch/youtube-music/commit/ebc087963b23265ff00528c8305d51597abf587a)
|
||||
- Translated using Weblate (Chinese (Traditional)) [`020bdc0`](https://github.com/th-ch/youtube-music/commit/020bdc0811ea45ad6c2853c62a05ae6695c5c4f9)
|
||||
|
||||
#### [v3.0.2](https://github.com/th-ch/youtube-music/compare/v3.0.1...v3.0.2)
|
||||
|
||||
> 3 December 2023
|
||||
|
||||
- fix(adblocker): fix In-Player adblocker [`#1478`](https://github.com/th-ch/youtube-music/issues/1478)
|
||||
- fix(menu): crash on linux [`#1477`](https://github.com/th-ch/youtube-music/issues/1477)
|
||||
- fix: update pnpm-lock.yaml [`9e2c6b1`](https://github.com/th-ch/youtube-music/commit/9e2c6b1afa33b5708853c8328946e68ec45b09c3)
|
||||
- Translated using Weblate (Chinese (Traditional)) [`125b69f`](https://github.com/th-ch/youtube-music/commit/125b69fd75a05c3eb893886119e2d9f2332b3e56)
|
||||
- Translated using Weblate (French) [`15c4551`](https://github.com/th-ch/youtube-music/commit/15c455105b5100a8ee2bd0a4631548d3d455f047)
|
||||
|
||||
#### [v3.0.1](https://github.com/th-ch/youtube-music/compare/v3.0.0...v3.0.1)
|
||||
|
||||
> 2 December 2023
|
||||
|
||||
- hotfix(adblocker): fix #1475 [`#1475`](https://github.com/th-ch/youtube-music/issues/1475)
|
||||
- Translated using Weblate (French) [`7f02afc`](https://github.com/th-ch/youtube-music/commit/7f02afc5a6839adfe8437d4e2cc8dee13a93b311)
|
||||
- Update changelog for v3.0.0 [`d8c8bd1`](https://github.com/th-ch/youtube-music/commit/d8c8bd17ecfbdf96ebd29eb4c5748c07876ee242)
|
||||
- Translated using Weblate (German) [`0660f0b`](https://github.com/th-ch/youtube-music/commit/0660f0b7ce6895ef5800f48ade1da2d7f8e0c1f7)
|
||||
|
||||
### [v3.0.0](https://github.com/th-ch/youtube-music/compare/v2.2.0...v3.0.0)
|
||||
|
||||
> 2 December 2023
|
||||
|
||||
- Add text to Translation section [`#1470`](https://github.com/th-ch/youtube-music/pull/1470)
|
||||
- fix(deps): update dependency youtubei.js to v8 [`#1473`](https://github.com/th-ch/youtube-music/pull/1473)
|
||||
- chore(deps): update dependency electron to v27.1.3 [`#1471`](https://github.com/th-ch/youtube-music/pull/1471)
|
||||
- fix(deps): update dependency @xhayper/discord-rpc to v1.1.1 [`#1472`](https://github.com/th-ch/youtube-music/pull/1472)
|
||||
- feat: add support i18n [`#1468`](https://github.com/th-ch/youtube-music/pull/1468)
|
||||
- chore(deps): update dependency electron to v27.1.2 [`#1441`](https://github.com/th-ch/youtube-music/pull/1441)
|
||||
- Nicer Readme [`#1439`](https://github.com/th-ch/youtube-music/pull/1439)
|
||||
- Windows Zoom, ScaleFactor [`#1402`](https://github.com/th-ch/youtube-music/pull/1402)
|
||||
- chore(deps): bump axios from 1.5.1 to 1.6.1 [`#1400`](https://github.com/th-ch/youtube-music/pull/1400)
|
||||
- Updated mac icon to better reflect the Mac styling [`#1395`](https://github.com/th-ch/youtube-music/pull/1395)
|
||||
- feat: rename plugins to clarify context [`#1392`](https://github.com/th-ch/youtube-music/pull/1392)
|
||||
- feat: refactor plugin utils [`#1391`](https://github.com/th-ch/youtube-music/pull/1391)
|
||||
- feat: plugin auto-importer with `vite-plugin-resolve` [`#1385`](https://github.com/th-ch/youtube-music/pull/1385)
|
||||
- feat: migrate from `rollup` to `electron-vite` [`#1364`](https://github.com/th-ch/youtube-music/pull/1364)
|
||||
- feat: enable `context-isolation` [`#1361`](https://github.com/th-ch/youtube-music/pull/1361)
|
||||
- fix: add workaround for `podcast` type video [`#1362`](https://github.com/th-ch/youtube-music/pull/1362)
|
||||
- fix: fix broken menu-layout [`#1360`](https://github.com/th-ch/youtube-music/pull/1360)
|
||||
- Add Homebrew cask install option for MacOS. [`#1357`](https://github.com/th-ch/youtube-music/pull/1357)
|
||||
- feat: changed Zoom shortcuts to standard [`#1458`](https://github.com/th-ch/youtube-music/issues/1458)
|
||||
- fix(in-app-menu): fix #1436 [`#1436`](https://github.com/th-ch/youtube-music/issues/1436)
|
||||
- fix(discord): update application client-id [`#1431`](https://github.com/th-ch/youtube-music/issues/1431)
|
||||
- chore(deps): update dependency electron to v27.0.4 [`#1324`](https://github.com/th-ch/youtube-music/issues/1324)
|
||||
- fix(in-app-menu): panel should close with the window when it is closed [`#1389`](https://github.com/th-ch/youtube-music/issues/1389)
|
||||
- fix: change titleBarOverlay height based on zoomFactor [`#1375`](https://github.com/th-ch/youtube-music/issues/1375)
|
||||
- fix: fixed an issue if "Always on top" is enabled, the dialog is displayed below the window [`#1379`](https://github.com/th-ch/youtube-music/issues/1379)
|
||||
- fix: fix winget version (fix #1363) [`#1363`](https://github.com/th-ch/youtube-music/issues/1363)
|
||||
- feat: run prettier [`a3104fd`](https://github.com/th-ch/youtube-music/commit/a3104fda4b0d58b076d0c737111636a66e468acc)
|
||||
- Translated using Weblate (Korean) [`b4b7ad8`](https://github.com/th-ch/youtube-music/commit/b4b7ad824b8c489ae483eba139b46e5b200231fc)
|
||||
- Translated using Weblate (English) [`d2eabaa`](https://github.com/th-ch/youtube-music/commit/d2eabaa4bbccd89eae529eae52cec035e8e2620c)
|
||||
|
||||
#### [v2.2.0](https://github.com/th-ch/youtube-music/compare/v2.1.3...v2.2.0)
|
||||
|
||||
> 27 October 2023
|
||||
|
||||
- feat(ambient-mode): add config for `ambient-mode` plugin [`#1349`](https://github.com/th-ch/youtube-music/pull/1349)
|
||||
- bump deps [`4248d20`](https://github.com/th-ch/youtube-music/commit/4248d20e8ef926ce7b1d07eb83743755a341d9f6)
|
||||
- Update changelog for v2.1.3 [`dc73561`](https://github.com/th-ch/youtube-music/commit/dc73561c8a8acfc8ba91aff2dc78e4267869f2fd)
|
||||
- Bump version to 2.2.0 [`6288d0b`](https://github.com/th-ch/youtube-music/commit/6288d0b171a65ea015922cdf3af6c7bd9a1f269b)
|
||||
|
||||
#### [v2.1.3](https://github.com/th-ch/youtube-music/compare/v2.1.2...v2.1.3)
|
||||
|
||||
> 23 October 2023
|
||||
|
||||
- fix: fixed bugs in downloader [`#1342`](https://github.com/th-ch/youtube-music/pull/1342)
|
||||
- feat(discord): rename `Listen Along` to `Play on YTM` [`#1341`](https://github.com/th-ch/youtube-music/issues/1341)
|
||||
- chore(deps): bump deps [`4333891`](https://github.com/th-ch/youtube-music/commit/4333891ccabe42aedf756fd48618be715db13262)
|
||||
- Update changelog for v2.1.2 [`fa4c69d`](https://github.com/th-ch/youtube-music/commit/fa4c69d228d4e06a7858e2b22fcdfa075a8ca766)
|
||||
- fix(store): fix listenAlong statement [`bceaa05`](https://github.com/th-ch/youtube-music/commit/bceaa05197d47a4a4bbd22e767d1e4d6ec277514)
|
||||
|
||||
#### [v2.1.2](https://github.com/th-ch/youtube-music/compare/v2.1.1...v2.1.2)
|
||||
|
||||
> 19 October 2023
|
||||
|
||||
- feat(in-app-menu): add an option to hide the window controls [`#1335`](https://github.com/th-ch/youtube-music/pull/1335)
|
||||
- fix: fixed an issue where the album name was missing [`#1334`](https://github.com/th-ch/youtube-music/pull/1334)
|
||||
- chore(deps): update dependency electron to v27.0.1 [`#1331`](https://github.com/th-ch/youtube-music/pull/1331)
|
||||
- fix: fixed an issue where only the first 100 songs in a playlist were downloaded [`#1329`](https://github.com/th-ch/youtube-music/pull/1329)
|
||||
- Updated readme plugins list [`#1326`](https://github.com/th-ch/youtube-music/pull/1326)
|
||||
- QOL: Move source code under the src directory. [`#1318`](https://github.com/th-ch/youtube-music/pull/1318)
|
||||
- feat: migrate from `npm` to `pnpm` [`#1316`](https://github.com/th-ch/youtube-music/pull/1316)
|
||||
- fix: fix unresponsive (fix #1325) [`#1325`](https://github.com/th-ch/youtube-music/issues/1325)
|
||||
- fix(blocker): remove the `app.isPackaged` check (fix #1315) [`#1315`](https://github.com/th-ch/youtube-music/issues/1315)
|
||||
- fix(discord): `Discord RPC fails if a song's title is only one character` (fix #1314) [`#1314`](https://github.com/th-ch/youtube-music/issues/1314)
|
||||
- chore(deps): Bump @rollup/plugin-commonjs, pnpm version, Remove ytpl [`9705f84`](https://github.com/th-ch/youtube-music/commit/9705f8489d7bf262bfd8b15ab84c2d3485f10eae)
|
||||
- chore(deps): Bump rollup, @xhayper/discord-rpc version [`00a3e8d`](https://github.com/th-ch/youtube-music/commit/00a3e8d35ec335e1913be19f30ae09dbe0b7acdd)
|
||||
- chore(deps): update dependency rollup to v4.1.4 [`6774d54`](https://github.com/th-ch/youtube-music/commit/6774d54f5eca432edc2e11743d9d1b1c2fda9ac8)
|
||||
|
||||
#### [v2.1.1](https://github.com/th-ch/youtube-music/compare/v2.1.0...v2.1.1)
|
||||
|
||||
> 14 October 2023
|
||||
|
||||
- hotfix(downloader): can't get an album title (fix #1313) [`#1313`](https://github.com/th-ch/youtube-music/issues/1313)
|
||||
- Update changelog for v2.1.0 [`92cab89`](https://github.com/th-ch/youtube-music/commit/92cab89d17175741e60e65ea61633e23ebdc1f45)
|
||||
- Bump version to 2.1.1 [`3bb5bc2`](https://github.com/th-ch/youtube-music/commit/3bb5bc2ca1856f4e222ee1e01e865f1ab804fdba)
|
||||
- Add "about" menu to show app version [`21c45fa`](https://github.com/th-ch/youtube-music/commit/21c45faf2043cf72a7c14d5cf6c8d848d0448528)
|
||||
|
||||
#### [v2.1.0](https://github.com/th-ch/youtube-music/compare/v2.0.4...v2.1.0)
|
||||
|
||||
> 14 October 2023
|
||||
|
||||
- feat(downloader): Added support for audio format auto-detection [`#1310`](https://github.com/th-ch/youtube-music/pull/1310)
|
||||
- feat(in-app-menu): enable in-app-menu by default (in Windows) [`#1311`](https://github.com/th-ch/youtube-music/pull/1311)
|
||||
- fix: winget publish [`#1307`](https://github.com/th-ch/youtube-music/pull/1307)
|
||||
- hotfix(downloader): fix invalid query selector (fix #1308) [`#1308`](https://github.com/th-ch/youtube-music/issues/1308)
|
||||
- chore(deps): bump dependencies [`3c6b3ae`](https://github.com/th-ch/youtube-music/commit/3c6b3aeff0aae32adb2f2ad9c091b0a9701d3c24)
|
||||
- chore(actions): create winget-cla.yml [`37181a7`](https://github.com/th-ch/youtube-music/commit/37181a7b5e2aa5bed6a36298eac3a66aac2762b8)
|
||||
- Update changelog for v2.0.4 [`e9398ad`](https://github.com/th-ch/youtube-music/commit/e9398adac34a8abb11801e32999a915a8be0ece6)
|
||||
|
||||
#### [v2.0.4](https://github.com/th-ch/youtube-music/compare/v2.0.3...v2.0.4)
|
||||
|
||||
> 12 October 2023
|
||||
|
||||
- hotfix(adblocker): fix `ipcRenderer.sendSync() with ...` [`#1301`](https://github.com/th-ch/youtube-music/pull/1301)
|
||||
- fix(downloader): Korean filename is broken on non-macOS devices [`#1297`](https://github.com/th-ch/youtube-music/pull/1297)
|
||||
- chore(deps): bump deps [`b6894dc`](https://github.com/th-ch/youtube-music/commit/b6894dca2974c63fa2945d3a4995665d11eb2a78)
|
||||
|
||||
@ -1,287 +0,0 @@
|
||||
import { blockers } from '../plugins/adblocker/blocker-types';
|
||||
|
||||
import { DefaultPresetList } from '../plugins/downloader/types';
|
||||
|
||||
export interface WindowSizeConfig {
|
||||
width: number;
|
||||
height: number;
|
||||
}
|
||||
|
||||
export interface DefaultConfig {
|
||||
'window-size': {
|
||||
width: number;
|
||||
height: number;
|
||||
}
|
||||
'window-maximized': boolean;
|
||||
'window-position': {
|
||||
x: number;
|
||||
y: number;
|
||||
}
|
||||
url: string;
|
||||
options: {
|
||||
tray: boolean;
|
||||
appVisible: boolean;
|
||||
autoUpdates: boolean;
|
||||
alwaysOnTop: boolean;
|
||||
hideMenu: boolean;
|
||||
hideMenuWarned: boolean;
|
||||
startAtLogin: boolean;
|
||||
disableHardwareAcceleration: boolean;
|
||||
removeUpgradeButton: boolean;
|
||||
restartOnConfigChanges: boolean;
|
||||
trayClickPlayPause: boolean;
|
||||
autoResetAppCache: boolean;
|
||||
resumeOnStart: boolean;
|
||||
likeButtons: string;
|
||||
proxy: string;
|
||||
startingPage: string;
|
||||
overrideUserAgent: boolean;
|
||||
themes: string[];
|
||||
}
|
||||
}
|
||||
|
||||
const defaultConfig = {
|
||||
'window-size': {
|
||||
width: 1100,
|
||||
height: 550,
|
||||
},
|
||||
'window-maximized': false,
|
||||
'window-position': {
|
||||
x: -1,
|
||||
y: -1,
|
||||
},
|
||||
'url': 'https://music.youtube.com',
|
||||
'options': {
|
||||
tray: false,
|
||||
appVisible: true,
|
||||
autoUpdates: true,
|
||||
alwaysOnTop: false,
|
||||
hideMenu: false,
|
||||
hideMenuWarned: false,
|
||||
startAtLogin: false,
|
||||
disableHardwareAcceleration: false,
|
||||
removeUpgradeButton: false,
|
||||
restartOnConfigChanges: false,
|
||||
trayClickPlayPause: false,
|
||||
autoResetAppCache: false,
|
||||
resumeOnStart: true,
|
||||
likeButtons: '',
|
||||
proxy: '',
|
||||
startingPage: '',
|
||||
overrideUserAgent: false,
|
||||
themes: [] as string[],
|
||||
},
|
||||
/** please order alphabetically */
|
||||
'plugins': {
|
||||
'adblocker': {
|
||||
enabled: true,
|
||||
cache: true,
|
||||
blocker: blockers.WithBlocklists as string,
|
||||
additionalBlockLists: [], // Additional list of filters, e.g "https://raw.githubusercontent.com/uBlockOrigin/uAssets/master/filters/filters.txt"
|
||||
disableDefaultLists: false,
|
||||
},
|
||||
'album-color-theme': {},
|
||||
'ambient-mode': {},
|
||||
'audio-compressor': {},
|
||||
'blur-nav-bar': {},
|
||||
'bypass-age-restrictions': {},
|
||||
'captions-selector': {
|
||||
enabled: false,
|
||||
disableCaptions: false,
|
||||
autoload: false,
|
||||
lastCaptionsCode: '',
|
||||
},
|
||||
'compact-sidebar': {},
|
||||
'crossfade': {
|
||||
enabled: false,
|
||||
fadeInDuration: 1500, // Ms
|
||||
fadeOutDuration: 5000, // Ms
|
||||
secondsBeforeEnd: 10, // S
|
||||
fadeScaling: 'linear', // 'linear', 'logarithmic' or a positive number in dB
|
||||
},
|
||||
'disable-autoplay': {
|
||||
applyOnce: false,
|
||||
},
|
||||
'discord': {
|
||||
enabled: false,
|
||||
autoReconnect: true, // If enabled, will try to reconnect to discord every 5 seconds after disconnecting or failing to connect
|
||||
activityTimoutEnabled: true, // If enabled, the discord rich presence gets cleared when music paused after the time specified below
|
||||
activityTimoutTime: 10 * 60 * 1000, // 10 minutes
|
||||
listenAlong: true, // Add a "listen along" button to rich presence
|
||||
hideGitHubButton: false, // Disable the "View App On GitHub" button
|
||||
hideDurationLeft: false, // Hides the start and end time of the song to rich presence
|
||||
},
|
||||
'downloader': {
|
||||
enabled: false,
|
||||
downloadFolder: undefined as string | undefined, // Custom download folder (absolute path)
|
||||
selectedPreset: 'mp3 (256kbps)', // Selected preset
|
||||
customPresetSetting: DefaultPresetList['mp3 (256kbps)'], // Presets
|
||||
skipExisting: false,
|
||||
playlistMaxItems: undefined as number | undefined,
|
||||
},
|
||||
'exponential-volume': {},
|
||||
'in-app-menu': {
|
||||
/**
|
||||
* true in Windows, false in Linux and macOS (see youtube-music/config/store.ts)
|
||||
*/
|
||||
enabled: false,
|
||||
},
|
||||
'last-fm': {
|
||||
enabled: false,
|
||||
token: undefined as string | undefined, // Token used for authentication
|
||||
session_key: undefined as string | undefined, // Session key used for scrobbling
|
||||
api_root: 'http://ws.audioscrobbler.com/2.0/',
|
||||
api_key: '04d76faaac8726e60988e14c105d421a', // Api key registered by @semvis123
|
||||
secret: 'a5d2a36fdf64819290f6982481eaffa2',
|
||||
},
|
||||
'lumiastream': {},
|
||||
'lyrics-genius': {
|
||||
romanizedLyrics: false,
|
||||
},
|
||||
'navigation': {
|
||||
enabled: true,
|
||||
},
|
||||
'no-google-login': {},
|
||||
'notifications': {
|
||||
enabled: false,
|
||||
unpauseNotification: false,
|
||||
urgency: 'normal', // Has effect only on Linux
|
||||
// the following has effect only on Windows
|
||||
interactive: true,
|
||||
toastStyle: 1, // See plugins/notifications/utils for more info
|
||||
refreshOnPlayPause: false,
|
||||
trayControls: true,
|
||||
hideButtonText: false,
|
||||
},
|
||||
'picture-in-picture': {
|
||||
'enabled': false,
|
||||
'alwaysOnTop': true,
|
||||
'savePosition': true,
|
||||
'saveSize': false,
|
||||
'hotkey': 'P',
|
||||
'pip-position': [10, 10],
|
||||
'pip-size': [450, 275],
|
||||
'isInPiP': false,
|
||||
'useNativePiP': false,
|
||||
},
|
||||
'playback-speed': {},
|
||||
'precise-volume': {
|
||||
enabled: false,
|
||||
steps: 1, // Percentage of volume to change
|
||||
arrowsShortcut: true, // Enable ArrowUp + ArrowDown local shortcuts
|
||||
globalShortcuts: {
|
||||
volumeUp: '',
|
||||
volumeDown: '',
|
||||
},
|
||||
savedVolume: undefined as number | undefined, // Plugin save volume between session here
|
||||
},
|
||||
'quality-changer': {},
|
||||
'shortcuts': {
|
||||
enabled: false,
|
||||
overrideMediaKeys: false,
|
||||
global: {
|
||||
previous: '',
|
||||
playPause: '',
|
||||
next: '',
|
||||
} as Record<string, string>,
|
||||
local: {
|
||||
previous: '',
|
||||
playPause: '',
|
||||
next: '',
|
||||
} as Record<string, string>,
|
||||
},
|
||||
'skip-silences': {
|
||||
onlySkipBeginning: false,
|
||||
},
|
||||
'sponsorblock': {
|
||||
enabled: false,
|
||||
apiURL: 'https://sponsor.ajay.app',
|
||||
categories: [
|
||||
'sponsor',
|
||||
'intro',
|
||||
'outro',
|
||||
'interaction',
|
||||
'selfpromo',
|
||||
'music_offtopic',
|
||||
],
|
||||
},
|
||||
'taskbar-mediacontrol': {},
|
||||
'touchbar': {},
|
||||
'tuna-obs': {},
|
||||
'video-toggle': {
|
||||
enabled: false,
|
||||
hideVideo: false,
|
||||
mode: 'custom',
|
||||
forceHide: false,
|
||||
align: '',
|
||||
},
|
||||
'visualizer': {
|
||||
enabled: false,
|
||||
type: 'butterchurn',
|
||||
// Config per visualizer
|
||||
butterchurn: {
|
||||
preset: 'martin [shadow harlequins shape code] - fata morgana',
|
||||
renderingFrequencyInMs: 500,
|
||||
blendTimeInSeconds: 2.7,
|
||||
},
|
||||
vudio: {
|
||||
effect: 'lighting',
|
||||
accuracy: 128,
|
||||
lighting: {
|
||||
maxHeight: 160,
|
||||
maxSize: 12,
|
||||
lineWidth: 1,
|
||||
color: '#49f3f7',
|
||||
shadowBlur: 2,
|
||||
shadowColor: 'rgba(244,244,244,.5)',
|
||||
fadeSide: true,
|
||||
prettify: false,
|
||||
horizontalAlign: 'center',
|
||||
verticalAlign: 'middle',
|
||||
dottify: true,
|
||||
},
|
||||
},
|
||||
wave: {
|
||||
animations: [
|
||||
{
|
||||
type: 'Cubes',
|
||||
config: {
|
||||
bottom: true,
|
||||
count: 30,
|
||||
cubeHeight: 5,
|
||||
fillColor: { gradient: ['#FAD961', '#F76B1C'] },
|
||||
lineColor: 'rgba(0,0,0,0)',
|
||||
radius: 20,
|
||||
},
|
||||
},
|
||||
{
|
||||
type: 'Cubes',
|
||||
config: {
|
||||
top: true,
|
||||
count: 12,
|
||||
cubeHeight: 5,
|
||||
fillColor: { gradient: ['#FAD961', '#F76B1C'] },
|
||||
lineColor: 'rgba(0,0,0,0)',
|
||||
radius: 10,
|
||||
},
|
||||
},
|
||||
{
|
||||
type: 'Circles',
|
||||
config: {
|
||||
lineColor: {
|
||||
gradient: ['#FAD961', '#FAD961', '#F76B1C'],
|
||||
rotate: 90,
|
||||
},
|
||||
lineWidth: 4,
|
||||
diameter: 20,
|
||||
count: 10,
|
||||
frequencyBand: 'base',
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
export default defaultConfig;
|
||||
@ -1,241 +0,0 @@
|
||||
/* eslint-disable @typescript-eslint/require-await */
|
||||
|
||||
import { ipcMain, ipcRenderer } from 'electron';
|
||||
|
||||
import defaultConfig from './defaults';
|
||||
|
||||
import { getOptions, setMenuOptions, setOptions } from './plugins';
|
||||
|
||||
|
||||
import { sendToFront } from '../providers/app-controls';
|
||||
import { Entries } from '../utils/type-utils';
|
||||
|
||||
export type DefaultPluginsConfig = typeof defaultConfig.plugins;
|
||||
export type OneOfDefaultConfigKey = keyof DefaultPluginsConfig;
|
||||
export type OneOfDefaultConfig = typeof defaultConfig.plugins[OneOfDefaultConfigKey];
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
const activePlugins: { [key in OneOfDefaultConfigKey]?: PluginConfig<any> } = {};
|
||||
|
||||
/**
|
||||
* [!IMPORTANT!]
|
||||
* The method is **sync** in the main process and **async** in the renderer process.
|
||||
*/
|
||||
export const getActivePlugins
|
||||
= process.type === 'renderer'
|
||||
? async () => ipcRenderer.invoke('get-active-plugins')
|
||||
: () => activePlugins;
|
||||
|
||||
if (process.type === 'browser') {
|
||||
ipcMain.handle('get-active-plugins', getActivePlugins);
|
||||
}
|
||||
|
||||
/**
|
||||
* [!IMPORTANT!]
|
||||
* The method is **sync** in the main process and **async** in the renderer process.
|
||||
*/
|
||||
export const isActive
|
||||
= process.type === 'renderer'
|
||||
? async (plugin: string) =>
|
||||
plugin in (await ipcRenderer.invoke('get-active-plugins'))
|
||||
: (plugin: string): boolean => plugin in activePlugins;
|
||||
|
||||
interface PluginConfigOptions {
|
||||
enableFront: boolean;
|
||||
initialOptions?: OneOfDefaultConfig;
|
||||
}
|
||||
|
||||
/**
|
||||
* This class is used to create a dynamic synced config for plugins.
|
||||
*
|
||||
* [!IMPORTANT!]
|
||||
* The methods are **sync** in the main process and **async** in the renderer process.
|
||||
*
|
||||
* @param {string} name - The name of the plugin.
|
||||
* @param {boolean} [options.enableFront] - Whether the config should be available in front.js. Default: false.
|
||||
* @param {object} [options.initialOptions] - The initial options for the plugin. Default: loaded from store.
|
||||
*
|
||||
* @example
|
||||
* const { PluginConfig } = require("../../config/dynamic");
|
||||
* const config = new PluginConfig("plugin-name", { enableFront: true });
|
||||
* module.exports = { ...config };
|
||||
*
|
||||
* // or
|
||||
*
|
||||
* module.exports = (win, options) => {
|
||||
* const config = new PluginConfig("plugin-name", {
|
||||
* enableFront: true,
|
||||
* initialOptions: options,
|
||||
* });
|
||||
* setupMyPlugin(win, config);
|
||||
* };
|
||||
*/
|
||||
export type ConfigType<T extends OneOfDefaultConfigKey> = typeof defaultConfig.plugins[T];
|
||||
type ValueOf<T> = T[keyof T];
|
||||
type Mode<T, Mode extends 'r' | 'm'> = Mode extends 'r' ? Promise<T> : T;
|
||||
export class PluginConfig<T extends OneOfDefaultConfigKey> {
|
||||
private readonly name: string;
|
||||
private readonly config: ConfigType<T>;
|
||||
private readonly defaultConfig: ConfigType<T>;
|
||||
private readonly enableFront: boolean;
|
||||
|
||||
private subscribers: { [key in keyof ConfigType<T>]?: (config: ConfigType<T>) => void } = {};
|
||||
private allSubscribers: ((config: ConfigType<T>) => void)[] = [];
|
||||
|
||||
constructor(
|
||||
name: T,
|
||||
options: PluginConfigOptions = {
|
||||
enableFront: false,
|
||||
},
|
||||
) {
|
||||
const pluginDefaultConfig = defaultConfig.plugins[name] ?? {};
|
||||
const pluginConfig = options.initialOptions || getOptions(name) || {};
|
||||
|
||||
this.name = name;
|
||||
this.enableFront = options.enableFront;
|
||||
this.defaultConfig = pluginDefaultConfig;
|
||||
this.config = { ...pluginDefaultConfig, ...pluginConfig };
|
||||
|
||||
if (this.enableFront) {
|
||||
this.setupFront();
|
||||
}
|
||||
|
||||
activePlugins[name] = this;
|
||||
}
|
||||
|
||||
get<Key extends keyof ConfigType<T> = keyof ConfigType<T>>(key: Key): ConfigType<T>[Key] {
|
||||
return this.config?.[key];
|
||||
}
|
||||
|
||||
set(key: keyof ConfigType<T>, value: ValueOf<ConfigType<T>>) {
|
||||
this.config[key] = value;
|
||||
this.onChange(key);
|
||||
this.save();
|
||||
}
|
||||
|
||||
getAll(): ConfigType<T> {
|
||||
return { ...this.config };
|
||||
}
|
||||
|
||||
setAll(options: Partial<ConfigType<T>>) {
|
||||
if (!options || typeof options !== 'object') {
|
||||
throw new Error('Options must be an object.');
|
||||
}
|
||||
|
||||
let changed = false;
|
||||
for (const [key, value] of Object.entries(options) as Entries<typeof options>) {
|
||||
if (this.config[key] !== value) {
|
||||
if (value !== undefined) this.config[key] = value;
|
||||
this.onChange(key, false);
|
||||
changed = true;
|
||||
}
|
||||
}
|
||||
|
||||
if (changed) {
|
||||
for (const fn of this.allSubscribers) {
|
||||
fn(this.config);
|
||||
}
|
||||
}
|
||||
|
||||
this.save();
|
||||
}
|
||||
|
||||
getDefaultConfig() {
|
||||
return this.defaultConfig;
|
||||
}
|
||||
|
||||
/**
|
||||
* Use this method to set an option and restart the app if `appConfig.restartOnConfigChange === true`
|
||||
*
|
||||
* Used for options that require a restart to take effect.
|
||||
*/
|
||||
setAndMaybeRestart(key: keyof ConfigType<T>, value: ValueOf<ConfigType<T>>) {
|
||||
this.config[key] = value;
|
||||
setMenuOptions(this.name, this.config);
|
||||
this.onChange(key);
|
||||
}
|
||||
|
||||
subscribe(valueName: keyof ConfigType<T>, fn: (config: ConfigType<T>) => void) {
|
||||
this.subscribers[valueName] = fn;
|
||||
}
|
||||
|
||||
subscribeAll(fn: (config: ConfigType<T>) => void) {
|
||||
this.allSubscribers.push(fn);
|
||||
}
|
||||
|
||||
/** Called only from back */
|
||||
private save() {
|
||||
setOptions(this.name, this.config);
|
||||
}
|
||||
|
||||
private onChange(valueName: keyof ConfigType<T>, single: boolean = true) {
|
||||
this.subscribers[valueName]?.(this.config[valueName] as ConfigType<T>);
|
||||
if (single) {
|
||||
for (const fn of this.allSubscribers) {
|
||||
fn(this.config);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private setupFront() {
|
||||
const ignoredMethods = ['subscribe', 'subscribeAll'];
|
||||
|
||||
if (process.type === 'renderer') {
|
||||
for (const [fnName, fn] of Object.entries(this) as Entries<this>) {
|
||||
if (typeof fn !== 'function' || fn.name in ignoredMethods) {
|
||||
return;
|
||||
}
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any,@typescript-eslint/no-unsafe-return
|
||||
this[fnName] = (async (...args: any) => await ipcRenderer.invoke(
|
||||
`${this.name}-config-${String(fnName)}`,
|
||||
// eslint-disable-next-line @typescript-eslint/no-unsafe-argument
|
||||
...args,
|
||||
)) as typeof this[keyof this];
|
||||
|
||||
this.subscribe = (valueName, fn: (config: ConfigType<T>) => void) => {
|
||||
if (valueName in this.subscribers) {
|
||||
console.error(`Already subscribed to ${String(valueName)}`);
|
||||
}
|
||||
|
||||
this.subscribers[valueName] = fn;
|
||||
ipcRenderer.on(
|
||||
`${this.name}-config-changed-${String(valueName)}`,
|
||||
(_, value: ConfigType<T>) => {
|
||||
fn(value);
|
||||
},
|
||||
);
|
||||
ipcRenderer.send(`${this.name}-config-subscribe`, valueName);
|
||||
};
|
||||
|
||||
this.subscribeAll = (fn: (config: ConfigType<T>) => void) => {
|
||||
ipcRenderer.on(`${this.name}-config-changed`, (_, value: ConfigType<T>) => {
|
||||
fn(value);
|
||||
});
|
||||
ipcRenderer.send(`${this.name}-config-subscribe-all`);
|
||||
};
|
||||
}
|
||||
} else if (process.type === 'browser') {
|
||||
for (const [fnName, fn] of Object.entries(this) as Entries<this>) {
|
||||
if (typeof fn !== 'function' || fn.name in ignoredMethods) {
|
||||
return;
|
||||
}
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/no-unsafe-argument,@typescript-eslint/no-unsafe-return
|
||||
ipcMain.handle(`${this.name}-config-${String(fnName)}`, (_, ...args) => fn(...args));
|
||||
}
|
||||
|
||||
ipcMain.on(`${this.name}-config-subscribe`, (_, valueName: keyof ConfigType<T>) => {
|
||||
this.subscribe(valueName, (value) => {
|
||||
sendToFront(`${this.name}-config-changed-${String(valueName)}`, value);
|
||||
});
|
||||
});
|
||||
|
||||
ipcMain.on(`${this.name}-config-subscribe-all`, () => {
|
||||
this.subscribeAll((value) => {
|
||||
sendToFront(`${this.name}-config-changed`, value);
|
||||
});
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -1,53 +0,0 @@
|
||||
import Store from 'electron-store';
|
||||
|
||||
import defaultConfig from './defaults';
|
||||
import plugins from './plugins';
|
||||
import store from './store';
|
||||
|
||||
import { restart } from '../providers/app-controls';
|
||||
|
||||
|
||||
const set = (key: string, value: unknown) => {
|
||||
store.set(key, value);
|
||||
};
|
||||
|
||||
function setMenuOption(key: string, value: unknown) {
|
||||
set(key, value);
|
||||
if (store.get('options.restartOnConfigChanges')) {
|
||||
restart();
|
||||
}
|
||||
}
|
||||
|
||||
// MAGIC OF TYPESCRIPT
|
||||
|
||||
type Prev = [never, 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10,
|
||||
11, 12, 13, 14, 15, 16, 17, 18, 19, 20, ...0[]]
|
||||
type Join<K, P> = K extends string | number ?
|
||||
P extends string | number ?
|
||||
`${K}${'' extends P ? '' : '.'}${P}`
|
||||
: never : never;
|
||||
type Paths<T, D extends number = 10> = [D] extends [never] ? never : T extends object ?
|
||||
{ [K in keyof T]-?: K extends string | number ?
|
||||
`${K}` | Join<K, Paths<T[K], Prev[D]>>
|
||||
: never
|
||||
}[keyof T] : ''
|
||||
|
||||
type SplitKey<K> = K extends `${infer A}.${infer B}` ? [A, B] : [K, string];
|
||||
type PathValue<T, K extends string> =
|
||||
SplitKey<K> extends [infer A extends keyof T, infer B extends string]
|
||||
? PathValue<T[A], B>
|
||||
: T;
|
||||
const get = <Key extends Paths<typeof defaultConfig>>(key: Key) => store.get(key) as PathValue<typeof defaultConfig, typeof key>;
|
||||
|
||||
export default {
|
||||
defaultConfig,
|
||||
get,
|
||||
set,
|
||||
setMenuOption,
|
||||
edit: () => store.openInEditor(),
|
||||
watch(cb: Parameters<Store['onDidChange']>[1]) {
|
||||
store.onDidChange('options', cb);
|
||||
store.onDidChange('plugins', cb);
|
||||
},
|
||||
plugins,
|
||||
};
|
||||
@ -1,63 +0,0 @@
|
||||
import store from './store';
|
||||
import defaultConfig from './defaults';
|
||||
|
||||
import { restart } from '../providers/app-controls';
|
||||
import { Entries } from '../utils/type-utils';
|
||||
|
||||
interface Plugin {
|
||||
enabled: boolean;
|
||||
}
|
||||
|
||||
type DefaultPluginsConfig = typeof defaultConfig.plugins;
|
||||
|
||||
export function getEnabled() {
|
||||
const plugins = store.get('plugins') as DefaultPluginsConfig;
|
||||
return (Object.entries(plugins) as Entries<DefaultPluginsConfig>).filter(([plugin]) =>
|
||||
isEnabled(plugin),
|
||||
);
|
||||
}
|
||||
|
||||
export function isEnabled(plugin: string) {
|
||||
const pluginConfig = (store.get('plugins') as Record<string, Plugin>)[plugin];
|
||||
return pluginConfig !== undefined && pluginConfig.enabled;
|
||||
}
|
||||
|
||||
export function setOptions<T>(plugin: string, options: T) {
|
||||
const plugins = store.get('plugins') as Record<string, T>;
|
||||
store.set('plugins', {
|
||||
...plugins,
|
||||
[plugin]: {
|
||||
...plugins[plugin],
|
||||
...options,
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
export function setMenuOptions<T>(plugin: string, options: T) {
|
||||
setOptions(plugin, options);
|
||||
if (store.get('options.restartOnConfigChanges')) {
|
||||
restart();
|
||||
}
|
||||
}
|
||||
|
||||
export function getOptions<T>(plugin: string): T {
|
||||
return (store.get('plugins') as Record<string, T>)[plugin];
|
||||
}
|
||||
|
||||
export function enable(plugin: string) {
|
||||
setMenuOptions(plugin, { enabled: true });
|
||||
}
|
||||
|
||||
export function disable(plugin: string) {
|
||||
setMenuOptions(plugin, { enabled: false });
|
||||
}
|
||||
|
||||
export default {
|
||||
isEnabled,
|
||||
getEnabled,
|
||||
enable,
|
||||
disable,
|
||||
setOptions,
|
||||
setMenuOptions,
|
||||
getOptions,
|
||||
};
|
||||
154
config/store.ts
154
config/store.ts
@ -1,154 +0,0 @@
|
||||
import Store from 'electron-store';
|
||||
import Conf from 'conf';
|
||||
import is from 'electron-is';
|
||||
|
||||
import defaults from './defaults';
|
||||
|
||||
import { DefaultPresetList, type Preset } from '../plugins/downloader/types';
|
||||
|
||||
const getDefaults = () => {
|
||||
if (is.windows()) {
|
||||
defaults.plugins['in-app-menu'].enabled = true;
|
||||
}
|
||||
return defaults;
|
||||
};
|
||||
|
||||
const setDefaultPluginOptions = (store: Conf<Record<string, unknown>>, plugin: keyof typeof defaults.plugins) => {
|
||||
if (!store.get(`plugins.${plugin}`)) {
|
||||
store.set(`plugins.${plugin}`, defaults.plugins[plugin]);
|
||||
}
|
||||
};
|
||||
|
||||
const migrations = {
|
||||
'>=2.1.0'(store: Conf<Record<string, unknown>>) {
|
||||
const originalPreset = store.get('plugins.downloader.preset') as string | undefined;
|
||||
if (originalPreset) {
|
||||
if (originalPreset !== 'opus') {
|
||||
store.set('plugins.downloader.selectedPreset', 'Custom');
|
||||
store.set('plugins.downloader.customPresetSetting', {
|
||||
extension: 'mp3',
|
||||
ffmpegArgs: store.get('plugins.downloader.ffmpegArgs') as string[] ?? DefaultPresetList['mp3 (256kbps)'].ffmpegArgs,
|
||||
} satisfies Preset);
|
||||
} else {
|
||||
store.set('plugins.downloader.selectedPreset', 'Source');
|
||||
store.set('plugins.downloader.customPresetSetting', {
|
||||
extension: null,
|
||||
ffmpegArgs: store.get('plugins.downloader.ffmpegArgs') as string[] ?? [],
|
||||
} satisfies Preset);
|
||||
}
|
||||
store.delete('plugins.downloader.preset');
|
||||
store.delete('plugins.downloader.ffmpegArgs');
|
||||
}
|
||||
},
|
||||
'>=1.20.0'(store: Conf<Record<string, unknown>>) {
|
||||
setDefaultPluginOptions(store, 'visualizer');
|
||||
|
||||
if (store.get('plugins.notifications.toastStyle') === undefined) {
|
||||
const pluginOptions = store.get('plugins.notifications') || {};
|
||||
store.set('plugins.notifications', {
|
||||
...defaults.plugins.notifications,
|
||||
...pluginOptions,
|
||||
});
|
||||
}
|
||||
|
||||
if (store.get('options.ForceShowLikeButtons')) {
|
||||
store.delete('options.ForceShowLikeButtons');
|
||||
store.set('options.likeButtons', 'force');
|
||||
}
|
||||
},
|
||||
'>=1.17.0'(store: Conf<Record<string, unknown>>) {
|
||||
setDefaultPluginOptions(store, 'picture-in-picture');
|
||||
|
||||
if (store.get('plugins.video-toggle.mode') === undefined) {
|
||||
store.set('plugins.video-toggle.mode', 'custom');
|
||||
}
|
||||
},
|
||||
'>=1.14.0'(store: Conf<Record<string, unknown>>) {
|
||||
if (
|
||||
typeof store.get('plugins.precise-volume.globalShortcuts') !== 'object'
|
||||
) {
|
||||
store.set('plugins.precise-volume.globalShortcuts', {});
|
||||
}
|
||||
|
||||
if (store.get('plugins.hide-video-player.enabled')) {
|
||||
store.delete('plugins.hide-video-player');
|
||||
store.set('plugins.video-toggle.enabled', true);
|
||||
}
|
||||
},
|
||||
'>=1.13.0'(store: Conf<Record<string, unknown>>) {
|
||||
if (store.get('plugins.discord.listenAlong') === undefined) {
|
||||
store.set('plugins.discord.listenAlong', true);
|
||||
}
|
||||
},
|
||||
'>=1.12.0'(store: Conf<Record<string, unknown>>) {
|
||||
const options = store.get('plugins.shortcuts') as Record<string, {
|
||||
action: string;
|
||||
shortcut: unknown;
|
||||
}[] | Record<string, unknown>>;
|
||||
let updated = false;
|
||||
for (const optionType of ['global', 'local']) {
|
||||
if (Array.isArray(options[optionType])) {
|
||||
const optionsArray = options[optionType] as {
|
||||
action: string;
|
||||
shortcut: unknown;
|
||||
}[];
|
||||
const updatedOptions: Record<string, unknown> = {};
|
||||
for (const optionObject of optionsArray) {
|
||||
if (optionObject.action && optionObject.shortcut) {
|
||||
updatedOptions[optionObject.action] = optionObject.shortcut;
|
||||
}
|
||||
}
|
||||
|
||||
options[optionType] = updatedOptions;
|
||||
updated = true;
|
||||
}
|
||||
}
|
||||
|
||||
if (updated) {
|
||||
store.set('plugins.shortcuts', options);
|
||||
}
|
||||
},
|
||||
'>=1.11.0'(store: Conf<Record<string, unknown>>) {
|
||||
if (store.get('options.resumeOnStart') === undefined) {
|
||||
store.set('options.resumeOnStart', true);
|
||||
}
|
||||
},
|
||||
'>=1.7.0'(store: Conf<Record<string, unknown>>) {
|
||||
const enabledPlugins = store.get('plugins') as string[];
|
||||
if (!Array.isArray(enabledPlugins)) {
|
||||
console.warn('Plugins are not in array format, cannot migrate');
|
||||
return;
|
||||
}
|
||||
|
||||
// Include custom options
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
const plugins: Record<string, any> = {
|
||||
adblocker: {
|
||||
enabled: true,
|
||||
cache: true,
|
||||
additionalBlockLists: [],
|
||||
},
|
||||
downloader: {
|
||||
enabled: false,
|
||||
ffmpegArgs: [], // E.g. ["-b:a", "192k"] for an audio bitrate of 192kb/s
|
||||
downloadFolder: undefined, // Custom download folder (absolute path)
|
||||
},
|
||||
};
|
||||
|
||||
for (const enabledPlugin of enabledPlugins) {
|
||||
// eslint-disable-next-line @typescript-eslint/no-unsafe-assignment
|
||||
plugins[enabledPlugin] = {
|
||||
...plugins[enabledPlugin],
|
||||
enabled: true,
|
||||
};
|
||||
}
|
||||
|
||||
store.set('plugins', plugins);
|
||||
},
|
||||
};
|
||||
|
||||
export default new Store({
|
||||
defaults: getDefaults(),
|
||||
clearInvalidConfig: false,
|
||||
migrations,
|
||||
});
|
||||
@ -478,7 +478,7 @@
|
||||
</a>
|
||||
</li>
|
||||
</ul>
|
||||
<div class="footer-copyright">© 2021 th-ch</div>
|
||||
<div class="footer-copyright">© 2024 th-ch</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
390
docs/readme/README-es.md
Normal file
390
docs/readme/README-es.md
Normal file
@ -0,0 +1,390 @@
|
||||
<div align="center">
|
||||
|
||||
# YouTube Music
|
||||
|
||||
[](https://github.com/th-ch/youtube-music/releases/)
|
||||
[](https://github.com/th-ch/youtube-music/blob/master/LICENSE)
|
||||
[](https://github.com/th-ch/youtube-music/blob/master/.eslintrc.js)
|
||||
[](https://GitHub.com/th-ch/youtube-music/releases/)
|
||||
[](https://GitHub.com/th-ch/youtube-music/releases/)
|
||||
[](https://aur.archlinux.org/packages/youtube-music-bin)
|
||||
[](https://snyk.io/test/github/th-ch/youtube-music)
|
||||
|
||||
</div>
|
||||
|
||||

|
||||
|
||||
|
||||
<div align="center">
|
||||
<a href="https://github.com/th-ch/youtube-music/releases/latest">
|
||||
<img src="/web/youtube-music.svg" width="400" height="100" alt="YouTube Music SVG">
|
||||
</a>
|
||||
</div>
|
||||
|
||||
**Electron wrapper de YouTube Music con las siguientes características:**
|
||||
|
||||
- Apariencia y sensación nativa, tiene como objetivo mantener la interfaz original
|
||||
- Framework para plugins personalizados: cambia YouTube Music según tus necesidades (estilo, contenido, funciones), habilita/deshabilita plugins con un solo clic
|
||||
|
||||
## Imagen de demostración
|
||||
|
||||
| Pantalla del reproductor (color del álbum como tema y luz ambiental) |
|
||||
|:---------------------------------------------------------------------------------------------------------:|
|
||||
||
|
||||
|
||||
## Contenido
|
||||
|
||||
- [Características](#características)
|
||||
- [Plugins disponibles](#plugins-disponibles)
|
||||
- [Traducción](#traducción)
|
||||
- [Descarga](#descarga)
|
||||
- [Arch Linux](#arch-linux)
|
||||
- [macOS](#macos)
|
||||
- [Windows](#windows)
|
||||
- [Cómo instalar sin conexión a internet? (en Windows)](#cómo-instalar-sin-conexión-a-internet-en-windows)
|
||||
- [Temas](#temas)
|
||||
- [Dev](#dev)
|
||||
- [Crea tus propios plugins](#crea-tus-propios-plugins)
|
||||
- [Creación de un plugin](#creación-de-un-plugin)
|
||||
- [Casos de uso comunes](#casos-de-uso-comunes)
|
||||
- [Compilar](#compilar)
|
||||
- [Vista previa de producción](#vista-previa-de-producción)
|
||||
- [Tests](#tests)
|
||||
- [Licencia](#licencia)
|
||||
- [Preguntas frecuentes](#preguntas-frecuentes)
|
||||
|
||||
## Características:
|
||||
|
||||
- **Confirmación automática al pausar** (Siempre habilitado): desactiva
|
||||
el mensaje emergente ["¿Continuar reproduciendo?"](https://user-images.githubusercontent.com/61631665/129977894-01c60740-7ec6-4bf0-9a2c-25da24491b0e.png)
|
||||
que pausa la música después de cierto tiempo
|
||||
|
||||
- Y más ...
|
||||
|
||||
## Plugins disponibles:
|
||||
|
||||
- **Bloqueador de Anuncios**: Bloquea todos los anuncios y rastreadores de forma predeterminada
|
||||
|
||||
- **Acciones de Álbum**: Agrega botones de deshacer No me gusta, No me gusta, Me gusta, y Deshacer me gusta a todas las canciones de una lista de reproducción o álbum
|
||||
|
||||
- **Tema de Color del Álbum**: Aplica un tema dinámico y efectos visuales basados en la paleta de colores del álbum
|
||||
|
||||
- **Modo Ambiente**: Aplica un efecto de iluminación proyectando colores suaves del video en el fondo de tu pantalla
|
||||
|
||||
- **Compresor de Audio**: Aplica compresión al audio (reduce el volumen de las partes más fuertes de la señal y aumenta el
|
||||
volumen de las partes más suaves)
|
||||
|
||||
- **Barra de Navegación Difuminada**: hace que la barra de navegación sea transparente y borrosa
|
||||
|
||||
- **Omitir Restricciones de Edades**: omite la verificación de edad de YouTube
|
||||
|
||||
- **Selector de Subtítulos**: Habilita los subtítulos
|
||||
|
||||
- **Barra Lateral Compacta**: Siempre muestra la barra lateral en modo compacto
|
||||
|
||||
- **Crossfade**: Transición suave entre canciones
|
||||
|
||||
- **Desactivar Reproducción Automática**: Hace que cada canción comience en modo "pausado"
|
||||
|
||||
- **[Discord](https://discord.com/) Rich Presence**: Muestra a tus amigos lo que estás escuchando
|
||||
con [Rich Presence](https://user-images.githubusercontent.com/28219076/104362104-a7a0b980-5513-11eb-9744-bb89eabe0016.png)
|
||||
|
||||
- **Descargador**: Descarga
|
||||
MP3 [directamente desde la interfaz](https://user-images.githubusercontent.com/61631665/129977677-83a7d067-c192-45e1-98ae-b5a4927393be.png) [(youtube-dl)](https://github.com/ytdl-org/youtube-dl)
|
||||
|
||||
- **Volumen Exponencial**: Hace que el control de volumen
|
||||
sea [exponencial](https://greasyfork.org/en/scripts/397686-youtube-music-fix-volume-ratio/) para facilitar la
|
||||
selección de volúmenes más bajos
|
||||
|
||||
- **Menú en la Aplicación**: [da a las barras un aspecto elegante y oscuro](https://user-images.githubusercontent.com/78568641/112215894-923dbf00-8c29-11eb-95c3-3ce15db27eca.png)
|
||||
|
||||
> (consulta [esta publicación](https://github.com/th-ch/youtube-music/issues/410#issuecomment-952060709) si tienes problemas
|
||||
para acceder al menú después de habilitar este plugin y la opción hide-menu)
|
||||
|
||||
- **Scrobbler**: Agrega soporte para scrobbling en [Last.fm](https://www.last.fm/) y [ListenBrainz](https://listenbrainz.org/)
|
||||
|
||||
- **Lumia Stream**: Agrega soporte para [Lumia Stream](https://lumiastream.com/)
|
||||
|
||||
- **Letras Genius**: Agrega soporte de letras para la mayoría de las canciones
|
||||
|
||||
- **Music Together**: Comparte una lista de reproducción con otros. Cuando el anfitrión reproduce una canción, todos los demás escucharán la misma canción
|
||||
|
||||
- **Navegación**: Flechas de siguiente/anterior integradas directamente en la interfaz, como en tu navegador favorito
|
||||
|
||||
- **Sin Inicio de Sesión de Google**: Elimina los botones y enlaces de inicio de sesión de Google de la interfaz
|
||||
|
||||
- **Notificaciones**: Muestra una notificación cuando comienza una canción
|
||||
a reproducirse ([notificaciones interactivas](https://user-images.githubusercontent.com/78568641/114102651-63ce0e00-98d0-11eb-9dfe-c5a02bb54f9c.png)
|
||||
están disponibles en Windows)
|
||||
|
||||
- **Picture-in-picture**: permite cambiar la aplicación al modo picture-in-picture
|
||||
|
||||
- **Velocidad de Reproducción**: Escucha rápido, escucha
|
||||
lento! [Agrega un deslizador que controla la velocidad de reproducción de las canciones](https://user-images.githubusercontent.com/61631665/129976003-e55db5ba-bf42-448c-a059-26a009775e68.png)
|
||||
|
||||
- **Volumen Preciso**: Controla el volumen de forma precisa utilizando la rueda del mouse/atajos de teclado, con un HUD personalizado y pasos de volumen personalizables
|
||||
|
||||
- **Atajos (& MPRIS)**: Permite configurar atajos globales para la reproducción (reproducir/pausar/siguiente/anterior) +
|
||||
desactivar [osd multimedia](https://user-images.githubusercontent.com/84923831/128601225-afa38c1f-dea8-4209-9f72-0f84c1dd8b54.png)
|
||||
al anular las teclas multimedia + habilitar Ctrl/CMD + F para buscar + habilitar el soporte mpris de Linux para
|
||||
teclas multimedia + [atajos personalizados](https://github.com/Araxeus/youtube-music/blob/1e591d6a3df98449bcda6e63baab249b28026148/providers/song-controls.js#L13-L50)
|
||||
para [usuarios avanzados](https://github.com/th-ch/youtube-music/issues/106#issuecomment-952156902)
|
||||
|
||||
- **Saltar Canción no Gustada**: Salta las canciones que no te gustan
|
||||
|
||||
- **Saltar Silencios**: Salta automáticamente las secciones de silencio
|
||||
|
||||
- [**SponsorBlock**](https://github.com/ajayyy/SponsorBlock): Salta automáticamente las partes que no son de música, como la introducción/final o
|
||||
partes de videos musicales donde no se reproduce la canción
|
||||
|
||||
- **Control Multimedia en la Barra de Tareas**: Controla la reproducción desde
|
||||
la [barra de tareas de Windows](https://user-images.githubusercontent.com/78568641/111916130-24a35e80-8a82-11eb-80c8-5021c1aa27f4.png)
|
||||
|
||||
- **TouchBar**: Diseño personalizado de TouchBar para macOS
|
||||
|
||||
- **Tuna OBS**: Integración con el complemento [Tuna](https://obsproject.com/forum/resources/tuna.843/) de [OBS](https://obsproject.com/)
|
||||
|
||||
- **Cambiador de Calidad de Video**: Permite cambiar la calidad del video con
|
||||
un [botón](https://user-images.githubusercontent.com/78568641/138574366-70324a5e-2d64-4f6a-acdd-dc2a2b9cecc5.png) en
|
||||
la superposición de video
|
||||
|
||||
- **Alternar Video**: Agrega
|
||||
un [botón](https://user-images.githubusercontent.com/28893833/173663950-63e6610e-a532-49b7-9afa-54cb57ddfc15.png) para
|
||||
alternar entre el modo de video/canción. también puede eliminar opcionalmente toda la pestaña de video
|
||||
|
||||
- **Visualizador**: Diferentes visualizadores de música
|
||||
|
||||
## Traducción
|
||||
|
||||
Puedes ayudar con la traducción en [Hosted Weblate](https://hosted.weblate.org/projects/youtube-music/).
|
||||
|
||||
<a href="https://hosted.weblate.org/engage/youtube-music/">
|
||||
<img src="https://hosted.weblate.org/widget/youtube-music/i18n/multi-auto.svg" alt="estado de traducción" />
|
||||
<img src="https://hosted.weblate.org/widget/youtube-music/i18n/287x66-black.png" alt="estado de traducción 2" />
|
||||
</a>
|
||||
|
||||
## Descarga
|
||||
|
||||
Puedes consultar la [última versión](https://github.com/th-ch/youtube-music/releases/latest) para encontrar rápidamente la versión más reciente.
|
||||
|
||||
### Arch Linux
|
||||
|
||||
Instala el paquete [`youtube-music-bin`](https://aur.archlinux.org/packages/youtube-music-bin) desde AUR. Para obtener instrucciones de instalación de AUR, consulta esta [página del wiki](https://wiki.archlinux.org/index.php/Arch_User_Repository#Installing_packages).
|
||||
|
||||
### macOS
|
||||
|
||||
Puedes instalar la aplicación usando Homebrew (consulta la [definición de cask](https://github.com/th-ch/homebrew-youtube-music)):
|
||||
|
||||
```bash
|
||||
brew install th-ch/youtube-music/youtube-music
|
||||
```
|
||||
|
||||
Si instalas la aplicación manualmente y obtienes un error "está dañado y no se puede abrir" al iniciar la aplicación, ejecuta lo siguiente en la Terminal:
|
||||
|
||||
```bash
|
||||
xattr -cr /Applications/YouTube\ Music.app
|
||||
```
|
||||
|
||||
### Windows
|
||||
|
||||
Puedes usar el [administrador de paquetes Scoop](https://scoop.sh) para instalar el paquete `youtube-music` desde
|
||||
el [`extras` bucket](https://github.com/ScoopInstaller/Extras).
|
||||
|
||||
```bash
|
||||
scoop bucket add extras
|
||||
scoop install extras/youtube-music
|
||||
```
|
||||
|
||||
Alternativamente, puedes usar [Winget](https://learn.microsoft.com/en-us/windows/package-manager/winget/), el administrador de paquetes CLI oficial de Windows 11 para instalar el paquete `th-ch.YouTubeMusic`.
|
||||
|
||||
*Nota: Microsoft Defender SmartScreen podría bloquear la instalación ya que proviene de un "editor desconocido". Esto también esválido para la instalación manual al intentar ejecutar el ejecutable (.exe) después de una descarga manual aquí en GitHub (mismo archivo).*
|
||||
|
||||
```bash
|
||||
winget install th-ch.YouTubeMusic
|
||||
```
|
||||
|
||||
#### Cómo instalar sin conexión a Internet? (en Windows)
|
||||
|
||||
- Descarga el archivo `*.nsis.7z` para _la arquitectura de tu dispositivo_ en la [página de lanzamientos](https://github.com/th-ch/youtube-music/releases/latest).
|
||||
- `x64` para Windows de 64 bits
|
||||
- `ia32` para Windows de 32 bits
|
||||
- `arm64` para Windows ARM64
|
||||
- Descarga el instalador en la página de lanzamientos. (`*-Setup.exe`)
|
||||
- Colócalos en el **mismo directorio**.
|
||||
- Ejecuta el instalador.
|
||||
|
||||
## Temas
|
||||
|
||||
Puedes cargar archivos CSS para cambiar la apariencia de la aplicación (Opciones > Ajustes visuales > Tema).
|
||||
|
||||
Algunos temas predefinidos están disponibles en https://github.com/kerichdev/themes-for-ytmdesktop-player.
|
||||
|
||||
## Dev
|
||||
|
||||
```bash
|
||||
git clone https://github.com/th-ch/youtube-music
|
||||
cd youtube-music
|
||||
pnpm install --frozen-lockfile
|
||||
pnpm dev
|
||||
```
|
||||
|
||||
## Crea tus propios plugins
|
||||
|
||||
Usando plugins, puedes:
|
||||
|
||||
- manipular la aplicación - se pasa el `BrowserWindow` de electron al controlador del plugin
|
||||
- cambiar la interfaz manipulando el HTML/CSS
|
||||
|
||||
### Creación de un plugin
|
||||
|
||||
Crea una carpeta en `src/plugins/NOMBRE-DEL-PLUGIN`:
|
||||
|
||||
- `index.ts`: el archivo principal del plugin
|
||||
```typescript
|
||||
import style from './style.css?inline'; // importar estilo como inline
|
||||
|
||||
import { createPlugin } from '@/utils';
|
||||
|
||||
export default createPlugin({
|
||||
name: "Plugin Label",
|
||||
restartNeeded: true, // si el valor es true, ytmusic muestra el diálogo de reinicio
|
||||
config: {
|
||||
enabled: false,
|
||||
}, // tu configuración personalizada
|
||||
stylesheets: [style], // tu estilo personalizado,
|
||||
menu: async ({ getConfig, setConfig }) => {
|
||||
// Todos los métodos *Config están envueltos en Promise<T>
|
||||
const config = await getConfig();
|
||||
return [
|
||||
{
|
||||
label: "menu",
|
||||
submenu: [1, 2, 3].map((value) => ({
|
||||
label: `value ${value}`,
|
||||
type: "radio",
|
||||
checked: config.value === value,
|
||||
click() {
|
||||
setConfig({ value });
|
||||
},
|
||||
})),
|
||||
},
|
||||
];
|
||||
},
|
||||
backend: {
|
||||
start({ window, ipc }) {
|
||||
window.maximize();
|
||||
|
||||
// puedes comunicarte con el plugin de renderizado
|
||||
ipc.handle("some-event", () => {
|
||||
return "hello";
|
||||
});
|
||||
},
|
||||
// se activa cuando cambia la configuración
|
||||
onConfigChange(newConfig) { /* ... */ },
|
||||
// se activa cuando se desactiva el plugin
|
||||
stop(context) { /* ... */ },
|
||||
},
|
||||
renderer: {
|
||||
async start(context) {
|
||||
console.log(await context.ipc.invoke("some-event"));
|
||||
},
|
||||
// Solo disponible en el plugin de renderizado
|
||||
onPlayerApiReady(api: YoutubePlayer, context: RendererContext) {
|
||||
// establecer la configuración del plugin fácilmente
|
||||
context.setConfig({ myConfig: api.getVolume() });
|
||||
},
|
||||
onConfigChange(newConfig) { /* ... */ },
|
||||
stop(_context) { /* ... */ },
|
||||
},
|
||||
preload: {
|
||||
async start({ getConfig }) {
|
||||
const config = await getConfig();
|
||||
},
|
||||
onConfigChange(newConfig) {},
|
||||
stop(_context) {},
|
||||
},
|
||||
});
|
||||
|
||||
```
|
||||
|
||||
### Casos de uso comunes
|
||||
|
||||
- inyectar CSS personalizado: crea un archivo `style.css` en la misma carpeta y luego:
|
||||
|
||||
```typescript
|
||||
// index.ts
|
||||
import style from './style.css?inline'; // importar estilo como inline
|
||||
|
||||
import { createPlugin } from '@/utils';
|
||||
|
||||
export default createPlugin({
|
||||
name: 'Plugin Label',
|
||||
restartNeeded: true, // si el valor es true, ytmusic mostrará el diálogo de reinicio
|
||||
config: {
|
||||
enabled: false,
|
||||
}, // tu configuración personalizada
|
||||
stylesheets: [style], // tu estilo personalizado
|
||||
renderer() {} // define el hook del renderizador
|
||||
});
|
||||
```
|
||||
|
||||
- Si quieres cambiar el HTML:
|
||||
|
||||
```typescript
|
||||
import { createPlugin } from '@/utils';
|
||||
|
||||
export default createPlugin({
|
||||
name: 'Plugin Label',
|
||||
restartNeeded: true, // si el valor es true, ytmusic mostrará el diálogo de reinicio
|
||||
config: {
|
||||
enabled: false,
|
||||
}, // tu configuración personalizada
|
||||
renderer() {
|
||||
// Elimina el botón de inicio de sesión
|
||||
document.querySelector(".sign-in-link.ytmusic-nav-bar").remove();
|
||||
} // define el hook del renderizador
|
||||
});
|
||||
```
|
||||
|
||||
- comunicación entre el front y el back: se puede hacer utilizando el módulo ipcMain de electron. Ver archivo `index.ts` y
|
||||
ejemplo en el plugin `sponsorblock`.
|
||||
|
||||
## Compilar
|
||||
|
||||
1. Clonar el repositorio
|
||||
2. Seguir [esta guía](https://pnpm.io/es/installation) para instalar `pnpm`
|
||||
3. Ejecutar `pnpm install --frozen-lockfile` para instalar las dependencias
|
||||
4. Ejecutar `pnpm build:OS`
|
||||
|
||||
- `pnpm dist:win` - Windows
|
||||
- `pnpm dist:linux` - Linux (amd64)
|
||||
- `pnpm dist:linux:deb-arm64` - Linux (arm64 para Debian)
|
||||
- `pnpm dist:linux:rpm-arm64` - Linux (arm64 para Fedora)
|
||||
- `pnpm dist:mac` - macOS (amd64)
|
||||
- `pnpm dist:mac:arm64` - macOS (arm64)
|
||||
|
||||
Construye la aplicación para macOS, Linux y Windows,
|
||||
utilizando [electron-builder](https://github.com/electron-userland/electron-builder).
|
||||
|
||||
## Vista previa de producción
|
||||
|
||||
```bash
|
||||
pnpm start
|
||||
```
|
||||
|
||||
## Tests
|
||||
|
||||
```bash
|
||||
pnpm test
|
||||
```
|
||||
|
||||
Utiliza [Playwright](https://playwright.dev/) para probar la aplicación.
|
||||
|
||||
## Licencia
|
||||
|
||||
MIT © [th-ch](https://github.com/th-ch/youtube-music)
|
||||
|
||||
## Preguntas frecuentes
|
||||
|
||||
### ¿Por qué no se muestra el menú de aplicaciones?
|
||||
|
||||
Si la opción `Ocultar menú` está activada - puedes mostrar el menú con la tecla <kbd>alt</kbd> (o <kbd>\`</kbd> [acento grave] si estás utilizando el plugin in-app-menu)
|
||||
388
docs/readme/README-fr.md
Normal file
388
docs/readme/README-fr.md
Normal file
@ -0,0 +1,388 @@
|
||||
<div align="center">
|
||||
|
||||
# YouTube Music
|
||||
|
||||
[](https://github.com/th-ch/youtube-music/releases/)
|
||||
[](https://github.com/th-ch/youtube-music/blob/master/LICENSE)
|
||||
[](https://github.com/th-ch/youtube-music/blob/master/.eslintrc.js)
|
||||
[](https://GitHub.com/th-ch/youtube-music/releases/)
|
||||
[](https://GitHub.com/th-ch/youtube-music/releases/)
|
||||
[](https://aur.archlinux.org/packages/youtube-music-bin)
|
||||
[](https://snyk.io/test/github/th-ch/youtube-music)
|
||||
|
||||
</div>
|
||||
|
||||

|
||||
|
||||
|
||||
<div align="center">
|
||||
<a href="https://github.com/th-ch/youtube-music/releases/latest">
|
||||
<img src="https://github.com/th-ch/youtube-music/raw/master/web/youtube-music.svg" width="400" height="100" alt="SVG YouTube Music">
|
||||
</a>
|
||||
</div>
|
||||
|
||||
|
||||
**Enveloppe Electron autour de YouTube Music offrant :**
|
||||
|
||||
- Aspect & sensation naturels, vise à conserver l'interface originale
|
||||
- Cadre pour les plugins personnalisés : modifiez YouTube Music selon vos besoins (style, contenu, fonctionnalités), activez/désactivez les plugins en
|
||||
un clic
|
||||
|
||||
## Image de démonstration
|
||||
|
||||
| Écran du lecteur (thème de couleur de l'album & lumière ambiante) |
|
||||
|:---------------------------------------------------------------------------------------------------------:|
|
||||
||
|
||||
|
||||
## Contenu
|
||||
|
||||
- [Fonctionnalités](#fonctionnalités)
|
||||
- [Plugins disponibles](#plugins-disponibles)
|
||||
- [Traduction](#traduction)
|
||||
- [Téléchargement](#téléchargement)
|
||||
- [Arch Linux](#arch-linux)
|
||||
- [MacOS](#macos)
|
||||
- [Windows](#windows)
|
||||
- [Comment installer sans connexion réseau ? (sous Windows)](#comment-installer-sans-connexion-réseau-sous-windows)
|
||||
- [Thèmes](#thèmes)
|
||||
- [Dev](#dev)
|
||||
- [Créez vos propres plugins](#créez-vos-propres-plugins)
|
||||
- [Créer un plugin](#créer-un-plugin)
|
||||
- [Cas d'utilisation courants](#cas-dutilisation-courants)
|
||||
- [Construction](#construction)
|
||||
- [Aperçu de la production](#aperçu-de-la-production)
|
||||
- [Tests](#tests)
|
||||
- [Licence](#licence)
|
||||
- [FAQ](#faq)
|
||||
|
||||
## Fonctionnalités :
|
||||
|
||||
- **Confirmation automatique lors de la pause** (Toujours activé) : désactiver
|
||||
la pop-up ["Continuer à regarder ?"](https://user-images.githubusercontent.com/61631665/129977894-01c60740-7ec6-4bf0-9a2c-25da24491b0e.png)
|
||||
qui pause la musique après un certain temps
|
||||
|
||||
- Et plus encore ...
|
||||
|
||||
## Plugins disponibles :
|
||||
|
||||
- **Bloqueur de publicités** : Bloquez toutes les publicités et le suivi dès le départ
|
||||
|
||||
- **Actions d'album** : Ajoute des boutons Je n'aime pas, Dislike, J'aime, et Unlike pour appliquer cela à toutes les chansons dans une playlist ou un album
|
||||
|
||||
- **Thème de couleur d'album** : Applique un thème dynamique et des effets visuels basés sur la palette de couleurs de l'album
|
||||
|
||||
- **Mode Ambiant** : Applique un effet d'éclairage en projetant des couleurs douces de la vidéo, sur l'arrière-plan de votre écran
|
||||
|
||||
- **Compresseur Audio** : Appliquer une compression audio (diminue le volume des parties les plus fortes du signal et augmente le
|
||||
volume des parties les plus douces)
|
||||
|
||||
- **Barre de navigation floue** : rend la barre de navigation transparente et floue
|
||||
|
||||
- **Contournement des restrictions d'âge** : contourner la vérification d'âge de YouTube
|
||||
|
||||
- **Sélecteur de sous-titres** : Activer les sous-titres
|
||||
|
||||
- **Barre latérale compacte** : Toujours définir la barre latérale en mode compact
|
||||
|
||||
- **Fondu enchaîné** : Fondu enchaîné entre les chansons
|
||||
|
||||
- **Désactiver la lecture automatique** : Fait démarrer chaque chanson en mode "pause"
|
||||
|
||||
- **[Discord](https://discord.com/) Présence riche** : Montrez à vos amis ce que vous écoutez
|
||||
avec [Présence riche](https://user-images.githubusercontent.com/28219076/104362104-a7a0b980-5513-11eb-9744-bb89eabe0016.png)
|
||||
|
||||
- **Téléchargeur** : télécharge des
|
||||
MP3 [directement depuis l'interface](https://user-images.githubusercontent.com/61631665/129977677-83a7d067-c192-45e1-98ae-b5a4927393be.png) [(youtube-dl)](https://github.com/ytdl-org/youtube-dl)
|
||||
|
||||
- **Volume exponentiel** : Rend le curseur de volume
|
||||
[exponentiel](https://greasyfork.org/en/scripts/397686-youtube-music-fix-volume-ratio/) afin qu'il soit plus facile de
|
||||
sélectionner des volumes plus bas
|
||||
|
||||
- **Menu In-App** : [donne aux barres un aspect chic et sombre](https://user-images.githubusercontent.com/78568641/112215894-923dbf00-8c29-11eb-95c3-3ce15db27eca.png)
|
||||
|
||||
> (voir [ce poste](https://github.com/th-ch/youtube-music/issues/410#issuecomment-952060709) si vous avez des problèmes
|
||||
pour accéder au menu après avoir activé ce plugin et l'option masquer-menu)
|
||||
|
||||
- **Scrobbler** : Ajoute le support de scrobbling pour [Last.fm](https://www.last.fm/) et [ListenBrainz](https://listenbrainz.org/)
|
||||
|
||||
- **Lumia Stream** : Ajoute le support de [Lumia Stream](https://lumiastream.com/)
|
||||
|
||||
- **Lyrics Genius** : Ajoute le support des paroles pour la plupart des chansons
|
||||
|
||||
- **Musique Ensemble** : Partagez une playlist avec d'autres. Lorsque l'hôte joue une chanson, tout le monde entendra la même chanson
|
||||
|
||||
- **Navigation** : Flèches de navigation Suivant/Retour directement intégrées dans l'interface, comme dans votre navigateur préféré
|
||||
|
||||
- **Pas de connexion Google** : Supprime les boutons et les liens de connexion Google de l'interface
|
||||
|
||||
- **Notifications** : Affiche une notification lorsqu'une chanson commence à jouer ([notifications interactives](https://user-images.githubusercontent.com/78568641/114102651-63ce0e00-98d0-11eb-9dfe-c5a02bb54f9c.png)
|
||||
sont disponibles sur Windows)
|
||||
|
||||
- **Image dans l'image** : permet de passer l'application en mode image dans l'image
|
||||
|
||||
- **Vitesse de lecture** : Écoutez rapidement, écoutez lentement ! [Ajoute un curseur qui contrôle la vitesse des chansons](https://user-images.githubusercontent.com/61631665/129976003-e55db5ba-bf42-448c-a059-26a009775e68.png)
|
||||
|
||||
- **Volume précis** : Contrôlez le volume précisément en utilisant la molette de la souris/raccourcis clavier, avec un hud personnalisé et des étapes de volume personnalisables
|
||||
|
||||
- **Raccourcis (& MPRIS)** : Permet de définir des raccourcis globaux pour la lecture (lecture/pause/suivant/précédent) +
|
||||
désactive [osd média](https://user-images.githubusercontent.com/84923831/128601225-afa38c1f-dea8-4209-9f72-0f84c1dd8b54.png)
|
||||
en remplaçant les touches multimédias + activer Ctrl/CMD + F pour rechercher + activer le support mpris linux pour
|
||||
les touches multimédias + [raccourcis personnalisés](https://github.com/Araxeus/youtube-music/blob/1e591d6a3df98449bcda6e63baab249b28026148/providers/song-controls.js#L13-L50)
|
||||
pour [utilisateurs avancés](https://github.com/th-ch/youtube-music/issues/106#issuecomment-952156902)
|
||||
|
||||
- **Passer la chanson non aimée** : passe les chansons non aimées
|
||||
|
||||
- **Passer les silences** : passe automatiquement les sections silencieuses
|
||||
|
||||
- [**SponsorBlock**](https://github.com/ajayyy/SponsorBlock) : Saute automatiquement les parties non musicales comme les intros/outros ou
|
||||
les parties des clips vidéo où la chanson n'est pas jouée
|
||||
|
||||
- **Contrôle multimédia de la barre des tâches** : Contrôlez la lecture depuis
|
||||
votre [barre des tâches Windows](https://user-images.githubusercontent.com/78568641/111916130-24a35e80-8a82-11eb-80c8-5021c1aa27f4.png)
|
||||
|
||||
- **TouchBar** : Disposition personnalisée de la TouchBar pour macOS
|
||||
|
||||
- **Tuna OBS** : Intégration avec le
|
||||
plugin [Tuna](https://obsproject.com/forum/resources/tuna.843/) d'[OBS](https://obsproject.com/)
|
||||
|
||||
- **Changeur de qualité vidéo** : Permet de changer la qualité vidéo avec
|
||||
un [bouton](https://user-images.githubusercontent.com/78568641/138574366-70324a5e-2d64-4f6a-acdd-dc2a2b9cecc5.png) sur
|
||||
l'overlay vidéo
|
||||
|
||||
- **Bascule vidéo** : Ajoute
|
||||
un [bouton](https://user-images.githubusercontent.com/28893833/173663950-63e6610e-a532-49b7-9afa-54cb57ddfc15.png) pour
|
||||
basculer entre le mode Vidéo/Chanson. peut également supprimer l'onglet vidéo entier
|
||||
|
||||
- **Visualiseur** : Différents visualiseurs musicaux
|
||||
|
||||
## Traduction
|
||||
|
||||
Vous pouvez aider à la traduction sur [Hosted Weblate](https://hosted.weblate.org/projects/youtube-music/).
|
||||
|
||||
<a href="https://hosted.weblate.org/engage/youtube-music/">
|
||||
<img src="https://hosted.weblate.org/widget/youtube-music/i18n/multi-auto.svg" alt="statut de la traduction" />
|
||||
<img src="https://hosted.weblate.org/widget/youtube-music/i18n/287x66-black.png" alt="statut de la traduction 2" />
|
||||
</a>
|
||||
|
||||
## Téléchargement
|
||||
|
||||
Vous pouvez consulter la [dernière sortie](https://github.com/th-ch/youtube-music/releases/latest) pour trouver rapidement la
|
||||
dernière version.
|
||||
|
||||
### Arch Linux
|
||||
|
||||
Installez le paquet [`youtube-music-bin`](https://aur.archlinux.org/packages/youtube-music-bin) depuis l'AUR. Pour les instructions d'installation de l'AUR, consultez
|
||||
cette [page wiki](https://wiki.archlinux.org/index.php/Arch_User_Repository#Installing_packages).
|
||||
|
||||
### MacOS
|
||||
|
||||
Vous pouvez installer l'application en utilisant Homebrew (voir la [définition du fût](https://github.com/th-ch/homebrew-youtube-music)) :
|
||||
|
||||
```bash
|
||||
brew install th-ch/youtube-music/youtube-music
|
||||
```
|
||||
|
||||
Si vous installez l'application manuellement et obtenez une erreur "est endommagé et ne peut pas être ouvert." lors du lancement de l'application, exécutez ce qui suit dans le Terminal :
|
||||
|
||||
```bash
|
||||
xattr -cr /Applications/YouTube\ Music.app
|
||||
```
|
||||
|
||||
### Windows
|
||||
|
||||
Vous pouvez utiliser le [gestionnaire de paquets Scoop](https://scoop.sh) pour installer le paquet `youtube-music` depuis le [seau `extras`](https://github.com/ScoopInstaller/Extras).
|
||||
|
||||
```bash
|
||||
scoop bucket add extras
|
||||
scoop install extras/youtube-music
|
||||
```
|
||||
|
||||
Alternativement, vous pouvez utiliser [Winget](https://learn.microsoft.com/fr-fr/windows/package-manager/winget/), le gestionnaire de paquets CLI officiel de Windows 11, pour installer le paquet `th-ch.YouTubeMusic`.
|
||||
|
||||
*Note : Microsoft Defender SmartScreen pourrait bloquer l'installation car elle provient d'un "éditeur inconnu". Ceci est également vrai pour l'installation manuelle lors de l'essai d'exécution de l'exécutable (.exe) après un téléchargement manuel ici sur GitHub (même fichier).*
|
||||
|
||||
```bash
|
||||
winget install th-ch.YouTubeMusic
|
||||
```
|
||||
|
||||
#### Comment installer sans connexion réseau ? (sous Windows)
|
||||
|
||||
- Téléchargez le fichier `*.nsis.7z` pour _l'architecture de votre appareil_ sur la [page des versions](https://github.com/th-ch/youtube-music/releases/latest).
|
||||
- `x64` pour Windows 64 bits
|
||||
- `ia32` pour Windows 32 bits
|
||||
- `arm64` pour Windows ARM64
|
||||
- Téléchargez l'installeur sur la page des versions. (`*-Setup.exe`)
|
||||
- Placez-les dans le **même dossier**.
|
||||
- Exécutez l'installeur.
|
||||
|
||||
## Thèmes
|
||||
|
||||
Vous pouvez charger des fichiers CSS pour changer l'apparence de l'application (Options > Ajustements visuels > Thèmes).
|
||||
|
||||
Certains thèmes prédéfinis sont disponibles sur [https://github.com/kerichdev/themes-for-ytmdesktop-player](https://github.com/kerichdev/themes-for-ytmdesktop-player).
|
||||
|
||||
## Dev
|
||||
|
||||
```bash
|
||||
git clone https://github.com/th-ch/youtube-music
|
||||
cd youtube-music
|
||||
pnpm install --frozen-lockfile
|
||||
pnpm dev
|
||||
```
|
||||
## Créez vos propres plugins
|
||||
|
||||
En utilisant des plugins, vous pouvez :
|
||||
|
||||
- manipuler l'application - la `BrowserWindow` d'Electron est passée au gestionnaire de plugin
|
||||
- changer le front en manipulant le HTML/CSS
|
||||
|
||||
### Créer un plugin
|
||||
|
||||
Créez un dossier dans `src/plugins/NOM-DE-VOTRE-PLUGIN` :
|
||||
|
||||
- `index.ts` : le fichier principal du plugin
|
||||
```typescript
|
||||
import style from './style.css?inline'; // importez le style comme inline
|
||||
|
||||
import { createPlugin } from '@/utils';
|
||||
|
||||
export default createPlugin({
|
||||
name: 'Étiquette du plugin',
|
||||
restartNeeded: true, // si la valeur est vraie, ytmusic affichera la boîte de dialogue de redémarrage
|
||||
config: {
|
||||
enabled: false,
|
||||
}, // votre configuration personnalisée
|
||||
stylesheets: [style], // votre style personnalisé,
|
||||
menu: async ({ getConfig, setConfig }) => {
|
||||
// Toutes les méthodes *Config sont des promesses encapsulées <T>
|
||||
const config = await getConfig();
|
||||
return [
|
||||
{
|
||||
label: 'menu',
|
||||
submenu: [1, 2, 3].map((value) => ({
|
||||
label: `valeur ${value}`,
|
||||
type: 'radio',
|
||||
checked: config.value === value,
|
||||
click() {
|
||||
setConfig({ value });
|
||||
},
|
||||
})),
|
||||
},
|
||||
];
|
||||
},
|
||||
backend: {
|
||||
start({ window, ipc }) {
|
||||
window.maximize();
|
||||
|
||||
// vous pouvez communiquer avec le plugin du rendu
|
||||
ipc.handle('un événement', () => {
|
||||
return 'bonjour';
|
||||
});
|
||||
},
|
||||
// il est déclenché lorsque la configuration change
|
||||
onConfigChange(newConfig) { /* ... */ },
|
||||
// il est déclenché lorsque le plugin est désactivé
|
||||
stop(context) { /* ... */ },
|
||||
},
|
||||
renderer: {
|
||||
async start(context) {
|
||||
console.log(await context.ipc.invoke('un événement'));
|
||||
},
|
||||
// Seul le crochet disponible pour le rendu
|
||||
onPlayerApiReady(api: YoutubePlayer, context: RendererContext) {
|
||||
// définir facilement la configuration du plugin
|
||||
context.setConfig({ myConfig: api.getVolume() });
|
||||
},
|
||||
onConfigChange(newConfig) { /* ... */ },
|
||||
stop(_context) { /* ... */ },
|
||||
},
|
||||
preload: {
|
||||
async start({ getConfig }) {
|
||||
const config is obtained by `getConfig` method.
|
||||
},
|
||||
onConfigChange(newConfig) {},
|
||||
stop(_context) {},
|
||||
},
|
||||
});
|
||||
|
||||
```
|
||||
|
||||
### Cas d'utilisation courants
|
||||
|
||||
- **Injection de CSS personnalisé** : créez un fichier `style.css` dans le même dossier puis :
|
||||
|
||||
```typescript
|
||||
// index.ts
|
||||
import style from './style.css?inline'; // importez le style comme en ligne
|
||||
|
||||
import { createPlugin } from '@/utils';
|
||||
|
||||
export default createPlugin({
|
||||
name: 'Étiquette du plugin',
|
||||
restartNeeded: true, // si la valeur est vraie, ytmusic affichera la boîte de dialogue de redémarrage
|
||||
config: {
|
||||
enabled: false,
|
||||
}, // votre configuration personnalisée
|
||||
stylesheets: [style], // votre style personnalisé
|
||||
renderer() {} // définissez le crochet de rendu
|
||||
});
|
||||
```
|
||||
|
||||
- **Si vous voulez modifier le HTML** :
|
||||
|
||||
```typescript
|
||||
import { createPlugin } from '@/utils';
|
||||
|
||||
export default createPlugin({
|
||||
name: 'Étiquette du plugin',
|
||||
restartNeeded: true, // si la valeur est vraie, ytmusic affichera la boîte de dialogue de redémarrage
|
||||
config: {
|
||||
enabled: false,
|
||||
}, // votre configuration personnalisée
|
||||
renderer() {
|
||||
// Supprimez le bouton de connexion
|
||||
document.querySelector(".sign-in-link.ytmusic-nav-bar").remove();
|
||||
} // définissez le crochet de rendu
|
||||
});
|
||||
|
||||
```
|
||||
|
||||
- **Communication entre le front et le back** : cela peut se faire en utilisant le module ipcMain d'Electron. Voir le fichier `index.ts` et l'exemple dans le plugin `sponsorblock`.
|
||||
|
||||
## Construction
|
||||
|
||||
1. Clonez le dépôt
|
||||
2. Suivez [ce guide](https://pnpm.io/installation) pour installer `pnpm`
|
||||
3. Exécutez `pnpm install --frozen-lockfile` pour installer les dépendances
|
||||
4. Exécutez `pnpm build:OS`
|
||||
|
||||
- `pnpm dist:win` - pour Windows
|
||||
- `pnpm dist:linux` - pour Linux
|
||||
- `pnpm dist:mac` - pour MacOS
|
||||
|
||||
Construit l'application pour macOS, Linux et Windows,
|
||||
en utilisant [electron-builder](https://github.com/electron-userland/electron-builder).
|
||||
|
||||
## Aperçu de la production
|
||||
|
||||
```bash
|
||||
pnpm start
|
||||
```
|
||||
|
||||
## Tests
|
||||
|
||||
```bash
|
||||
pnpm test
|
||||
```
|
||||
|
||||
Utilise [Playwright](https://playwright.dev/) pour tester l'application.
|
||||
|
||||
## Licence
|
||||
|
||||
MIT © [th-ch](https://github.com/th-ch/youtube-music)
|
||||
|
||||
## FAQ
|
||||
|
||||
### Pourquoi le menu de l'application ne s'affiche-t-il pas ?
|
||||
|
||||
Si l'option `Masquer le menu` est activée - vous pouvez afficher le menu avec la touche <kbd>alt</kbd> (ou <kbd>\`</kbd> [backtick] si vous utilisez le plugin du menu intégré)
|
||||
388
docs/readme/README-is.md
Normal file
388
docs/readme/README-is.md
Normal file
@ -0,0 +1,388 @@
|
||||
<div align="center">
|
||||
|
||||
# YouTube Tónlist
|
||||
|
||||
[](https://github.com/th-ch/youtube-music/releases/)
|
||||
[](https://github.com/th-ch/youtube-music/blob/master/LICENSE)
|
||||
[](https://github.com/th-ch/youtube-music/blob/master/.eslintrc.js)
|
||||
[](https://GitHub.com/th-ch/youtube-music/releases/)
|
||||
[](https://GitHub.com/th-ch/youtube-music/releases/)
|
||||
[](https://aur.archlinux.org/packages/youtube-music-bin)
|
||||
[](https://snyk.io/test/github/th-ch/youtube-music)
|
||||
|
||||
</div>
|
||||
|
||||

|
||||
|
||||
|
||||
<div align="center">
|
||||
<a href="https://github.com/th-ch/youtube-music/releases/latest">
|
||||
<img src="../../web/youtube-music.svg" width="400" height="100" alt="YouTube Music SVG">
|
||||
</a>
|
||||
</div>
|
||||
|
||||
**Electron umbúðir utan um YouTube Tónlist sem inniheldur:**
|
||||
|
||||
- Innfæddur útlit og tilfinning, miðar að því að halda upprunalegu viðmótinu
|
||||
- Rammi fyrir sérsniðnar tengiforrit: breyttu YouTube Tónlist að þínum þörfum (stíl, efni, eiginleikar), virkjaðu/slökktu á viðbætur í
|
||||
einn smellur
|
||||
|
||||
## Sýnishornsmynd
|
||||
|
||||
| Spilaraskjár (albúmslitaþema & umhverfisljós) |
|
||||
|:---------------------------------------------------------------------------------------------------------:|
|
||||
||
|
||||
|
||||
## Efni
|
||||
|
||||
- [Eiginleikar](#eiginleikar)
|
||||
- [Tiltæk tengiforrit](#tiltæk-tengiforrit)
|
||||
- [Þýðing](#þýðing)
|
||||
- [Sækja](#sækja)
|
||||
- [Arch Linux](#arch-linux)
|
||||
- [MacOS](#macos)
|
||||
- [Windows](#windows)
|
||||
- [Hvernig á að setja upp án nettengingar? (í Windows)](#hvernig-á-að-setja-upp-án-nettengingar-í-windows)
|
||||
- [Þemu](#þemu)
|
||||
- [Þróun](#þróun)
|
||||
- [Búðu til þín eigin viðbætur](#búðu-til-þín-eigin-viðbætur)
|
||||
- [Er að búa til viðbót](#er-að-búa-til-viðbót)
|
||||
- [Algeng notkunartilvik](#algeng-notkunartilvik)
|
||||
- [Byggja](#byggja)
|
||||
- [Framleiðsluforskoðun](#framleiðsluforskoðun)
|
||||
- [Prófanir](#prófanir)
|
||||
- [Leyfi](#leyfi)
|
||||
- [Algengustu spurningar](#algengustu-spurningar)
|
||||
|
||||
## Eiginleikar:
|
||||
|
||||
- **Sjálfvirk staðfesting þegar gert er hlé** (Alltaf virkt): slökkva á
|
||||
["Halda áfram að horfa?"](https://user-images.githubusercontent.com/61631665/129977894-01c60740-7ec6-4bf0-9a2c-25da24491b0e.png)
|
||||
popup sem gerir hlé á tónlist eftir ákveðinn tíma
|
||||
|
||||
- Og meira...
|
||||
|
||||
## Tiltæk tengiforrit:
|
||||
|
||||
- **Auglýsingablokkari**: Lokaðu fyrir allar auglýsingar og rakningar úr kassanum
|
||||
|
||||
- **Albúmsaðgerðir**: Bætir Ódíslika, Mislíkt, Líkt, og Ólíkt til að nota þetta á öll lög á spilunarlista eða albúm
|
||||
|
||||
- **Albúmslitaþema**: Beitir kraftmikið þema og sjónrænum áhrifum sem byggjast á litavali albúmsins
|
||||
|
||||
- **Umhverfishamur**: Beitir lýsingaráhrifum með því að varpa mildum litum úr myndbandinu í bakgrunn skjásins
|
||||
|
||||
- **Hljóðþjöppur**: Notaðu þjöppun á hljóð (lækkar hljóðstyrk háværustu hluta merkis og hækkar hljóðstyrk í mýkstu hlutunum)
|
||||
|
||||
- **Þoka Leiðsagnarstika**: Gerir leiðsögustikuna gagnsæja og óskýrt
|
||||
|
||||
- **Farið Framhjá Aldurstakmörkunum**: Framhjá aldursstaðfestingu YouTube
|
||||
|
||||
- **Yfirskriftarval**: Virkja skjátexta
|
||||
|
||||
- **Fyrirferðarlítillhliðarstika**: Stilltu hliðarstikuna alltaf í þétta stillingu
|
||||
|
||||
- **Krossfæra**: Krossfæra á milli lög
|
||||
|
||||
- **Slökkva á Sjálfvirkri Spilun**: Gerir lag að byrja í "hlé" ham
|
||||
|
||||
- **[Discord](https://discord.com/) Rík Nærveru**: Sýndu vinum þínum hvað þú hlustar á
|
||||
með [Rík Nærveru](https://user-images.githubusercontent.com/28219076/104362104-a7a0b980-5513-11eb-9744-bb89eabe0016.png)
|
||||
|
||||
- **Niðurhalari**: Niðurhalum
|
||||
MP3 [beint úr viðmótinu](https://user-images.githubusercontent.com/61631665/129977677-83a7d067-c192-45e1-98ae-b5a4927393be.png) [(youtube-dl)](https://github.com/ytdl-org/youtube-dl)
|
||||
|
||||
- **Veldibundiðrúmmál**: Gerir hljóðstyrkssleðann [veldisvísis](https://greasyfork.org/en/scripts/397686-youtube-music-fix-volume-ratio/)
|
||||
svo það er auðveldara að velja lægra hljóðstyrk.
|
||||
|
||||
- **Valmynd í Forriti**: [Gefur börum flott, dökkt útlit](https://user-images.githubusercontent.com/78568641/112215894-923dbf00-8c29-11eb-95c3-3ce15db27eca.png)
|
||||
|
||||
> (sjá [þessa færslu](https://github.com/th-ch/youtube-music/issues/410#issuecomment-952060709) ef þú átt í vandræðum
|
||||
með að fá aðgang að valmyndinni eftir að hafa virkjað þessa viðbót og fela valmyndarvalkostinn)
|
||||
|
||||
- **Scrobbler**: Bætir við scrobbling stuðningi fyrir [Last.fm](https://www.last.fm/) og [ListenBrainz](https://listenbrainz.org/)
|
||||
|
||||
- **Lumia Stream**: Bætir við [Lumia Stream](https://lumiastream.com/) stuðningi
|
||||
|
||||
- **Söngtexti Snilld**: Bætir stuðningi við texta fyrir flest lög
|
||||
|
||||
- **Tónlist Saman**: Deila spilunarlista með öðrum. Þegar gestgjafinn spilar lag munu allir aðrir heyra sama lagið
|
||||
|
||||
- **Leiðsögn**: Næsta/Til baka leiðsagnarörvar beint samþættar í viðmótinu, eins og í uppáhalds vafranum þínum
|
||||
|
||||
- **Engin Google Innskráning**: Fjarlægðu Google innskráningarhnappa og tengla úr viðmótinu
|
||||
|
||||
- **Tilkynningar**: Birta tilkynningu þegar lag byrjar að spila
|
||||
([gagnvirkartilkynningar](https://user-images.githubusercontent.com/78568641/114102651-63ce0e00-98d0-11eb-9dfe-c5a02bb54f9c.png) eru fáanlegar á Windows)
|
||||
|
||||
- **Mynd-í-Mynd**: Gerir kleift að skipta forritinu yfir í mynd-í-mynd stillingu
|
||||
|
||||
- **Spilunarhraði**: Hlustaðu hratt, hlustaðu hægt!
|
||||
[Bætir við sleða sem stjórnar lagahraðanum](https://user-images.githubusercontent.com/61631665/129976003-e55db5ba-bf42-448c-a059-26a009775e68.png)
|
||||
|
||||
- **Nákvæmshljóðstyrkur**: Stjórnaðu hljóðstyrknum nákvæmlega með músarhjóli/hraðtökkum, með sérsniðnum HUD og sérsniðnum hljóðstyrksþrepum
|
||||
|
||||
- **Flýtileiðir (og MPRIS)**: Leyfir að stilla alþjóðlegarflýtilyklar fyrir spilun (spila/gera hlé/næsta/fyrri) +
|
||||
óvirkja [media osd](https://user-images.githubusercontent.com/84923831/128601225-afa38c1f-dea8-4209-9f72-0f84c1dd8b54.png)
|
||||
með því að hnekkja miðlunarlyklum + virkja Ctrl/CMD + F til að leita + virkja linux mpris stuðning fyrir
|
||||
miðlunarlyklar + [sérsniðnir flýtilyklar](https://github.com/Araxeus/youtube-music/blob/1e591d6a3df98449bcda6e63baab249b28026148/providers/song-controls.js#L13-L50)
|
||||
fyrir [háþróaða notendur](https://github.com/th-ch/youtube-music/issues/106#issuecomment-952156902)
|
||||
- **Slepptu Lögum sem Mislíkuðust**: Sleppir mislíkaði lög
|
||||
|
||||
- **Slepptu Þögnum**: Slepptu sjálfkrafa þagnarköflum í lögum
|
||||
|
||||
- [**Styrktarblokk**](https://github.com/ajayyy/SponsorBlock): Sleppur sjálfkrafa hlutum sem ekki eru tónlist, eins og inngangur/lok
|
||||
eða hlutar af tónlistarmyndböndum þar sem lag er ekki að spila
|
||||
|
||||
- **Miðlunarstýringarverkefnastikunnar**: Stjórnaðu spilun frá [Windows verkefnastikunni þinni](https://user-images.githubusercontent.com/78568641/111916130-24a35e80-8a82-11eb-80c8-5021c1aa27f4.png)
|
||||
|
||||
- **Snertistiku**: Sérsniðið Snertistikuútlit fyrir macOS
|
||||
|
||||
- **Tuna OBS**: Samþætting við [OBS](https://obsproject.com/)
|
||||
viðbótina [Tuna](https://obsproject.com/forum/resources/tuna.843/)
|
||||
|
||||
- **Myndbandgæðisbreyting**: Leyfir að breyta myndbandgæðum með
|
||||
[hnappi](https://user-images.githubusercontent.com/78568641/138574366-70324a5e-2d64-4f6a-acdd-dc2a2b9cecc5.png) á
|
||||
myndbandsyfirlaginu
|
||||
|
||||
- **Myndbandsrofi**: Bætir við [hnappi](https://user-images.githubusercontent.com/28893833/173663950-63e6610e-a532-49b7-9afa-54cb57ddfc15.png) til
|
||||
að skipta á milli myndbands/lagshams. Getur einnig valfrjálst fjarlægt allan myndbandsflipann
|
||||
|
||||
- **Sjónrænir**: Mismunandi tónlist sjónrænir
|
||||
|
||||
## Þýðing
|
||||
|
||||
Þú getur aðstoðað við þýðingar á [Hosted Weblate](https://hosted.weblate.org/projects/youtube-music/).
|
||||
|
||||
<a href="https://hosted.weblate.org/engage/youtube-music/">
|
||||
<img src="https://hosted.weblate.org/widget/youtube-music/i18n/multi-auto.svg" alt="translation status" />
|
||||
<img src="https://hosted.weblate.org/widget/youtube-music/i18n/287x66-black.png" alt="translation status 2" />
|
||||
</a>
|
||||
|
||||
## Sækja
|
||||
|
||||
Þú getur skoðað [nýjustu útgáfuna](https://github.com/th-ch/youtube-music/releases/latest) til að finna fljótt
|
||||
nýjustu útgáfuna.
|
||||
|
||||
### Arch Linux
|
||||
|
||||
Settu upp [`youtube-music-bin`](https://aur.archlinux.org/packages/youtube-music-bin) pakkann frá AUR. Fyrir AUR uppsetningarleiðbeiningar skaltu skoða
|
||||
þessa [wiki síðu](https://wiki.archlinux.org/index.php/Arch_User_Repository#Installing_packages).
|
||||
|
||||
### MacOS
|
||||
|
||||
Þú getur sett upp appið með því að nota Homebrew (sjá [cask skilgreiningu](https://github.com/th-ch/homebrew-youtube-music))
|
||||
|
||||
```bash
|
||||
brew install th-ch/youtube-music/youtube-music
|
||||
```
|
||||
|
||||
Ef þú setur upp forritið handvirkt og færð villu "er skemmd og ekki er hægt að opna það," þegar þú ræsir forritið skaltu keyra eftirfarandi í flugstöðinni:
|
||||
|
||||
```bash
|
||||
xattr -cr /Applications/YouTube\ Music.app
|
||||
```
|
||||
|
||||
### Windows
|
||||
|
||||
Þú getur notað [Scoop pakkastjórnun](https://scoop.sh) til að setja upp `youtube-music` pakkann frá
|
||||
[`extras` fötunni](https://github.com/ScoopInstaller/Extras).
|
||||
|
||||
```bash
|
||||
scoop bucket add extras
|
||||
scoop install extras/youtube-music
|
||||
```
|
||||
|
||||
Að öðrum kosti geturðu notað [Winget](https://learn.microsoft.com/en-us/windows/package-manager/winget/), Windows 11s
|
||||
opinber CLI pakkastjóri til að setja upp `th-ch.YouTubeMusic` pakkann.
|
||||
|
||||
*Athugið: Microsoft Defender SmartScreen gæti lokað uppsetningunni þar sem hún er frá „óþekktum útgefanda“. Þetta er einnig
|
||||
satt fyrir handvirka uppsetningu þegar reynt er að keyra executable(.exe) eftir handvirkt niðurhal hér á github (sama
|
||||
skrá).*
|
||||
|
||||
```bash
|
||||
winget install th-ch.YouTubeMusic
|
||||
```
|
||||
|
||||
#### Hvernig á að setja upp án nettengingar? (í Windows)
|
||||
|
||||
- Sæktu `*.nsis.7z` skrána fyrir _arkitektúr tækisins þíns_ á [útgáfusíðu](https://github.com/th-ch/youtube-music/releases/latest).
|
||||
- `x64` fyrir 64-bita Windows
|
||||
- `ia32` fyrir 32-bita Windows
|
||||
- `arm64` fyrir ARM64 Windows
|
||||
- Sæktu uppsetningarforrit á útgáfusíðu. (`*-Setup.exe`)
|
||||
- Settu þær í **sömu möppuna**.
|
||||
- Keyrðu uppsetningarforritið.
|
||||
|
||||
## Þemu
|
||||
|
||||
Þú getur hlaðið CSS skrám til að breyta útliti forritsins (Valkostir > Sjónræn klip > Þemu).
|
||||
|
||||
Sum fyrirframskilgreind þemu eru fáanleg á https://github.com/kerichdev/themes-for-ytmdesktop-player.
|
||||
|
||||
## Þróun
|
||||
|
||||
```bash
|
||||
git clone https://github.com/th-ch/youtube-music
|
||||
cd youtube-music
|
||||
pnpm install --frozen-lockfile
|
||||
pnpm dev
|
||||
```
|
||||
|
||||
## Búðu til þín eigin tengiforrit
|
||||
|
||||
Með því að nota tengiforrit geturðu:
|
||||
|
||||
- vinna með appið - `BrowserWindow` frá electron er sent til tengiforritsstjórans
|
||||
- breyttu framhliðinni með því að vinna með HTML/CSS
|
||||
|
||||
### Er að búa til tengiforrit
|
||||
|
||||
Búðu til möppu í `src/plugins/YOUR-PLUGIN-NAME`:
|
||||
|
||||
- `index.ts`: aðal skránni af tengiforritið
|
||||
```typescript
|
||||
import style from './style.css?inline'; // flytja inn stíl sem inline
|
||||
|
||||
import { createPlugin } from '@/utils';
|
||||
|
||||
export default createPlugin({
|
||||
name: 'Plugin Label',
|
||||
restartNeeded: true, // ef gildi er satt, ytmusic sjá endurræsa gluggann
|
||||
config: {
|
||||
enabled: false,
|
||||
}, // sérsniðnastillingar þinn
|
||||
stylesheets: [style], // sérsniðnastílinn þinn
|
||||
menu: async ({ getConfig, setConfig }) => {
|
||||
// Allar *stillingaraðferðir eru umvafnar Lofor<T>
|
||||
const config = await getConfig();
|
||||
return [
|
||||
{
|
||||
label: 'menu',
|
||||
submenu: [1, 2, 3].map((value) => ({
|
||||
label: `value ${value}`,
|
||||
type: 'radio',
|
||||
checked: config.value === value,
|
||||
click() {
|
||||
setConfig({ value });
|
||||
},
|
||||
})),
|
||||
},
|
||||
];
|
||||
},
|
||||
backend: {
|
||||
start({ window, ipc }) {
|
||||
window.maximize();
|
||||
|
||||
// þú getur tengst við renderer tengiforritið
|
||||
ipc.handle('some-event', () => {
|
||||
return 'hello';
|
||||
});
|
||||
},
|
||||
// það kviknaði þegar stillingum var breytt
|
||||
onConfigChange(newConfig) { /* ... */ },
|
||||
// it fired when plugin disabled
|
||||
stop(context) { /* ... */ },
|
||||
},
|
||||
renderer: {
|
||||
async start(context) {
|
||||
console.log(await context.ipc.invoke('some-event'));
|
||||
},
|
||||
// Aðeins krókur sem er í boði fyrir renderer
|
||||
onPlayerApiReady(api: YoutubePlayer, context: RendererContext) {
|
||||
// stilltu stillingar viðbótarinnar auðveldlega
|
||||
context.setConfig({ myConfig: api.getVolume() });
|
||||
},
|
||||
onConfigChange(newConfig) { /* ... */ },
|
||||
stop(_context) { /* ... */ },
|
||||
},
|
||||
preload: {
|
||||
async start({ getConfig }) {
|
||||
const config = await getConfig();
|
||||
},
|
||||
onConfigChange(newConfig) {},
|
||||
stop(_context) {},
|
||||
},
|
||||
});
|
||||
```
|
||||
|
||||
### Algeng notkunartilvik
|
||||
|
||||
- er að sprauta sérsniðnum CSS: búðu til `style.css` skrá í sömu möppu þá:
|
||||
|
||||
```typescript
|
||||
// index.ts
|
||||
import style from './style.css?inline'; // flytja inn stíl sem inline
|
||||
|
||||
import { createPlugin } from '@/utils';
|
||||
|
||||
export default createPlugin({
|
||||
name: 'Plugin Label',
|
||||
restartNeeded: true, // ef gildi er satt, ytmusic sjá endurræsa gluggann
|
||||
config: {
|
||||
enabled: false,
|
||||
}, // sérsniðnastillingar þinn
|
||||
stylesheets: [style], // sérsniðnastílinn þinn
|
||||
renderer() {} // skilgreina renderer krók
|
||||
});
|
||||
```
|
||||
|
||||
- Ef þú vilt breyta HTML:
|
||||
|
||||
```typescript
|
||||
import { createPlugin } from '@/utils';
|
||||
|
||||
export default createPlugin({
|
||||
name: 'Plugin Label',
|
||||
restartNeeded: true, // ef gildi er satt, ytmusic sjá endurræsa gluggann
|
||||
config: {
|
||||
enabled: false,
|
||||
}, // sérsniðnastillingar þinn
|
||||
renderer() {
|
||||
// Fjarlægðu innskráningarhnappinn
|
||||
document.querySelector(".sign-in-link.ytmusic-nav-bar").remove();
|
||||
} // skilgreina renderer krók
|
||||
});
|
||||
```
|
||||
|
||||
- samskipti á milli að framan og aftan: hægt að gera með því að nota ipcMain eininguna frá electron. Sjá `index.ts` skrá og
|
||||
dæmi í 'styrktarblokk' tengiforritinu.
|
||||
|
||||
## Byggja
|
||||
|
||||
1. Klóna geymsluna
|
||||
2. Fylgdu [þessa handbók](https://pnpm.io/installation) til að setja upp 'pnpm'
|
||||
3. Keyrðu `pnpm install --frozen-lockfile` til að setja upp ósjálfstæði
|
||||
4. Keyrðu `pnpm build:OS`
|
||||
|
||||
- `pnpm dist:win` - Windows
|
||||
- `pnpm dist:linux` - Linux
|
||||
- `pnpm dist:mac` - MacOS
|
||||
|
||||
Byggir appið fyrir macOS, Linux og Windows,
|
||||
með því að nota [electron-builder](https://github.com/electron-userland/electron-builder).
|
||||
|
||||
## Framleiðsluforskoðun
|
||||
|
||||
```bash
|
||||
pnpm start
|
||||
```
|
||||
|
||||
## Prófanir
|
||||
|
||||
```bash
|
||||
pnpm test
|
||||
```
|
||||
|
||||
Notar [Playwright](https://playwright.dev/) til að prófa forritið.
|
||||
|
||||
## Leyfi
|
||||
|
||||
MIT © [th-ch](https://github.com/th-ch/youtube-music)
|
||||
|
||||
## Algengustu Spurningar
|
||||
|
||||
### Hvers vegna forritavalmynd birtist ekki?
|
||||
|
||||
Ef valmöguleikinn „Fela valmynd“ er á - þú getur sýnt valmyndina með <kbd>alt</kbd> lyklinum (eða <kbd>\`</kbd> [bakka]
|
||||
ef þú notar viðbótina fyrir valmynd í forriti)
|
||||
348
docs/readme/README-ko.md
Normal file
348
docs/readme/README-ko.md
Normal file
@ -0,0 +1,348 @@
|
||||
<div align="center">
|
||||
|
||||
# 유튜브 뮤직 (YouTube Music)
|
||||
|
||||
[](https://github.com/th-ch/youtube-music/releases/)
|
||||
[](https://github.com/th-ch/youtube-music/blob/master/LICENSE)
|
||||
[](https://github.com/th-ch/youtube-music/blob/master/.eslintrc.js)
|
||||
[](https://GitHub.com/th-ch/youtube-music/releases/)
|
||||
[](https://GitHub.com/th-ch/youtube-music/releases/)
|
||||
[](https://aur.archlinux.org/packages/youtube-music-bin)
|
||||
[](https://snyk.io/test/github/th-ch/youtube-music)
|
||||
|
||||
</div>
|
||||
|
||||

|
||||
|
||||
<div align="center">
|
||||
<a href="https://github.com/th-ch/youtube-music/releases/latest">
|
||||
<img src="../../web/youtube-music.svg" width="400" height="100" alt="YouTube Music SVG">
|
||||
</a>
|
||||
</div>
|
||||
|
||||
**유튜브 뮤직의 Electron 래퍼; 기능:**
|
||||
|
||||
- 원래의 인터페이스를 유지하는 것을 목표로 하는 네이티브 디자인 및 느낌
|
||||
- 맞춤 플러그인을 위한 프레임워크: 스타일, 콘텐츠, 기능 등 필요에 따라 유튜브 뮤직을 변경하고, 클릭 한 번으로 플러그인을 활성화/비활성화할 수 있습니다.
|
||||
|
||||
## Content
|
||||
|
||||
- [기능](#기능)
|
||||
- [사용 가능한 플러그인](#사용-가능한-플러그인)
|
||||
- [번역](#번역)
|
||||
- [다운로드](#다운로드)
|
||||
- [Arch Linux](#arch-linux)
|
||||
- [MacOS](#macos)
|
||||
- [Windows](#windows)
|
||||
- [(Windows에서) 네트워크에 연결하지 않고 설치하는 방법은 무엇인가요?](#windows에서-네트워크에-연결하지-않고-설치하는-방법은-무엇인가요)
|
||||
- [테마](#테마)
|
||||
- [개발](#개발)
|
||||
- [나만의 플러그인 만들기](#나만의-플러그인-만들기)
|
||||
- [플러그인 만들기](#플러그인-만들기)
|
||||
- [일반적인 사용 예](#일반적인-사용-예)
|
||||
- [빌드](#빌드)
|
||||
- [프로덕션 빌드 미리보기](#프로덕션-빌드-미리보기)
|
||||
- [테스트](#테스트)
|
||||
- [라이선스](#라이선스)
|
||||
- [자주 묻는 질문](#자주-묻는-질문)
|
||||
|
||||
## 기능:
|
||||
|
||||
- **일시 정지 시 자동 확인** (항상 활성화 됨): 일정 시간이 지나면 음악을 일시 정지하는 ["계속 시청하시겠습니까?"](https://user-images.githubusercontent.com/61631665/129977894-01c60740-7ec6-4bf0-9a2c-25da24491b0e.png) 팝업을 비활성화합니다.
|
||||
|
||||
- 이외에 더 많은 기능 ...
|
||||
|
||||
## 사용 가능한 플러그인:
|
||||
|
||||
- **애드블록**: 모든 광고와 트래커를 즉시 차단합니다
|
||||
|
||||
- **앨범 컬러 기반 테마**: 앨범 색상 팔레트를 기반으로 동적 테마 및 시각 효과를 적용합니다
|
||||
|
||||
- **앰비언트 모드**: 영상의 간접 조명을 화면 배경에 투사합니다.
|
||||
|
||||
- **오디오 컴프레서**: 오디오에 컴프레서를 적용합니다 (신호에서 가장 시끄러운 부분의 음량을 낮추고 가장 조용한 부분의 음량을 높임)
|
||||
|
||||
- **네비게이션 바 흐림 효과**: 내비게이션 바를 투명하고 흐릿하게 만듭니다
|
||||
|
||||
- **나이 제한 우회**: 유튜브의 나이 제한을 우회합니다
|
||||
|
||||
- **자막 선택기**: 자막을 활성화합니다
|
||||
|
||||
- **컴팩트 사이드바**: 사이드바를 항상 컴팩트 모드로 설정합니다
|
||||
|
||||
- **크로스페이드**: 노래 사이에 크로스페이드 효과를 적용합니다
|
||||
|
||||
- **자동 재생 해제**: 노래를 '일시 정지' 모드로 시작하게 합니다
|
||||
|
||||
- [**디스코드 활동 상태**](https://discord.com/): [활동 상태 (Rich Presence)](https://user-images.githubusercontent.com/28219076/104362104-a7a0b980-5513-11eb-9744-bb89eabe0016.png)를 사용하여 친구들에게 내가 듣는 음악을 보여주세요
|
||||
|
||||
- **다운로더**: UI에서 [직접](https://user-images.githubusercontent.com/61631665/129977677-83a7d067-c192-45e1-98ae-b5a4927393be.png) MP3/소스 오디오를 다운로드하세요
|
||||
|
||||
- **지수 볼륨**: 음량 슬라이더를 [지수적](https://greasyfork.org/en/scripts/397686-youtube-music-fix-volume-ratio/)으로 만들어 더 낮은 음량을 쉽게 선택할 수 있도록 합니다.
|
||||
|
||||
- **인앱 메뉴**: [메뉴 표시줄을 더 멋지게, 그리고 다크 또는 앨범의 색상으로 만듭니다](https://user-images.githubusercontent.com/78568641/112215894-923dbf00-8c29-11eb-95c3-3ce15db27eca.png)
|
||||
|
||||
> (이 플러그인 및 메뉴 숨기기 옵션을 활성화한 후 메뉴에 액세스하는 데 문제가 있는 경우 [이 글](https://github.com/th-ch/youtube-music/issues/410#issuecomment-952060709)을 참조하세요)
|
||||
|
||||
- [**Last.fm**](https://www.last.fm/): Last.fm에 대한 스크러블 지원을 추가합니다
|
||||
|
||||
- **Lumia Stream**: [Lumia Stream](https://lumiastream.com/) 지원을 추가합니다
|
||||
|
||||
- **Genius 가사**: 더 많은 곡에 대해 가사 지원을 추가합니다
|
||||
|
||||
- **네비게이션**: 브라우저에서처럼, UI에 직접 통합된 앞으로/뒤로 탐색하는 화살표를 추가합니다
|
||||
|
||||
- **Google 로그인 제거**: UI에서 Google 로그인 버튼 및 링크 제거하기
|
||||
|
||||
- **알림**: 노래 재생이 시작되면 알림을 표시 (Windows에서는 [대화형 알림](https://user-images.githubusercontent.com/78568641/114102651-63ce0e00-98d0-11eb-9dfe-c5a02bb54f9c.png) 사용 가능)
|
||||
|
||||
- **PiP**: 앱을 PiP 모드로 전환할 수 있게 허용합니다
|
||||
|
||||
- **재생 속도**: 빨리 듣거나, 천천히 들어보세요! [노래 속도를 제어하는 슬라이더를 추가합니다](https://user-images.githubusercontent.com/61631665/129976003-e55db5ba-bf42-448c-a059-26a009775e68.png)
|
||||
|
||||
- **정확한 음량**: 사용자 지정 HUD와 사용자 지정 음량 단계 및 마우스 휠/단축키를 사용하여 음량을 정확하게 제어하세요
|
||||
|
||||
- **영상 품질 체인저**: 영상 오버레이의 [버튼](https://user-images.githubusercontent.com/78568641/138574366-70324a5e-2d64-4f6a-acdd-dc2a2b9cecc5.png)으로 영상 품질을 변경할 수 있게 합니다
|
||||
|
||||
- **단축키 (& MPRIS)**: 재생을 위한 전역 단축키 설정 허용 (재생/일시 정지/다음/이전) + 미디어 키를 재정의하여 [미디어 osd](https://user-images.githubusercontent.com/84923831/128601225-afa38c1f-dea8-4209-9f72-0f84c1dd8b54.png) 비활성화 + Ctrl/CMD + F 검색 활성화 + 미디어 키에 대한 리눅스 MPRIS 지원 활성화 + [고급 사용자](https://github.com/th-ch/youtube-music/issues/106#issuecomment-952156902)를 위한 [사용자 지정 단축키](https://github.com/Araxeus/youtube-music/blob/1e591d6a3df98449bcda6e63baab249b28026148/providers/song-controls.js#L13-L50) 지원
|
||||
|
||||
- **무음 건너뛰기** - 노래의 무음 부분을 자동으로 건너뜁니다
|
||||
|
||||
- [**SponsorBlock**](https://github.com/ajayyy/SponsorBlock): 인트로/아웃트로와 같은 음악이 아닌 부분이나, 노래가 재생되지 않는 뮤직 비디오의 일부를 자동으로 건너뜁니다
|
||||
|
||||
- **작업표시줄 미디어 컨트롤**: [Windows 작업표시줄](https://user-images.githubusercontent.com/78568641/111916130-24a35e80-8a82-11eb-80c8-5021c1aa27f4.png)에서 재생을 제어하세요
|
||||
|
||||
- **TouchBar**: macOS 사용자를 위한 TouchBar 위젯을 추가합니다
|
||||
|
||||
- **Tuna-OBS**: [OBS](https://obsproject.com/)의 플러그인, [Tuna](https://obsproject.com/forum/resources/tuna.843/)와 통합을 활성화합니다
|
||||
|
||||
- **영상 전환**: 영상/노래 모드를 전환하는 [버튼](https://user-images.githubusercontent.com/28893833/173663950-63e6610e-a532-49b7-9afa-54cb57ddfc15.png)을 추가합니다. 선택적으로 전체 영상 탭을 제거할 수도 있습니다
|
||||
|
||||
- **비주얼라이저**: 플레이어에 시각화 도구 추가
|
||||
|
||||
## 번역
|
||||
|
||||
[Hosted Weblate](https://hosted.weblate.org/projects/youtube-music/)에서 번역을 도울 수 있습니다.
|
||||
|
||||
<a href="https://hosted.weblate.org/engage/youtube-music/">
|
||||
<img src="https://hosted.weblate.org/widget/youtube-music/i18n/multi-auto.svg" alt="번역 상태" />
|
||||
<img src="https://hosted.weblate.org/widget/youtube-music/i18n/287x66-black.png" alt="번역 상태 2" />
|
||||
</a>
|
||||
|
||||
## 다운로드
|
||||
|
||||
[최신 릴리즈](https://github.com/th-ch/youtube-music/releases/latest)를 확인하여 최신 버전을 빠르게 찾을 수 있습니다.
|
||||
|
||||
### Arch Linux
|
||||
|
||||
AUR에서 [`youtube-music-bin`](https://aur.archlinux.org/packages/youtube-music-bin) 패키지를 설치합니다. AUR 설치 지침은 [이 위키 페이지](https://wiki.archlinux.org/index.php/Arch_User_Repository#Installing_packages)를 참조하세요.
|
||||
|
||||
### MacOS
|
||||
|
||||
Homebrew를 사용하여 앱을 설치할 수 있습니다:
|
||||
```bash
|
||||
brew install --cask https://raw.githubusercontent.com/th-ch/youtube-music/master/youtube-music.rb
|
||||
```
|
||||
|
||||
(앱을 수동으로 설치하고) 앱을 실행할 때 `손상되었기 때문에 열 수 없습니다.`라는 오류가 발생하면 터미널에서 다음을 실행하세요:
|
||||
|
||||
```bash
|
||||
xattr -cr /Applications/YouTube\ Music.app
|
||||
```
|
||||
|
||||
### Windows
|
||||
|
||||
[Scoop 패키지 매니저](https://scoop.sh)를 사용하여 [`extras` 버킷](https://github.com/ScoopInstaller/Extras)에서 `youtube-music` 패키지를 설치할 수 있습니다.
|
||||
|
||||
```bash
|
||||
scoop bucket add extras
|
||||
scoop install extras/youtube-music
|
||||
```
|
||||
|
||||
또는 Windows 11의 공식 CLI 패키지 관리자인 [Winget](https://learn.microsoft.com/en-us/windows/package-manager/winget/)을 사용하여 `th-ch.YouTubeMusic` 패키지를 설치할 수 있습니다.
|
||||
|
||||
*참고: "알 수 없는 게시자"의 파일이기 때문에 Microsoft Defender의 SmartScreen에서 설치를 차단할 수 있습니다. 이는 GitHub에서 동일 파일을 수동으로 다운로드한 후 실행 파일(.exe)을 실행하려고 할 때도 마찬가지로 발생합니다.*
|
||||
|
||||
```bash
|
||||
winget install th-ch.YouTubeMusic
|
||||
```
|
||||
|
||||
#### (Windows에서) 네트워크에 연결하지 않고 설치하는 방법은 무엇인가요?
|
||||
|
||||
- [릴리즈 페이지](https://github.com/th-ch/youtube-music/releases/latest)에서 _본인 기기 아키텍처_에 맞는 `*.nsis.7z` 파일을 다운로드하세요.
|
||||
- `x64`는 64비트 Windows 용입니다.
|
||||
- `ia32`는 32비트 Windows 용입니다.
|
||||
- `arm64`는 ARM64 Windows 용입니다.
|
||||
- 릴리즈 페이지에서 설치기를 다운로드하세요. (`*-Setup.exe`)
|
||||
- 두 파일을 **동일한 위치**에 놓아주세요.
|
||||
- 설치기를 실행하세요.
|
||||
|
||||
## 테마
|
||||
|
||||
CSS 파일을 로드하여 애플리케이션의 모양을 변경할 수 있습니다(설정 > 시각적 변경 > 테마).
|
||||
|
||||
일부 사전 정의 테마는 https://github.com/kerichdev/themes-for-ytmdesktop-player 에서 사용할 수 있습니다.
|
||||
|
||||
## 개발
|
||||
|
||||
```bash
|
||||
git clone https://github.com/th-ch/youtube-music
|
||||
cd youtube-music
|
||||
pnpm install --frozen-lockfile
|
||||
pnpm dev
|
||||
```
|
||||
|
||||
## 나만의 플러그인 만들기
|
||||
|
||||
플러그인을 사용하면 할 수 있는 것들:
|
||||
|
||||
- 앱 조작 - Electron에서 `BrowserWindow`가 플러그인 핸들러로 전달
|
||||
- HTML/CSS를 조작하여 프론트엔드를 변경
|
||||
|
||||
### 플러그인 만들기
|
||||
|
||||
`plugins/나만의-플러그인-이름`에 폴더를 만듭니다:
|
||||
|
||||
- `index.ts`: 플러그인의 메인 파일입니다.
|
||||
```typescript
|
||||
import style from './style.css?inline'; // 스타일을 인라인으로 가져옵니다
|
||||
|
||||
import { createPlugin } from '@/utils';
|
||||
|
||||
export default createPlugin({
|
||||
name: 'Plugin Label',
|
||||
restartNeeded: true, // 값이 true면, YTM은 재시작 다이얼로그를 표시합니다
|
||||
config: {
|
||||
enabled: false,
|
||||
}, // 나의 커스텀 config
|
||||
stylesheets: [style], // 나의 스타일
|
||||
menu: async ({ getConfig, setConfig }) => {
|
||||
// 모든 *Config 메서드는 Promise<T>로 래핑됩니다
|
||||
const config = await getConfig();
|
||||
return [
|
||||
{
|
||||
label: 'menu',
|
||||
submenu: [1, 2, 3].map((value) => ({
|
||||
label: `value ${value}`,
|
||||
type: 'radio',
|
||||
checked: config.value === value,
|
||||
click() {
|
||||
setConfig({ value });
|
||||
},
|
||||
})),
|
||||
},
|
||||
];
|
||||
},
|
||||
backend: {
|
||||
start({ window, ipc }) {
|
||||
window.maximize();
|
||||
|
||||
// 이를 사용하여 렌더러 플러그인과 통신할 수 있습니다
|
||||
ipc.handle('some-event', () => {
|
||||
return 'hello';
|
||||
});
|
||||
},
|
||||
// config가 변경되면 실행됩니다
|
||||
onConfigChange(newConfig) { /* ... */ },
|
||||
// 플러그인이 비활성화되면 실행됩니다
|
||||
stop(context) { /* ... */ },
|
||||
},
|
||||
renderer: {
|
||||
async start(context) {
|
||||
console.log(await context.ipc.invoke('some-event'));
|
||||
},
|
||||
// 렌더러에서만 사용 가능한 훅입니다
|
||||
onPlayerApiReady(api: YoutubePlayer, context: RendererContext<T>) {
|
||||
// 플러그인의 config를 간단하게 설정할 수 있습니다
|
||||
context.setConfig({ myConfig: api.getVolume() });
|
||||
},
|
||||
onConfigChange(newConfig) { /* ... */ },
|
||||
stop(_context) { /* ... */ },
|
||||
},
|
||||
preload: {
|
||||
async start({ getConfig }) {
|
||||
const config = await getConfig();
|
||||
},
|
||||
onConfigChange(newConfig) {},
|
||||
stop(_context) {},
|
||||
},
|
||||
});
|
||||
```
|
||||
|
||||
### 일반적인 사용 예
|
||||
|
||||
- 사용자 정의 CSS 삽입: 같은 폴더에 `style.css` 파일을 생성합니다:
|
||||
|
||||
```typescript
|
||||
// index.ts
|
||||
import style from './style.css?inline'; // 스타일을 인라인으로 가져옵니다
|
||||
|
||||
import { createPlugin } from '@/utils';
|
||||
|
||||
export default createPlugin({
|
||||
name: 'Plugin Label',
|
||||
restartNeeded: true, // 값이 true면, YTM은 재시작 다이얼로그를 표시합니다
|
||||
config: {
|
||||
enabled: false,
|
||||
}, // 나의 커스텀 config
|
||||
stylesheets: [style], // 나의 커스텀 스타일
|
||||
renderer() {} // 렌더러 훅 정의
|
||||
});
|
||||
```
|
||||
|
||||
- HTML을 변경하려는 경우:
|
||||
|
||||
```typescript
|
||||
import { createPlugin } from '@/utils';
|
||||
|
||||
export default createPlugin({
|
||||
name: 'Plugin Label',
|
||||
restartNeeded: true, // 값이 true면, YTM은 재시작 다이얼로그를 표시합니다
|
||||
config: {
|
||||
enabled: false,
|
||||
}, // 나의 커스텀 config
|
||||
renderer() {
|
||||
// 로그인 버튼을 제거합니다
|
||||
document.querySelector(".sign-in-link.ytmusic-nav-bar").remove();
|
||||
} // 렌더러 훅 정의
|
||||
});
|
||||
```
|
||||
|
||||
- 프론트엔드와 백엔드 간의 통신: Electron의 `ipcMain` 모듈을 사용하여 수행할 수 있습니다. `SponsorBlock` 플러그인의 `index.ts` 파일과 예제를 참조하세요.
|
||||
|
||||
## 빌드
|
||||
|
||||
1. 레포지토리를 복제 (clone) 합니다
|
||||
2. [이 가이드](https://pnpm.io/installation)에 따라 `pnpm`을 설치합니다.
|
||||
3. `pnpm install --frozen-lockfile`을 실행하여 종속성을 설치합니다.
|
||||
4. `pnpm build:OS`을 실행합니다.
|
||||
|
||||
- `pnpm dist:win` - Windows
|
||||
- `pnpm dist:linux` - Linux
|
||||
- `pnpm dist:mac` - MacOS
|
||||
|
||||
[electron-builder](https://github.com/electron-userland/electron-builder)를 사용하여 macOS, Linux 및 Windows용 앱을 빌드합니다.
|
||||
|
||||
## 프로덕션 빌드 미리보기
|
||||
|
||||
```bash
|
||||
pnpm start
|
||||
```
|
||||
|
||||
## 테스트
|
||||
|
||||
```bash
|
||||
pnpm test
|
||||
```
|
||||
|
||||
[Playwright](https://playwright.dev/)를 사용하여 앱을 테스트합니다.
|
||||
|
||||
## 라이선스
|
||||
|
||||
MIT © [th-ch](https://github.com/th-ch/youtube-music)
|
||||
|
||||
## 자주 묻는 질문
|
||||
|
||||
### 앱 메뉴가 표시되지 않는 이유는 무엇인가요?
|
||||
|
||||
`메뉴 숨기기` 옵션이 켜져 있는 경우 - <kbd>alt</kbd> 키(또는 인앱 메뉴 플러그인을 사용하는 경우 <kbd>\`</kbd> [백틱] 키)로 메뉴를 표시할 수 있습니다.
|
||||
374
docs/readme/README-ru.md
Normal file
374
docs/readme/README-ru.md
Normal file
@ -0,0 +1,374 @@
|
||||
<div align="center">
|
||||
|
||||
# YouTube Music
|
||||
|
||||
[](https://github.com/th-ch/youtube-music/releases/)
|
||||
[](https://github.com/th-ch/youtube-music/blob/master/LICENSE)
|
||||
[](https://github.com/th-ch/youtube-music/blob/master/.eslintrc.js)
|
||||
[](https://GitHub.com/th-ch/youtube-music/releases/)
|
||||
[](https://GitHub.com/th-ch/youtube-music/releases/)
|
||||
[](https://aur.archlinux.org/packages/youtube-music-bin)
|
||||
[](https://snyk.io/test/github/th-ch/youtube-music)
|
||||
|
||||
</div>
|
||||
|
||||

|
||||
|
||||
|
||||
<div align="center">
|
||||
<a href="https://github.com/th-ch/youtube-music/releases/latest">
|
||||
<img src="web/youtube-music.svg" width="400" height="100" alt="YouTube Music SVG">
|
||||
</a>
|
||||
</div>
|
||||
|
||||
|
||||
|
||||
**Клиент для YouTube Music основанный на Electron с поддержкой:**
|
||||
|
||||
- Нативный вид приложения, нацелен на сохранение оригинального интерфейса
|
||||
- Фреймворк для пользовательских плагинов: изменяйте YouTube Music под ваши нужды (внешний вид, контент, возможности), включайте/выключайте плагины в один клик
|
||||
|
||||
## Демо-изображение
|
||||
|
||||
| Экран плеера (цветовая тема альбома & режим Ambient) |
|
||||
|:---------------------------------------------------------------------------------------------------------:|
|
||||
||
|
||||
|
||||
## Содержание
|
||||
|
||||
- [Возможности](#features)
|
||||
- [Доступные плагины](#available-plugins)
|
||||
- [Перевод](#translation)
|
||||
- [Скачать](#download)
|
||||
- [Arch Linux](#arch-linux)
|
||||
- [MacOS](#macos)
|
||||
- [Windows](#windows)
|
||||
- [Как установить без подключения к интернету? (в Windows)](#how-to-install-without-a-network-connection-in-windows)
|
||||
- [Темы](#themes)
|
||||
- [Для разработчиков](#dev)
|
||||
- [Создайте свои собственные плагины](#build-your-own-plugins)
|
||||
- [Создание плагина](#creating-a-plugin)
|
||||
- [Примеры использования](#common-use-cases)
|
||||
- [Сборка](#build)
|
||||
- [Предварительный просмотр](#production-preview)
|
||||
- [Тестирование](#tests)
|
||||
- [Лицензия](#license)
|
||||
- [Часто задаваемые вопросы](#faq)
|
||||
|
||||
## Возможности:
|
||||
|
||||
- **Авто-подтверждение при паузе** (Всегда включено): отключает всплывающие уведомление ["Продолжить просмотр?"](https://user-images.githubusercontent.com/61631665/129977894-01c60740-7ec6-4bf0-9a2c-25da24491b0e.png),
|
||||
которое приостанавливает воспроизведение через определённое время
|
||||
|
||||
- И больше ...
|
||||
|
||||
## Доступные плагины:
|
||||
|
||||
- **Блокировщик рекламы**: Блокирует всю рекламу и трекеры
|
||||
|
||||
- **Действия с альбомом**: Добавляет кнопки "Убрать дизлайк", "Дизлайк", "Лайк", "Убрать лайк" и применяет их действия ко всем трекам в плейлисте или альбоме
|
||||
|
||||
- **Цветовая тема альбома**: Применяет динамическую тему и эффекты, основываясь на цветовой палитре альбома
|
||||
|
||||
- **Режим Ambient**: Применяет световой эффект, проецируя нежные цвета из видео на задний фон вашего экрана
|
||||
|
||||
- **Нормализация аудио**: Применяет нормализацию к аудио (уменьшает громкость громких частей трека и повышает громкость тихих частей трека)
|
||||
|
||||
- **Размытие панели навигации**: Делает панель навигации прозрачной и размытой
|
||||
|
||||
- **Обход возрастных ограничений**: Обходит проверку возраста YouTube
|
||||
|
||||
- **Выбор субтитров**: Включить субтитры
|
||||
|
||||
- **Компактная боковая панель**: Всегда показывать боковую панель компактно
|
||||
|
||||
- **Плавный переход**: Плавный переход между треками
|
||||
|
||||
- **Отключить автопроигрыш**: Каждый трек начинается в режиме паузы
|
||||
|
||||
- **[Discord](https://discord.com/) Rich Presence**: Показывает вашим друзьям, что вы слушаете с помощью [Rich Presence](https://user-images.githubusercontent.com/28219076/104362104-a7a0b980-5513-11eb-9744-bb89eabe0016.png)
|
||||
|
||||
- **Загрузчик**: Загрузка MP3 [напрямую из интерфейса](https://user-images.githubusercontent.com/61631665/129977677-83a7d067-c192-45e1-98ae-b5a4927393be.png) [(youtube-dl)](https://github.com/ytdl-org/youtube-dl)
|
||||
|
||||
- **Расширенная громкость**: Делает слайдер громкости [расширенным](https://greasyfork.org/en/scripts/397686-youtube-music-fix-volume-ratio/) облегчая выбор громкости
|
||||
|
||||
- **Меню в приложении**: [Придаёт панели меню красивый тёмный вид](https://user-images.githubusercontent.com/78568641/112215894-923dbf00-8c29-11eb-95c3-3ce15db27eca.png)
|
||||
|
||||
> (посмотрите [этот пост,](https://github.com/th-ch/youtube-music/issues/410#issuecomment-952060709) если у вас есть проблемы с доступом к меню после включения этого плагина и опции "Скрыть меню")
|
||||
|
||||
- **Скробблер**: Добавляет поддержку скробблинга [Last.fm](https://www.last.fm/) и [ListenBrainz](https://listenbrainz.org/)
|
||||
|
||||
- **Lumia Stream**: Добавляет поддержку [Lumia Stream](https://lumiastream.com/)
|
||||
|
||||
- **Тесты песен Genius**: Добавляет поддержку текстов для большинства песен
|
||||
|
||||
- **Music Together**: Делитесь плейлистом с другими. Когда ведущий воспроизводит трек, все остальные будут слушать этот же трек.
|
||||
|
||||
- **Навигация**: Кнопки Назад/Вперед интегрированы в интерфейс, как в вашем любимом браузере
|
||||
|
||||
- **Без входа в систему Google**: Убирает из интерфейса кнопки и ссылки для входа через Google
|
||||
|
||||
- **Уведомления**: Показывает уведомление, когда трек начинает играть ([интерактивные уведомления](https://user-images.githubusercontent.com/78568641/114102651-63ce0e00-98d0-11eb-9dfe-c5a02bb54f9c.png) доступны только для Windows)
|
||||
|
||||
- **Картинка в картинке**: Позволяет переключить приложение в режим "картинка в картинке"
|
||||
|
||||
- **Скорость воспроизведения**: Слушайте быстрее, слушайте медленнее! [Добавляет слайдер для контроля скорости трека](https://user-images.githubusercontent.com/61631665/129976003-e55db5ba-bf42-448c-a059-26a009775e68.png)
|
||||
|
||||
- **Точная громкость**: Точечно управляйте громкостью с помощью колеса мыши/горячих клавиш, с кастомным интерфейсом и настраиваемыми шагами громкости
|
||||
|
||||
- **Ярлыки (и MPRIS)**: Позволяет настроить глобальные горячие клавиши управления воспроизведением (плей/пауза/следующий/предыдущий) + отключает [отображение медиа на экране,](https://user-images.githubusercontent.com/84923831/128601225-afa38c1f-dea8-4209-9f72-0f84c1dd8b54.png) переопределяя клавиши управления + включает Ctrl/CMD + F для поиска + включает поддержку linux mpris для клавиш управления медиа + [настраиваемые сочетания клавиш](https://github.com/Araxeus/youtube-music/blob/1e591d6a3df98449bcda6e63baab249b28026148/providers/song-controls.js#L13-L50) для [продвинутых пользователей](https://github.com/th-ch/youtube-music/issues/106#issuecomment-952156902)
|
||||
|
||||
- **Пропускать непонравившиеся треки**: Пропускает непонравившиеся треки
|
||||
|
||||
- **Пропуск тишины**: Автоматически пропускает тихие моменты в песнях
|
||||
|
||||
- [**SponsorBlock**](https://github.com/ajayyy/SponsorBlock): Автоматически пропускает немузыкальные части, такие как интро/аутро музыкальных видео, где трек не играет
|
||||
|
||||
- **Управление воспроизведением из панели задач**: Управляйте воспроизведением из [панели задач Windows](https://user-images.githubusercontent.com/78568641/111916130-24a35e80-8a82-11eb-80c8-5021c1aa27f4.png)
|
||||
|
||||
- **TouchBar**: Кастомная раскладка TouchBar для MacOS
|
||||
|
||||
- **Tuna OBS**: Интеграция с [OBS](https://obsproject.com/) плагином [Tuna](https://obsproject.com/forum/resources/tuna.843/)
|
||||
|
||||
- **Изменение качества видео**: Позволяет менять качество видео [кнопкой](https://user-images.githubusercontent.com/78568641/138574366-70324a5e-2d64-4f6a-acdd-dc2a2b9cecc5.png) на медиаплеере видео
|
||||
|
||||
- **Переключатель видео**: Добавляет
|
||||
[кнопку](https://user-images.githubusercontent.com/28893833/173663950-63e6610e-a532-49b7-9afa-54cb57ddfc15.png) переключения режимов Трек/Видео. Также может удалять вкладку "Видео" полностью
|
||||
|
||||
- **Визуализатор**: Различные визуализаторы музыки
|
||||
|
||||
- **Synced Lyrics**:
|
||||
Предоставляет синхронизированные слова для песен из таких источников, как [LRClib](https://lrclib.net).
|
||||
|
||||
## Перевод
|
||||
|
||||
Вы можете помочь с переводом на ваш язык на [Hosted Weblate](https://hosted.weblate.org/projects/youtube-music/).
|
||||
|
||||
<a href="https://hosted.weblate.org/engage/youtube-music/">
|
||||
<img src="https://hosted.weblate.org/widget/youtube-music/i18n/multi-auto.svg" alt="translation status" />
|
||||
<img src="https://hosted.weblate.org/widget/youtube-music/i18n/287x66-black.png" alt="translation status 2" />
|
||||
</a>
|
||||
|
||||
## Скачать
|
||||
|
||||
Вы можете посмотреть [latest release,](https://github.com/th-ch/youtube-music/releases/latest) чтобы быстро найти новую версию.
|
||||
|
||||
### Arch Linux
|
||||
|
||||
Установите пакет [`youtube-music-bin`](https://aur.archlinux.org/packages/youtube-music-bin) из AUR. Инструкции по установке из AUR можете найти на этой [вики-странице](https://wiki.archlinux.org/index.php/Arch_User_Repository#Installing_packages).
|
||||
|
||||
### macOS
|
||||
|
||||
Вы можете установить приложение с помощью Homebrew (сморите [cask definition](https://github.com/th-ch/homebrew-youtube-music)):
|
||||
|
||||
```bash
|
||||
brew install th-ch/youtube-music/youtube-music
|
||||
```
|
||||
|
||||
Если вы устанавливаете приложение вручную и получаете ошибку "is damaged and can’t be opened.", запустите в терминале следующую команду:
|
||||
|
||||
```bash
|
||||
xattr -cr /Applications/YouTube\ Music.app
|
||||
```
|
||||
|
||||
### Windows
|
||||
|
||||
Вы можете использовать [пакетный менеджер Scoop](https://scoop.sh) для установки пакета `youtube-music` из [`extras` bucket](https://github.com/ScoopInstaller/Extras).
|
||||
|
||||
```bash
|
||||
scoop bucket add extras
|
||||
scoop install extras/youtube-music
|
||||
```
|
||||
|
||||
Также для установки вы можете использовать [Winget](https://learn.microsoft.com/en-us/windows/package-manager/winget/), официальный пакетный менеджер командной строки Windows 11, для установки пакета `th-ch.YouTubeMusic`.
|
||||
|
||||
*К сведению: SmartScreen защитника Windows может блокировать установку, так как она от "неизвестного издателя". Это также применимо к методу ручной установки, когда вы пытаетесь запустить исполняемый файл(.exe) после загрузки здесь, на GitHub (тот же файл).*
|
||||
|
||||
```bash
|
||||
winget install th-ch.YouTubeMusic
|
||||
```
|
||||
|
||||
#### Установка без подключения к Интернету? (в Windows)
|
||||
|
||||
- Скачайте файл `*.nsis.7z` из _архетиктура вашего устройства_ на [release page](https://github.com/th-ch/youtube-music/releases/latest).
|
||||
- `x64` для 64-bit Windows
|
||||
- `ia32` для 32-bit Windows
|
||||
- `arm64` для ARM64 Windows
|
||||
- Скачайте установщик в release page. (`*-Setup.exe`)
|
||||
- Поместите их в **одной директории**.
|
||||
- Запустите установщик.
|
||||
|
||||
## Темы
|
||||
|
||||
Вы можете загрузить файл CSS для смены внешнего вида приложения (Настройки > Визуальные настройки > Тема).
|
||||
|
||||
Некоторые предустановленные темы доступны здесь: https://github.com/kerichdev/themes-for-ytmdesktop-player.
|
||||
|
||||
## Для разработчиков
|
||||
|
||||
```bash
|
||||
git clone https://github.com/th-ch/youtube-music
|
||||
cd youtube-music
|
||||
pnpm install --frozen-lockfile
|
||||
pnpm dev
|
||||
```
|
||||
|
||||
## Создайте свои собственные плагины
|
||||
|
||||
Используя плагины вы можете:
|
||||
|
||||
- Манипулировать приложением - `BrowserWindow` из electron проброшен обработчику плагинов
|
||||
- Изменять внешний вид, манипулируя HTML/CSS
|
||||
|
||||
### Создание плагина
|
||||
|
||||
Создайте директорию в `src/plugins/YOUR-PLUGIN-NAME`:
|
||||
|
||||
- `index.ts`: основной файл плагина
|
||||
```typescript
|
||||
import style from './style.css?inline'; // import style as inline
|
||||
|
||||
import { createPlugin } from '@/utils';
|
||||
|
||||
export default createPlugin({
|
||||
name: 'Plugin Label',
|
||||
restartNeeded: true, // if value is true, ytmusic show restart dialog
|
||||
config: {
|
||||
enabled: false,
|
||||
}, // your custom config
|
||||
stylesheets: [style], // your custom style,
|
||||
menu: async ({ getConfig, setConfig }) => {
|
||||
// All *Config methods are wrapped Promise<T>
|
||||
const config = await getConfig();
|
||||
return [
|
||||
{
|
||||
label: 'menu',
|
||||
submenu: [1, 2, 3].map((value) => ({
|
||||
label: `value ${value}`,
|
||||
type: 'radio',
|
||||
checked: config.value === value,
|
||||
click() {
|
||||
setConfig({ value });
|
||||
},
|
||||
})),
|
||||
},
|
||||
];
|
||||
},
|
||||
backend: {
|
||||
start({ window, ipc }) {
|
||||
window.maximize();
|
||||
|
||||
// you can communicate with renderer plugin
|
||||
ipc.handle('some-event', () => {
|
||||
return 'hello';
|
||||
});
|
||||
},
|
||||
// it fired when config changed
|
||||
onConfigChange(newConfig) { /* ... */ },
|
||||
// it fired when plugin disabled
|
||||
stop(context) { /* ... */ },
|
||||
},
|
||||
renderer: {
|
||||
async start(context) {
|
||||
console.log(await context.ipc.invoke('some-event'));
|
||||
},
|
||||
// Only renderer available hook
|
||||
onPlayerApiReady(api: YoutubePlayer, context: RendererContext) {
|
||||
// set plugin config easily
|
||||
context.setConfig({ myConfig: api.getVolume() });
|
||||
},
|
||||
onConfigChange(newConfig) { /* ... */ },
|
||||
stop(_context) { /* ... */ },
|
||||
},
|
||||
preload: {
|
||||
async start({ getConfig }) {
|
||||
const config = await getConfig();
|
||||
},
|
||||
onConfigChange(newConfig) {},
|
||||
stop(_context) {},
|
||||
},
|
||||
});
|
||||
```
|
||||
|
||||
### Примеры использования
|
||||
|
||||
- Кастомный CSS: создайте файл `style.css` в той же директории, затем:
|
||||
|
||||
```typescript
|
||||
// index.ts
|
||||
import style from './style.css?inline'; // import style as inline
|
||||
|
||||
import { createPlugin } from '@/utils';
|
||||
|
||||
export default createPlugin({
|
||||
name: 'Plugin Label',
|
||||
restartNeeded: true, // if value is true, ytmusic will show a restart dialog
|
||||
config: {
|
||||
enabled: false,
|
||||
}, // your custom config
|
||||
stylesheets: [style], // your custom style
|
||||
renderer() {} // define renderer hook
|
||||
});
|
||||
```
|
||||
|
||||
- Если вы хотите изменить HTML:
|
||||
|
||||
```typescript
|
||||
import { createPlugin } from '@/utils';
|
||||
|
||||
export default createPlugin({
|
||||
name: 'Plugin Label',
|
||||
restartNeeded: true, // if value is true, ytmusic will show the restart dialog
|
||||
config: {
|
||||
enabled: false,
|
||||
}, // your custom config
|
||||
renderer() {
|
||||
// Remove the login button
|
||||
document.querySelector(".sign-in-link.ytmusic-nav-bar").remove();
|
||||
} // define renderer hook
|
||||
});
|
||||
```
|
||||
|
||||
- обмен между фронтом и бэком может быть выполнен с помощью модуля ipcMain из electron. Смотрите файл `index.ts` и
|
||||
пример в плагине `sponsorblock`.
|
||||
|
||||
## Сборка
|
||||
|
||||
1. Склонируйте репозиторий
|
||||
2. Следуйте [этой инструкции,](https://pnpm.io/installation) чтобы установить `pnpm`
|
||||
3. Запустите `pnpm install --frozen-lockfile` для установки зависимостей
|
||||
4. Запустите `pnpm build:OS`
|
||||
|
||||
- `pnpm dist:win` - Windows
|
||||
- `pnpm dist:linux` - Linux (amd64)
|
||||
- `pnpm dist:linux:deb-arm64` - Linux (arm64 for Debian)
|
||||
- `pnpm dist:linux:rpm-arm64` - Linux (arm64 for Fedora)
|
||||
- `pnpm dist:mac` - macOS (amd64)
|
||||
- `pnpm dist:mac:arm64` - macOS (arm64)
|
||||
|
||||
Сборка приложения для macOS, Linux, и Windows,
|
||||
используя [electron-builder](https://github.com/electron-userland/electron-builder).
|
||||
|
||||
## Предварительный просмотр
|
||||
|
||||
```bash
|
||||
pnpm start
|
||||
```
|
||||
|
||||
## Тестирование
|
||||
|
||||
```bash
|
||||
pnpm test
|
||||
```
|
||||
|
||||
Использует [Playwright](https://playwright.dev/) для тестирования приложения.
|
||||
|
||||
## Лицензия
|
||||
|
||||
MIT © [th-ch](https://github.com/th-ch/youtube-music)
|
||||
|
||||
## Часто задаваемые вопросы
|
||||
|
||||
### Почему меня приложения не отображается?
|
||||
|
||||
Если опция `Скрыть меню` включена - вы можете отобразить меню с помощью клавиши <kbd>alt</kbd> (или <kbd>\`</kbd> [обратный апостроф], если используете плагин "Меню в приложении")
|
||||
172
electron.vite.config.mts
Normal file
172
electron.vite.config.mts
Normal file
@ -0,0 +1,172 @@
|
||||
import { resolve, dirname, join } from 'node:path';
|
||||
import { fileURLToPath } from 'node:url';
|
||||
|
||||
import { UserConfig } from 'vite';
|
||||
import { defineConfig, defineViteConfig } from 'electron-vite';
|
||||
import builtinModules from 'builtin-modules';
|
||||
import viteResolve from 'vite-plugin-resolve';
|
||||
import Inspect from 'vite-plugin-inspect';
|
||||
import solidPlugin from 'vite-plugin-solid';
|
||||
|
||||
import { pluginVirtualModuleGenerator } from './vite-plugins/plugin-importer.mjs';
|
||||
import pluginLoader from './vite-plugins/plugin-loader.mjs';
|
||||
|
||||
import { i18nImporter } from './vite-plugins/i18n-importer.mjs';
|
||||
|
||||
const __dirname = dirname(fileURLToPath(import.meta.url));
|
||||
|
||||
const resolveAlias = {
|
||||
'@': resolve(__dirname, './src'),
|
||||
'@assets': resolve(__dirname, './assets'),
|
||||
};
|
||||
|
||||
export default defineConfig({
|
||||
main: defineViteConfig(({ mode }) => {
|
||||
const commonConfig: UserConfig = {
|
||||
plugins: [
|
||||
pluginLoader('backend'),
|
||||
viteResolve({
|
||||
'virtual:i18n': i18nImporter(),
|
||||
'virtual:plugins': pluginVirtualModuleGenerator('main'),
|
||||
}),
|
||||
],
|
||||
publicDir: 'assets',
|
||||
build: {
|
||||
lib: {
|
||||
entry: 'src/index.ts',
|
||||
formats: ['cjs'],
|
||||
},
|
||||
outDir: 'dist/main',
|
||||
commonjsOptions: {
|
||||
ignoreDynamicRequires: true,
|
||||
},
|
||||
rollupOptions: {
|
||||
external: ['electron', 'custom-electron-prompt', ...builtinModules],
|
||||
input: './src/index.ts',
|
||||
},
|
||||
},
|
||||
resolve: {
|
||||
alias: resolveAlias,
|
||||
},
|
||||
};
|
||||
|
||||
if (mode === 'development') {
|
||||
commonConfig.build!.sourcemap = 'inline';
|
||||
commonConfig.plugins?.push(
|
||||
Inspect({
|
||||
build: true,
|
||||
outputDir: join(__dirname, '.vite-inspect/backend'),
|
||||
}),
|
||||
);
|
||||
return commonConfig;
|
||||
}
|
||||
|
||||
return {
|
||||
...commonConfig,
|
||||
build: {
|
||||
...commonConfig.build,
|
||||
minify: true,
|
||||
cssMinify: true,
|
||||
},
|
||||
};
|
||||
}),
|
||||
preload: defineViteConfig(({ mode }) => {
|
||||
const commonConfig: UserConfig = {
|
||||
plugins: [
|
||||
pluginLoader('preload'),
|
||||
viteResolve({
|
||||
'virtual:i18n': i18nImporter(),
|
||||
'virtual:plugins': pluginVirtualModuleGenerator('preload'),
|
||||
}),
|
||||
],
|
||||
build: {
|
||||
lib: {
|
||||
entry: 'src/preload.ts',
|
||||
formats: ['cjs'],
|
||||
},
|
||||
outDir: 'dist/preload',
|
||||
commonjsOptions: {
|
||||
ignoreDynamicRequires: true,
|
||||
},
|
||||
rollupOptions: {
|
||||
external: ['electron', 'custom-electron-prompt', ...builtinModules],
|
||||
input: './src/preload.ts',
|
||||
},
|
||||
},
|
||||
resolve: {
|
||||
alias: resolveAlias,
|
||||
},
|
||||
};
|
||||
|
||||
if (mode === 'development') {
|
||||
commonConfig.build!.sourcemap = 'inline';
|
||||
commonConfig.plugins?.push(
|
||||
Inspect({
|
||||
build: true,
|
||||
outputDir: join(__dirname, '.vite-inspect/preload'),
|
||||
}),
|
||||
);
|
||||
return commonConfig;
|
||||
}
|
||||
|
||||
return {
|
||||
...commonConfig,
|
||||
build: {
|
||||
...commonConfig.build,
|
||||
minify: true,
|
||||
cssMinify: true,
|
||||
},
|
||||
};
|
||||
}),
|
||||
renderer: defineViteConfig(({ mode }) => {
|
||||
const commonConfig: UserConfig = {
|
||||
plugins: [
|
||||
pluginLoader('renderer'),
|
||||
viteResolve({
|
||||
'virtual:i18n': i18nImporter(),
|
||||
'virtual:plugins': pluginVirtualModuleGenerator('renderer'),
|
||||
}),
|
||||
solidPlugin(),
|
||||
],
|
||||
root: './src/',
|
||||
build: {
|
||||
lib: {
|
||||
entry: 'src/index.html',
|
||||
formats: ['iife'],
|
||||
name: 'renderer',
|
||||
},
|
||||
outDir: 'dist/renderer',
|
||||
commonjsOptions: {
|
||||
ignoreDynamicRequires: true,
|
||||
},
|
||||
rollupOptions: {
|
||||
external: ['electron', ...builtinModules],
|
||||
input: './src/index.html',
|
||||
},
|
||||
},
|
||||
resolve: {
|
||||
alias: resolveAlias,
|
||||
},
|
||||
};
|
||||
|
||||
if (mode === 'development') {
|
||||
commonConfig.build!.sourcemap = 'inline';
|
||||
commonConfig.plugins?.push(
|
||||
Inspect({
|
||||
build: true,
|
||||
outputDir: join(__dirname, '.vite-inspect/renderer'),
|
||||
}),
|
||||
);
|
||||
return commonConfig;
|
||||
}
|
||||
|
||||
return {
|
||||
...commonConfig,
|
||||
build: {
|
||||
...commonConfig.build,
|
||||
minify: true,
|
||||
cssMinify: true,
|
||||
},
|
||||
};
|
||||
}),
|
||||
});
|
||||
50
error.html
50
error.html
@ -1,50 +0,0 @@
|
||||
<!DOCTYPE html>
|
||||
<html>
|
||||
<head>
|
||||
<meta charset="utf-8"/>
|
||||
<title>Cannot load YouTube Music</title>
|
||||
<style>
|
||||
body {
|
||||
background: #000;
|
||||
}
|
||||
|
||||
.container {
|
||||
margin: 0;
|
||||
font-family: Roboto, Arial, sans-serif;
|
||||
font-size: 20px;
|
||||
font-weight: 500;
|
||||
color: rgba(255, 255, 255, 0.5);
|
||||
position: absolute;
|
||||
top: 50%;
|
||||
left: 50%;
|
||||
margin-right: -50%;
|
||||
transform: translate(-50%, -50%);
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.button {
|
||||
background: #065fd4;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
color: white;
|
||||
font: inherit;
|
||||
text-transform: uppercase;
|
||||
text-decoration: none;
|
||||
border-radius: 2px;
|
||||
font-size: 16px;
|
||||
font-weight: normal;
|
||||
text-align: center;
|
||||
padding: 8px 22px;
|
||||
display: inline-block;
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
|
||||
<body>
|
||||
<div class="container">
|
||||
<p>Cannot load YouTube Music… Internet disconnected?</p>
|
||||
<a class="button" href="#" onclick="reload()">Retry</a>
|
||||
</div>
|
||||
</body>
|
||||
</html>
|
||||
639
index.ts
639
index.ts
@ -1,639 +0,0 @@
|
||||
import path from 'node:path';
|
||||
|
||||
import { BrowserWindow, app, screen, globalShortcut, session, shell, dialog, ipcMain } from 'electron';
|
||||
import enhanceWebRequest, { BetterSession } from '@jellybrick/electron-better-web-request';
|
||||
import is from 'electron-is';
|
||||
import unhandled from 'electron-unhandled';
|
||||
import { autoUpdater } from 'electron-updater';
|
||||
import electronDebug from 'electron-debug';
|
||||
|
||||
import config from './config';
|
||||
import { refreshMenu, setApplicationMenu } from './menu';
|
||||
import { fileExists, injectCSS, injectCSSAsFile } from './plugins/utils';
|
||||
import { isTesting } from './utils/testing';
|
||||
import { setUpTray } from './tray';
|
||||
import { setupSongInfo } from './providers/song-info';
|
||||
import { restart, setupAppControls } from './providers/app-controls';
|
||||
import { APP_PROTOCOL, handleProtocol, setupProtocolHandler } from './providers/protocol-handler';
|
||||
|
||||
import adblocker from './plugins/adblocker/back';
|
||||
import albumColorTheme from './plugins/album-color-theme/back';
|
||||
import ambientMode from './plugins/ambient-mode/back';
|
||||
import blurNavigationBar from './plugins/blur-nav-bar/back';
|
||||
import captionsSelector from './plugins/captions-selector/back';
|
||||
import crossfade from './plugins/crossfade/back';
|
||||
import discord from './plugins/discord/back';
|
||||
import downloader from './plugins/downloader/back';
|
||||
import inAppMenu from './plugins/in-app-menu/back';
|
||||
import lastFm from './plugins/last-fm/back';
|
||||
import lumiaStream from './plugins/lumiastream/back';
|
||||
import lyricsGenius from './plugins/lyrics-genius/back';
|
||||
import navigation from './plugins/navigation/back';
|
||||
import noGoogleLogin from './plugins/no-google-login/back';
|
||||
import notifications from './plugins/notifications/back';
|
||||
import pictureInPicture, { setOptions as pipSetOptions } from './plugins/picture-in-picture/back';
|
||||
import preciseVolume from './plugins/precise-volume/back';
|
||||
import qualityChanger from './plugins/quality-changer/back';
|
||||
import shortcuts from './plugins/shortcuts/back';
|
||||
import sponsorBlock from './plugins/sponsorblock/back';
|
||||
import taskbarMediaControl from './plugins/taskbar-mediacontrol/back';
|
||||
import touchbar from './plugins/touchbar/back';
|
||||
import tunaObs from './plugins/tuna-obs/back';
|
||||
import videoToggle from './plugins/video-toggle/back';
|
||||
import visualizer from './plugins/visualizer/back';
|
||||
|
||||
import youtubeMusicCSS from './youtube-music.css';
|
||||
|
||||
// Catch errors and log them
|
||||
unhandled({
|
||||
logger: console.error,
|
||||
showDialog: false,
|
||||
});
|
||||
|
||||
// Disable Node options if the env var is set
|
||||
process.env.NODE_OPTIONS = '';
|
||||
|
||||
// Prevent window being garbage collected
|
||||
let mainWindow: Electron.BrowserWindow | null;
|
||||
autoUpdater.autoDownload = false;
|
||||
|
||||
const gotTheLock = app.requestSingleInstanceLock();
|
||||
if (!gotTheLock) {
|
||||
app.exit();
|
||||
}
|
||||
|
||||
// SharedArrayBuffer: Required for downloader (@ffmpeg/core-mt)
|
||||
// OverlayScrollbar: Required for overlay scrollbars
|
||||
app.commandLine.appendSwitch('enable-features', 'OverlayScrollbar,SharedArrayBuffer');
|
||||
if (config.get('options.disableHardwareAcceleration')) {
|
||||
if (is.dev()) {
|
||||
console.log('Disabling hardware acceleration');
|
||||
}
|
||||
|
||||
app.disableHardwareAcceleration();
|
||||
}
|
||||
|
||||
if (is.linux() && config.plugins.isEnabled('shortcuts')) {
|
||||
// Stops chromium from launching its own MPRIS service
|
||||
app.commandLine.appendSwitch('disable-features', 'MediaSessionService');
|
||||
}
|
||||
|
||||
if (config.get('options.proxy')) {
|
||||
app.commandLine.appendSwitch('proxy-server', config.get('options.proxy'));
|
||||
}
|
||||
|
||||
// Adds debug features like hotkeys for triggering dev tools and reload
|
||||
electronDebug({
|
||||
showDevTools: false, // Disable automatic devTools on new window
|
||||
});
|
||||
|
||||
let icon = 'assets/youtube-music.png';
|
||||
if (process.platform === 'win32') {
|
||||
icon = 'assets/generated/icon.ico';
|
||||
} else if (process.platform === 'darwin') {
|
||||
icon = 'assets/generated/icon.icns';
|
||||
}
|
||||
|
||||
function onClosed() {
|
||||
// Dereference the window
|
||||
// For multiple Windows store them in an array
|
||||
mainWindow = null;
|
||||
}
|
||||
|
||||
const mainPlugins = {
|
||||
'adblocker': adblocker,
|
||||
'album-color-theme': albumColorTheme,
|
||||
'ambient-mode': ambientMode,
|
||||
'blur-nav-bar': blurNavigationBar,
|
||||
'captions-selector': captionsSelector,
|
||||
'crossfade': crossfade,
|
||||
'discord': discord,
|
||||
'downloader': downloader,
|
||||
'in-app-menu': inAppMenu,
|
||||
'last-fm': lastFm,
|
||||
'lumiastream': lumiaStream,
|
||||
'lyrics-genius': lyricsGenius,
|
||||
'navigation': navigation,
|
||||
'no-google-login': noGoogleLogin,
|
||||
'notifications': notifications,
|
||||
'picture-in-picture': pictureInPicture,
|
||||
'precise-volume': preciseVolume,
|
||||
'quality-changer': qualityChanger,
|
||||
'shortcuts': shortcuts,
|
||||
'sponsorblock': sponsorBlock,
|
||||
'taskbar-mediacontrol': undefined as typeof taskbarMediaControl | undefined,
|
||||
'touchbar': undefined as typeof touchbar | undefined,
|
||||
'tuna-obs': tunaObs,
|
||||
'video-toggle': videoToggle,
|
||||
'visualizer': visualizer,
|
||||
};
|
||||
export const mainPluginNames = Object.keys(mainPlugins);
|
||||
|
||||
if (is.windows()) {
|
||||
mainPlugins['taskbar-mediacontrol'] = taskbarMediaControl;
|
||||
delete mainPlugins['touchbar'];
|
||||
} else if (is.macOS()) {
|
||||
mainPlugins['touchbar'] = touchbar;
|
||||
delete mainPlugins['taskbar-mediacontrol'];
|
||||
} else {
|
||||
delete mainPlugins['touchbar'];
|
||||
delete mainPlugins['taskbar-mediacontrol'];
|
||||
}
|
||||
|
||||
ipcMain.handle('get-main-plugin-names', () => Object.keys(mainPlugins));
|
||||
|
||||
async function loadPlugins(win: BrowserWindow) {
|
||||
injectCSS(win.webContents, youtubeMusicCSS);
|
||||
// Load user CSS
|
||||
const themes: string[] = config.get('options.themes');
|
||||
if (Array.isArray(themes)) {
|
||||
for (const cssFile of themes) {
|
||||
fileExists(
|
||||
cssFile,
|
||||
() => {
|
||||
injectCSSAsFile(win.webContents, cssFile);
|
||||
},
|
||||
() => {
|
||||
console.warn(`CSS file "${cssFile}" does not exist, ignoring`);
|
||||
},
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
win.webContents.once('did-finish-load', () => {
|
||||
if (is.dev()) {
|
||||
console.log('did finish load');
|
||||
win.webContents.openDevTools();
|
||||
}
|
||||
});
|
||||
|
||||
for (const [plugin, options] of config.plugins.getEnabled()) {
|
||||
try {
|
||||
if (Object.hasOwn(mainPlugins, plugin)) {
|
||||
console.log('Loaded plugin - ' + plugin);
|
||||
const handler = mainPlugins[plugin as keyof typeof mainPlugins];
|
||||
if (handler) {
|
||||
await handler(win, options as never);
|
||||
}
|
||||
}
|
||||
} catch (e) {
|
||||
console.error(`Failed to load plugin "${plugin}"`, e);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
async function createMainWindow() {
|
||||
const windowSize = config.get('window-size');
|
||||
const windowMaximized = config.get('window-maximized');
|
||||
const windowPosition: Electron.Point = config.get('window-position');
|
||||
const useInlineMenu = config.plugins.isEnabled('in-app-menu');
|
||||
|
||||
const win = new BrowserWindow({
|
||||
icon,
|
||||
width: windowSize.width,
|
||||
height: windowSize.height,
|
||||
backgroundColor: '#000',
|
||||
show: false,
|
||||
webPreferences: {
|
||||
// TODO: re-enable contextIsolation once it can work with FFMpeg.wasm
|
||||
// Possible bundling? https://github.com/ffmpegwasm/ffmpeg.wasm/issues/126
|
||||
contextIsolation: false,
|
||||
preload: path.join(__dirname, 'preload.js'),
|
||||
nodeIntegrationInSubFrames: true,
|
||||
...(isTesting()
|
||||
? undefined
|
||||
: {
|
||||
// Sandbox is only enabled in tests for now
|
||||
// See https://www.electronjs.org/docs/latest/tutorial/sandbox#preload-scripts
|
||||
sandbox: false,
|
||||
}),
|
||||
},
|
||||
frame: !is.macOS() && !useInlineMenu,
|
||||
titleBarOverlay: {
|
||||
color: '#00000000',
|
||||
symbolColor: '#ffffff',
|
||||
height: 36,
|
||||
},
|
||||
titleBarStyle: useInlineMenu
|
||||
? 'hidden'
|
||||
: (is.macOS()
|
||||
? 'hiddenInset'
|
||||
: 'default'),
|
||||
autoHideMenuBar: config.get('options.hideMenu'),
|
||||
});
|
||||
await loadPlugins(win);
|
||||
|
||||
if (windowPosition) {
|
||||
const { x: windowX, y: windowY } = windowPosition;
|
||||
const winSize = win.getSize();
|
||||
const displaySize
|
||||
= screen.getDisplayNearestPoint(windowPosition).bounds;
|
||||
if (
|
||||
windowX + winSize[0] < displaySize.x - 8
|
||||
|| windowX - winSize[0] > displaySize.x + displaySize.width
|
||||
|| windowY < displaySize.y - 8
|
||||
|| windowY > displaySize.y + displaySize.height
|
||||
) {
|
||||
// Window is offscreen
|
||||
if (is.dev()) {
|
||||
console.log(
|
||||
`Window tried to render offscreen, windowSize=${String(winSize)}, displaySize=${String(displaySize)}, position=${String(windowPosition)}`,
|
||||
);
|
||||
}
|
||||
} else {
|
||||
win.setPosition(windowX, windowY);
|
||||
}
|
||||
}
|
||||
|
||||
if (windowMaximized) {
|
||||
win.maximize();
|
||||
}
|
||||
|
||||
if (config.get('options.alwaysOnTop')) {
|
||||
win.setAlwaysOnTop(true);
|
||||
}
|
||||
|
||||
const urlToLoad = config.get('options.resumeOnStart')
|
||||
? config.get('url')
|
||||
: config.defaultConfig.url;
|
||||
win.on('closed', onClosed);
|
||||
|
||||
type PiPOptions = typeof config.defaultConfig.plugins['picture-in-picture'];
|
||||
const setPiPOptions = config.plugins.isEnabled('picture-in-picture')
|
||||
// eslint-disable-next-line @typescript-eslint/no-var-requires
|
||||
? (key: string, value: unknown) => pipSetOptions({ [key]: value })
|
||||
: () => {};
|
||||
|
||||
win.on('move', () => {
|
||||
if (win.isMaximized()) {
|
||||
return;
|
||||
}
|
||||
|
||||
const position = win.getPosition();
|
||||
const isPiPEnabled: boolean
|
||||
= config.plugins.isEnabled('picture-in-picture')
|
||||
&& config.plugins.getOptions<PiPOptions>('picture-in-picture').isInPiP;
|
||||
if (!isPiPEnabled) {
|
||||
|
||||
lateSave('window-position', { x: position[0], y: position[1] });
|
||||
} else if (config.plugins.getOptions<PiPOptions>('picture-in-picture').savePosition) {
|
||||
lateSave('pip-position', position, setPiPOptions);
|
||||
}
|
||||
});
|
||||
|
||||
let winWasMaximized: boolean;
|
||||
|
||||
win.on('resize', () => {
|
||||
const windowSize = win.getSize();
|
||||
const isMaximized = win.isMaximized();
|
||||
|
||||
const isPiPEnabled
|
||||
= config.plugins.isEnabled('picture-in-picture')
|
||||
&& config.plugins.getOptions<PiPOptions>('picture-in-picture').isInPiP;
|
||||
|
||||
if (!isPiPEnabled && winWasMaximized !== isMaximized) {
|
||||
winWasMaximized = isMaximized;
|
||||
config.set('window-maximized', isMaximized);
|
||||
}
|
||||
|
||||
if (isMaximized) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (!isPiPEnabled) {
|
||||
lateSave('window-size', {
|
||||
width: windowSize[0],
|
||||
height: windowSize[1],
|
||||
});
|
||||
} else if (config.plugins.getOptions<PiPOptions>('picture-in-picture').saveSize) {
|
||||
lateSave('pip-size', windowSize, setPiPOptions);
|
||||
}
|
||||
});
|
||||
|
||||
const savedTimeouts: Record<string, NodeJS.Timeout | undefined> = {};
|
||||
|
||||
function lateSave(key: string, value: unknown, fn: (key: string, value: unknown) => void = config.set) {
|
||||
if (savedTimeouts[key]) {
|
||||
clearTimeout(savedTimeouts[key]);
|
||||
}
|
||||
|
||||
savedTimeouts[key] = setTimeout(() => {
|
||||
fn(key, value);
|
||||
savedTimeouts[key] = undefined;
|
||||
}, 600);
|
||||
}
|
||||
|
||||
app.on('render-process-gone', (event, webContents, details) => {
|
||||
showUnresponsiveDialog(win, details);
|
||||
});
|
||||
|
||||
win.once('ready-to-show', () => {
|
||||
if (config.get('options.appVisible')) {
|
||||
win.show();
|
||||
}
|
||||
});
|
||||
|
||||
removeContentSecurityPolicy();
|
||||
|
||||
win.webContents.loadURL(urlToLoad);
|
||||
|
||||
return win;
|
||||
}
|
||||
|
||||
app.once('browser-window-created', (event, win) => {
|
||||
if (config.get('options.overrideUserAgent')) {
|
||||
// User agents are from https://developers.whatismybrowser.com/useragents/explore/
|
||||
const originalUserAgent = win.webContents.userAgent;
|
||||
const userAgents = {
|
||||
mac: 'Mozilla/5.0 (Macintosh; Intel Mac OS X 12.1; rv:95.0) Gecko/20100101 Firefox/95.0',
|
||||
windows: 'Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:95.0) Gecko/20100101 Firefox/95.0',
|
||||
linux: 'Mozilla/5.0 (Linux x86_64; rv:95.0) Gecko/20100101 Firefox/95.0',
|
||||
};
|
||||
|
||||
const updatedUserAgent
|
||||
= is.macOS() ? userAgents.mac
|
||||
: (is.windows() ? userAgents.windows
|
||||
: userAgents.linux);
|
||||
|
||||
win.webContents.userAgent = updatedUserAgent;
|
||||
app.userAgentFallback = updatedUserAgent;
|
||||
|
||||
win.webContents.session.webRequest.onBeforeSendHeaders((details, cb) => {
|
||||
// This will only happen if login failed, and "retry" was pressed
|
||||
if (win.webContents.getURL().startsWith('https://accounts.google.com') && details.url.startsWith('https://accounts.google.com')) {
|
||||
details.requestHeaders['User-Agent'] = originalUserAgent;
|
||||
}
|
||||
|
||||
cb({ requestHeaders: details.requestHeaders });
|
||||
});
|
||||
}
|
||||
|
||||
setupSongInfo(win);
|
||||
setupAppControls();
|
||||
|
||||
win.webContents.on('did-fail-load', (
|
||||
_event,
|
||||
errorCode,
|
||||
errorDescription,
|
||||
validatedURL,
|
||||
isMainFrame,
|
||||
frameProcessId,
|
||||
frameRoutingId,
|
||||
) => {
|
||||
const log = JSON.stringify({
|
||||
error: 'did-fail-load',
|
||||
errorCode,
|
||||
errorDescription,
|
||||
validatedURL,
|
||||
isMainFrame,
|
||||
frameProcessId,
|
||||
frameRoutingId,
|
||||
}, null, '\t');
|
||||
if (is.dev()) {
|
||||
console.log(log);
|
||||
}
|
||||
|
||||
if (errorCode !== -3) { // -3 is a false positive
|
||||
win.webContents.send('log', log);
|
||||
win.webContents.loadFile(path.join(__dirname, 'error.html'));
|
||||
}
|
||||
});
|
||||
|
||||
win.webContents.on('will-prevent-unload', (event) => {
|
||||
event.preventDefault();
|
||||
});
|
||||
});
|
||||
|
||||
app.on('window-all-closed', () => {
|
||||
if (process.platform !== 'darwin') {
|
||||
app.quit();
|
||||
}
|
||||
|
||||
// Unregister all shortcuts.
|
||||
globalShortcut.unregisterAll();
|
||||
});
|
||||
|
||||
app.on('activate', async () => {
|
||||
// On OS X it's common to re-create a window in the app when the
|
||||
// dock icon is clicked and there are no other windows open.
|
||||
if (mainWindow === null) {
|
||||
mainWindow = await createMainWindow();
|
||||
} else if (!mainWindow.isVisible()) {
|
||||
mainWindow.show();
|
||||
}
|
||||
});
|
||||
|
||||
app.on('ready', async () => {
|
||||
if (config.get('options.autoResetAppCache')) {
|
||||
// Clear cache after 20s
|
||||
const clearCacheTimeout = setTimeout(() => {
|
||||
if (is.dev()) {
|
||||
console.log('Clearing app cache.');
|
||||
}
|
||||
|
||||
session.defaultSession.clearCache();
|
||||
clearTimeout(clearCacheTimeout);
|
||||
}, 20_000);
|
||||
}
|
||||
|
||||
// Register appID on windows
|
||||
if (is.windows()) {
|
||||
const appID = 'com.github.th-ch.youtube-music';
|
||||
app.setAppUserModelId(appID);
|
||||
const appLocation = process.execPath;
|
||||
const appData = app.getPath('appData');
|
||||
// Check shortcut validity if not in dev mode / running portable app
|
||||
if (!is.dev() && !appLocation.startsWith(path.join(appData, '..', 'Local', 'Temp'))) {
|
||||
const shortcutPath = path.join(appData, 'Microsoft', 'Windows', 'Start Menu', 'Programs', 'YouTube Music.lnk');
|
||||
try { // Check if shortcut is registered and valid
|
||||
const shortcutDetails = shell.readShortcutLink(shortcutPath); // Throw error if doesn't exist yet
|
||||
if (
|
||||
shortcutDetails.target !== appLocation
|
||||
|| shortcutDetails.appUserModelId !== appID
|
||||
) {
|
||||
throw 'needUpdate';
|
||||
}
|
||||
} catch (error) { // If not valid -> Register shortcut
|
||||
shell.writeShortcutLink(
|
||||
shortcutPath,
|
||||
error === 'needUpdate' ? 'update' : 'create',
|
||||
{
|
||||
target: appLocation,
|
||||
cwd: path.dirname(appLocation),
|
||||
description: 'YouTube Music Desktop App - including custom plugins',
|
||||
appUserModelId: appID,
|
||||
},
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
mainWindow = await createMainWindow();
|
||||
setApplicationMenu(mainWindow);
|
||||
refreshMenu(mainWindow);
|
||||
setUpTray(app, mainWindow);
|
||||
|
||||
setupProtocolHandler(mainWindow);
|
||||
|
||||
app.on('second-instance', (_, commandLine) => {
|
||||
const uri = `${APP_PROTOCOL}://`;
|
||||
const protocolArgv = commandLine.find((arg) => arg.startsWith(uri));
|
||||
if (protocolArgv) {
|
||||
const lastIndex = protocolArgv.endsWith('/') ? -1 : undefined;
|
||||
const command = protocolArgv.slice(uri.length, lastIndex);
|
||||
if (is.dev()) {
|
||||
console.debug(`Received command over protocol: "${command}"`);
|
||||
}
|
||||
|
||||
handleProtocol(command);
|
||||
return;
|
||||
}
|
||||
|
||||
if (!mainWindow) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (mainWindow.isMinimized()) {
|
||||
mainWindow.restore();
|
||||
}
|
||||
|
||||
if (!mainWindow.isVisible()) {
|
||||
mainWindow.show();
|
||||
}
|
||||
|
||||
mainWindow.focus();
|
||||
});
|
||||
|
||||
// Autostart at login
|
||||
app.setLoginItemSettings({
|
||||
openAtLogin: config.get('options.startAtLogin'),
|
||||
});
|
||||
|
||||
if (!is.dev() && config.get('options.autoUpdates')) {
|
||||
const updateTimeout = setTimeout(() => {
|
||||
autoUpdater.checkForUpdatesAndNotify();
|
||||
clearTimeout(updateTimeout);
|
||||
}, 2000);
|
||||
autoUpdater.on('update-available', () => {
|
||||
const downloadLink
|
||||
= 'https://github.com/th-ch/youtube-music/releases/latest';
|
||||
const dialogOptions: Electron.MessageBoxOptions = {
|
||||
type: 'info',
|
||||
buttons: ['OK', 'Download', 'Disable updates'],
|
||||
title: 'Application Update',
|
||||
message: 'A new version is available',
|
||||
detail: `A new version is available and can be downloaded at ${downloadLink}`,
|
||||
};
|
||||
dialog.showMessageBox(dialogOptions).then((dialogOutput) => {
|
||||
switch (dialogOutput.response) {
|
||||
// Download
|
||||
case 1: {
|
||||
shell.openExternal(downloadLink);
|
||||
break;
|
||||
}
|
||||
|
||||
// Disable updates
|
||||
case 2: {
|
||||
config.set('options.autoUpdates', false);
|
||||
break;
|
||||
}
|
||||
|
||||
default: {
|
||||
break;
|
||||
}
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
if (config.get('options.hideMenu') && !config.get('options.hideMenuWarned')) {
|
||||
dialog.showMessageBox(mainWindow, {
|
||||
type: 'info', title: 'Hide Menu Enabled',
|
||||
message: "Menu is hidden, use 'Alt' to show it (or 'Escape' if using in-app-menu)",
|
||||
});
|
||||
config.set('options.hideMenuWarned', true);
|
||||
}
|
||||
|
||||
// Optimized for Mac OS X
|
||||
if (is.macOS() && !config.get('options.appVisible')) {
|
||||
app.dock.hide();
|
||||
}
|
||||
|
||||
let forceQuit = false;
|
||||
app.on('before-quit', () => {
|
||||
forceQuit = true;
|
||||
});
|
||||
|
||||
if (is.macOS() || config.get('options.tray')) {
|
||||
mainWindow.on('close', (event) => {
|
||||
// Hide the window instead of quitting (quit is available in tray options)
|
||||
if (!forceQuit) {
|
||||
event.preventDefault();
|
||||
mainWindow!.hide();
|
||||
}
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
function showUnresponsiveDialog(win: BrowserWindow, details: Electron.RenderProcessGoneDetails) {
|
||||
if (details) {
|
||||
console.log('Unresponsive Error!\n' + JSON.stringify(details, null, '\t'));
|
||||
}
|
||||
|
||||
dialog.showMessageBox(win, {
|
||||
type: 'error',
|
||||
title: 'Window Unresponsive',
|
||||
message: 'The Application is Unresponsive',
|
||||
detail: 'We are sorry for the inconvenience! please choose what to do:',
|
||||
buttons: ['Wait', 'Relaunch', 'Quit'],
|
||||
cancelId: 0,
|
||||
}).then((result) => {
|
||||
switch (result.response) {
|
||||
case 1: {
|
||||
restart();
|
||||
break;
|
||||
}
|
||||
|
||||
case 2: {
|
||||
app.quit();
|
||||
break;
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
function removeContentSecurityPolicy(
|
||||
betterSession: BetterSession = session.defaultSession as BetterSession,
|
||||
) {
|
||||
// Allows defining multiple "onHeadersReceived" listeners
|
||||
// by enhancing the session.
|
||||
// Some plugins (e.g. adblocker) also define a "onHeadersReceived" listener
|
||||
enhanceWebRequest(betterSession);
|
||||
|
||||
// Custom listener to tweak the content security policy
|
||||
betterSession.webRequest.onHeadersReceived((details, callback) => {
|
||||
details.responseHeaders ??= {};
|
||||
|
||||
// Remove the content security policy
|
||||
delete details.responseHeaders['content-security-policy-report-only'];
|
||||
delete details.responseHeaders['content-security-policy'];
|
||||
|
||||
callback({ cancel: false, responseHeaders: details.responseHeaders });
|
||||
});
|
||||
|
||||
// When multiple listeners are defined, apply them all
|
||||
betterSession.webRequest.setResolver('onHeadersReceived', async (listeners) => {
|
||||
return listeners.reduce(
|
||||
async (accumulator, listener) => {
|
||||
const acc = await accumulator;
|
||||
if (acc.cancel) {
|
||||
return acc;
|
||||
}
|
||||
|
||||
const result = await listener.apply();
|
||||
return { ...accumulator, ...result };
|
||||
},
|
||||
Promise.resolve({ cancel: false }),
|
||||
);
|
||||
});
|
||||
}
|
||||
474
menu.ts
474
menu.ts
@ -1,474 +0,0 @@
|
||||
import is from 'electron-is';
|
||||
import { app, BrowserWindow, clipboard, dialog, Menu } from 'electron';
|
||||
import prompt from 'custom-electron-prompt';
|
||||
|
||||
import { restart } from './providers/app-controls';
|
||||
import config from './config';
|
||||
import { startingPages } from './providers/extracted-data';
|
||||
import promptOptions from './providers/prompt-options';
|
||||
|
||||
import adblockerMenu from './plugins/adblocker/menu';
|
||||
import captionsSelectorMenu from './plugins/captions-selector/menu';
|
||||
import crossfadeMenu from './plugins/crossfade/menu';
|
||||
import disableAutoplayMenu from './plugins/disable-autoplay/menu';
|
||||
import discordMenu from './plugins/discord/menu';
|
||||
import downloaderMenu from './plugins/downloader/menu';
|
||||
import lyricsGeniusMenu from './plugins/lyrics-genius/menu';
|
||||
import notificationsMenu from './plugins/notifications/menu';
|
||||
import pictureInPictureMenu from './plugins/picture-in-picture/menu';
|
||||
import preciseVolumeMenu from './plugins/precise-volume/menu';
|
||||
import shortcutsMenu from './plugins/shortcuts/menu';
|
||||
import videoToggleMenu from './plugins/video-toggle/menu';
|
||||
import visualizerMenu from './plugins/visualizer/menu';
|
||||
import { getAvailablePluginNames } from './plugins/utils';
|
||||
|
||||
export type MenuTemplate = Electron.MenuItemConstructorOptions[];
|
||||
|
||||
// True only if in-app-menu was loaded on launch
|
||||
const inAppMenuActive = config.plugins.isEnabled('in-app-menu');
|
||||
|
||||
const betaPlugins = ['crossfade', 'lumiastream'];
|
||||
|
||||
const pluginMenus = {
|
||||
'adblocker': adblockerMenu,
|
||||
'disable-autoplay': disableAutoplayMenu,
|
||||
'captions-selector': captionsSelectorMenu,
|
||||
'crossfade': crossfadeMenu,
|
||||
'discord': discordMenu,
|
||||
'downloader': downloaderMenu,
|
||||
'lyrics-genius': lyricsGeniusMenu,
|
||||
'notifications': notificationsMenu,
|
||||
'picture-in-picture': pictureInPictureMenu,
|
||||
'precise-volume': preciseVolumeMenu,
|
||||
'shortcuts': shortcutsMenu,
|
||||
'video-toggle': videoToggleMenu,
|
||||
'visualizer': visualizerMenu,
|
||||
};
|
||||
|
||||
const pluginEnabledMenu = (plugin: string, label = '', hasSubmenu = false, refreshMenu: (() => void ) | undefined = undefined): Electron.MenuItemConstructorOptions => ({
|
||||
label: label || plugin,
|
||||
type: 'checkbox',
|
||||
checked: config.plugins.isEnabled(plugin),
|
||||
click(item: Electron.MenuItem) {
|
||||
if (item.checked) {
|
||||
config.plugins.enable(plugin);
|
||||
} else {
|
||||
config.plugins.disable(plugin);
|
||||
}
|
||||
|
||||
if (hasSubmenu) {
|
||||
refreshMenu?.();
|
||||
}
|
||||
},
|
||||
});
|
||||
|
||||
export const refreshMenu = (win: BrowserWindow) => {
|
||||
setApplicationMenu(win);
|
||||
if (inAppMenuActive) {
|
||||
win.webContents.send('refreshMenu');
|
||||
}
|
||||
};
|
||||
|
||||
export const mainMenuTemplate = (win: BrowserWindow): MenuTemplate => {
|
||||
const innerRefreshMenu = () => refreshMenu(win);
|
||||
|
||||
return [
|
||||
{
|
||||
label: 'Plugins',
|
||||
submenu:
|
||||
getAvailablePluginNames().map((pluginName) => {
|
||||
let pluginLabel = pluginName;
|
||||
if (betaPlugins.includes(pluginLabel)) {
|
||||
pluginLabel += ' [beta]';
|
||||
}
|
||||
|
||||
if (Object.hasOwn(pluginMenus, pluginName)) {
|
||||
const getPluginMenu = pluginMenus[pluginName as keyof typeof pluginMenus];
|
||||
|
||||
if (!config.plugins.isEnabled(pluginName)) {
|
||||
return pluginEnabledMenu(pluginName, pluginLabel, true, innerRefreshMenu);
|
||||
}
|
||||
|
||||
return {
|
||||
label: pluginLabel,
|
||||
submenu: [
|
||||
pluginEnabledMenu(pluginName, 'Enabled', true, innerRefreshMenu),
|
||||
{ type: 'separator' },
|
||||
...getPluginMenu(win, config.plugins.getOptions(pluginName), innerRefreshMenu),
|
||||
],
|
||||
} satisfies Electron.MenuItemConstructorOptions;
|
||||
}
|
||||
|
||||
return pluginEnabledMenu(pluginName, pluginLabel);
|
||||
}),
|
||||
},
|
||||
{
|
||||
label: 'Options',
|
||||
submenu: [
|
||||
{
|
||||
label: 'Auto-update',
|
||||
type: 'checkbox',
|
||||
checked: config.get('options.autoUpdates'),
|
||||
click(item) {
|
||||
config.setMenuOption('options.autoUpdates', item.checked);
|
||||
},
|
||||
},
|
||||
{
|
||||
label: 'Resume last song when app starts',
|
||||
type: 'checkbox',
|
||||
checked: config.get('options.resumeOnStart'),
|
||||
click(item) {
|
||||
config.setMenuOption('options.resumeOnStart', item.checked);
|
||||
},
|
||||
},
|
||||
{
|
||||
label: 'Starting page',
|
||||
submenu: (() => {
|
||||
const subMenuArray: Electron.MenuItemConstructorOptions[] = Object.keys(startingPages).map((name) => ({
|
||||
label: name,
|
||||
type: 'radio',
|
||||
checked: config.get('options.startingPage') === name,
|
||||
click() {
|
||||
config.set('options.startingPage', name);
|
||||
},
|
||||
}));
|
||||
subMenuArray.unshift({
|
||||
label: 'Unset',
|
||||
type: 'radio',
|
||||
checked: config.get('options.startingPage') === '',
|
||||
click() {
|
||||
config.set('options.startingPage', '');
|
||||
},
|
||||
});
|
||||
return subMenuArray;
|
||||
})(),
|
||||
},
|
||||
{
|
||||
label: 'Visual Tweaks',
|
||||
submenu: [
|
||||
{
|
||||
label: 'Remove upgrade button',
|
||||
type: 'checkbox',
|
||||
checked: config.get('options.removeUpgradeButton'),
|
||||
click(item) {
|
||||
config.setMenuOption('options.removeUpgradeButton', item.checked);
|
||||
},
|
||||
},
|
||||
{
|
||||
label: 'Like buttons',
|
||||
submenu: [
|
||||
{
|
||||
label: 'Default',
|
||||
type: 'radio',
|
||||
checked: !config.get('options.likeButtons'),
|
||||
click() {
|
||||
config.set('options.likeButtons', '');
|
||||
},
|
||||
},
|
||||
{
|
||||
label: 'Force show',
|
||||
type: 'radio',
|
||||
checked: config.get('options.likeButtons') === 'force',
|
||||
click() {
|
||||
config.set('options.likeButtons', 'force');
|
||||
},
|
||||
},
|
||||
{
|
||||
label: 'Hide',
|
||||
type: 'radio',
|
||||
checked: config.get('options.likeButtons') === 'hide',
|
||||
click() {
|
||||
config.set('options.likeButtons', 'hide');
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
label: 'Theme',
|
||||
submenu: [
|
||||
{
|
||||
label: 'No theme',
|
||||
type: 'radio',
|
||||
checked: config.get('options.themes')?.length === 0, // Todo rename "themes"
|
||||
click() {
|
||||
config.set('options.themes', []);
|
||||
},
|
||||
},
|
||||
{ type: 'separator' },
|
||||
{
|
||||
label: 'Import custom CSS file',
|
||||
type: 'normal',
|
||||
async click() {
|
||||
const { filePaths } = await dialog.showOpenDialog({
|
||||
filters: [{ name: 'CSS Files', extensions: ['css'] }],
|
||||
properties: ['openFile', 'multiSelections'],
|
||||
});
|
||||
if (filePaths) {
|
||||
config.set('options.themes', filePaths);
|
||||
}
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
label: 'Single instance lock',
|
||||
type: 'checkbox',
|
||||
checked: true,
|
||||
click(item) {
|
||||
if (!item.checked && app.hasSingleInstanceLock()) {
|
||||
app.releaseSingleInstanceLock();
|
||||
} else if (item.checked && !app.hasSingleInstanceLock()) {
|
||||
app.requestSingleInstanceLock();
|
||||
}
|
||||
},
|
||||
},
|
||||
{
|
||||
label: 'Always on top',
|
||||
type: 'checkbox',
|
||||
checked: config.get('options.alwaysOnTop'),
|
||||
click(item) {
|
||||
config.setMenuOption('options.alwaysOnTop', item.checked);
|
||||
win.setAlwaysOnTop(item.checked);
|
||||
},
|
||||
},
|
||||
...(is.windows() || is.linux()
|
||||
? [
|
||||
{
|
||||
label: 'Hide menu',
|
||||
type: 'checkbox',
|
||||
checked: config.get('options.hideMenu'),
|
||||
click(item) {
|
||||
config.setMenuOption('options.hideMenu', item.checked);
|
||||
if (item.checked && !config.get('options.hideMenuWarned')) {
|
||||
dialog.showMessageBox(win, {
|
||||
type: 'info', title: 'Hide Menu Enabled',
|
||||
message: 'Menu will be hidden on next launch, use [Alt] to show it (or backtick [`] if using in-app-menu)',
|
||||
});
|
||||
}
|
||||
},
|
||||
},
|
||||
]
|
||||
: []) satisfies Electron.MenuItemConstructorOptions[],
|
||||
...(is.windows() || is.macOS()
|
||||
? // Only works on Win/Mac
|
||||
// https://www.electronjs.org/docs/api/app#appsetloginitemsettingssettings-macos-windows
|
||||
[
|
||||
{
|
||||
label: 'Start at login',
|
||||
type: 'checkbox',
|
||||
checked: config.get('options.startAtLogin'),
|
||||
click(item) {
|
||||
config.setMenuOption('options.startAtLogin', item.checked);
|
||||
},
|
||||
},
|
||||
]
|
||||
: []) satisfies Electron.MenuItemConstructorOptions[],
|
||||
{
|
||||
label: 'Tray',
|
||||
submenu: [
|
||||
{
|
||||
label: 'Disabled',
|
||||
type: 'radio',
|
||||
checked: !config.get('options.tray'),
|
||||
click() {
|
||||
config.setMenuOption('options.tray', false);
|
||||
config.setMenuOption('options.appVisible', true);
|
||||
},
|
||||
},
|
||||
{
|
||||
label: 'Enabled + app visible',
|
||||
type: 'radio',
|
||||
checked: config.get('options.tray') && config.get('options.appVisible'),
|
||||
click() {
|
||||
config.setMenuOption('options.tray', true);
|
||||
config.setMenuOption('options.appVisible', true);
|
||||
},
|
||||
},
|
||||
{
|
||||
label: 'Enabled + app hidden',
|
||||
type: 'radio',
|
||||
checked: config.get('options.tray') && !config.get('options.appVisible'),
|
||||
click() {
|
||||
config.setMenuOption('options.tray', true);
|
||||
config.setMenuOption('options.appVisible', false);
|
||||
},
|
||||
},
|
||||
{ type: 'separator' },
|
||||
{
|
||||
label: 'Play/Pause on click',
|
||||
type: 'checkbox',
|
||||
checked: config.get('options.trayClickPlayPause'),
|
||||
click(item) {
|
||||
config.setMenuOption('options.trayClickPlayPause', item.checked);
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
{ type: 'separator' },
|
||||
{
|
||||
label: 'Advanced options',
|
||||
submenu: [
|
||||
{
|
||||
label: 'Set Proxy',
|
||||
type: 'normal',
|
||||
async click(item) {
|
||||
await setProxy(item, win);
|
||||
},
|
||||
},
|
||||
{
|
||||
label: 'Override useragent',
|
||||
type: 'checkbox',
|
||||
checked: config.get('options.overrideUserAgent'),
|
||||
click(item) {
|
||||
config.setMenuOption('options.overrideUserAgent', item.checked);
|
||||
},
|
||||
},
|
||||
{
|
||||
label: 'Disable hardware acceleration',
|
||||
type: 'checkbox',
|
||||
checked: config.get('options.disableHardwareAcceleration'),
|
||||
click(item) {
|
||||
config.setMenuOption('options.disableHardwareAcceleration', item.checked);
|
||||
},
|
||||
},
|
||||
{
|
||||
label: 'Restart on config changes',
|
||||
type: 'checkbox',
|
||||
checked: config.get('options.restartOnConfigChanges'),
|
||||
click(item) {
|
||||
config.setMenuOption('options.restartOnConfigChanges', item.checked);
|
||||
},
|
||||
},
|
||||
{
|
||||
label: 'Reset App cache when app starts',
|
||||
type: 'checkbox',
|
||||
checked: config.get('options.autoResetAppCache'),
|
||||
click(item) {
|
||||
config.setMenuOption('options.autoResetAppCache', item.checked);
|
||||
},
|
||||
},
|
||||
{ type: 'separator' },
|
||||
is.macOS()
|
||||
? {
|
||||
label: 'Toggle DevTools',
|
||||
// Cannot use "toggleDevTools" role in macOS
|
||||
click() {
|
||||
const { webContents } = win;
|
||||
if (webContents.isDevToolsOpened()) {
|
||||
webContents.closeDevTools();
|
||||
} else {
|
||||
webContents.openDevTools();
|
||||
}
|
||||
},
|
||||
}
|
||||
: { role: 'toggleDevTools' },
|
||||
{
|
||||
label: 'Edit config.json',
|
||||
click() {
|
||||
config.edit();
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
label: 'View',
|
||||
submenu: [
|
||||
{ role: 'reload' },
|
||||
{ role: 'forceReload' },
|
||||
{ type: 'separator' },
|
||||
{ role: 'zoomIn', accelerator: process.platform === 'darwin' ? 'Cmd+I' : 'Ctrl+I' },
|
||||
{ role: 'zoomOut', accelerator: process.platform === 'darwin' ? 'Cmd+O' : 'Ctrl+O' },
|
||||
{ role: 'resetZoom' },
|
||||
{ type: 'separator' },
|
||||
{ role: 'togglefullscreen' },
|
||||
],
|
||||
},
|
||||
{
|
||||
label: 'Navigation',
|
||||
submenu: [
|
||||
{
|
||||
label: 'Go back',
|
||||
click() {
|
||||
if (win.webContents.canGoBack()) {
|
||||
win.webContents.goBack();
|
||||
}
|
||||
},
|
||||
},
|
||||
{
|
||||
label: 'Go forward',
|
||||
click() {
|
||||
if (win.webContents.canGoForward()) {
|
||||
win.webContents.goForward();
|
||||
}
|
||||
},
|
||||
},
|
||||
{
|
||||
label: 'Copy current URL',
|
||||
click() {
|
||||
const currentURL = win.webContents.getURL();
|
||||
clipboard.writeText(currentURL);
|
||||
},
|
||||
},
|
||||
{
|
||||
label: 'Restart App',
|
||||
click: restart,
|
||||
},
|
||||
{ role: 'quit' },
|
||||
],
|
||||
},
|
||||
];
|
||||
};
|
||||
export const setApplicationMenu = (win: Electron.BrowserWindow) => {
|
||||
const menuTemplate: MenuTemplate = [...mainMenuTemplate(win)];
|
||||
if (process.platform === 'darwin') {
|
||||
const { name } = app;
|
||||
menuTemplate.unshift({
|
||||
label: name,
|
||||
submenu: [
|
||||
{ role: 'about' },
|
||||
{ type: 'separator' },
|
||||
{ role: 'hide' },
|
||||
{ role: 'hideOthers' },
|
||||
{ role: 'unhide' },
|
||||
{ type: 'separator' },
|
||||
{ role: 'selectAll' },
|
||||
{ role: 'cut' },
|
||||
{ role: 'copy' },
|
||||
{ role: 'paste' },
|
||||
{ type: 'separator' },
|
||||
{ role: 'minimize' },
|
||||
{ role: 'close' },
|
||||
{ role: 'quit' },
|
||||
],
|
||||
});
|
||||
}
|
||||
|
||||
const menu = Menu.buildFromTemplate(menuTemplate);
|
||||
Menu.setApplicationMenu(menu);
|
||||
};
|
||||
|
||||
async function setProxy(item: Electron.MenuItem, win: BrowserWindow) {
|
||||
const output = await prompt({
|
||||
title: 'Set Proxy',
|
||||
label: 'Enter Proxy Address: (leave empty to disable)',
|
||||
value: config.get('options.proxy'),
|
||||
type: 'input',
|
||||
inputAttrs: {
|
||||
type: 'url',
|
||||
placeholder: "Example: 'socks5://127.0.0.1:9999",
|
||||
},
|
||||
width: 450,
|
||||
...promptOptions(),
|
||||
}, win);
|
||||
|
||||
if (typeof output === 'string') {
|
||||
config.setMenuOption('options.proxy', output);
|
||||
item.checked = output !== '';
|
||||
} else { // User pressed cancel
|
||||
item.checked = !item.checked; // Reset checkbox
|
||||
}
|
||||
}
|
||||
10137
package-lock.json
generated
10137
package-lock.json
generated
File diff suppressed because it is too large
Load Diff
199
package.json
199
package.json
@ -1,9 +1,9 @@
|
||||
{
|
||||
"name": "youtube-music",
|
||||
"productName": "YouTube Music",
|
||||
"version": "2.1.0",
|
||||
"version": "3.5.3",
|
||||
"description": "YouTube Music Desktop App - including custom plugins",
|
||||
"main": "./dist/index.js",
|
||||
"main": "./dist/main/index.js",
|
||||
"license": "MIT",
|
||||
"repository": "th-ch/youtube-music",
|
||||
"author": {
|
||||
@ -17,6 +17,7 @@
|
||||
"files": [
|
||||
"!*",
|
||||
"dist",
|
||||
"assets",
|
||||
"license",
|
||||
"!node_modules",
|
||||
"node_modules/custom-electron-prompt/**",
|
||||
@ -25,6 +26,9 @@
|
||||
"!node_modules/**/*.map",
|
||||
"!node_modules/**/*.ts"
|
||||
],
|
||||
"asarUnpack": [
|
||||
"assets"
|
||||
],
|
||||
"mac": {
|
||||
"identity": null,
|
||||
"target": [
|
||||
@ -73,6 +77,29 @@
|
||||
"rpm"
|
||||
]
|
||||
},
|
||||
"deb": {
|
||||
"depends": [
|
||||
"libgtk-3-0",
|
||||
"libnotify4",
|
||||
"libnss3",
|
||||
"libxss1",
|
||||
"libxtst6",
|
||||
"xdg-utils",
|
||||
"libatspi2.0-0",
|
||||
"libuuid1",
|
||||
"libasound2",
|
||||
"libgbm1"
|
||||
]
|
||||
},
|
||||
"rpm": {
|
||||
"depends": [
|
||||
"/usr/lib64/libuuid.so.1"
|
||||
],
|
||||
"fpm": [
|
||||
"--rpm-rpmbuild-define",
|
||||
"_build_id_links none"
|
||||
]
|
||||
},
|
||||
"snap": {
|
||||
"slots": [
|
||||
{
|
||||
@ -87,101 +114,135 @@
|
||||
}
|
||||
},
|
||||
"scripts": {
|
||||
"test": "npm run build && playwright test",
|
||||
"test:debug": "DEBUG=pw:browser* npm run build && playwright test",
|
||||
"rollup:preload": "rollup -c rollup.preload.config.ts --configPlugin @rollup/plugin-typescript --bundleConfigAsCjs",
|
||||
"rollup:main": "rollup -c rollup.main.config.ts --configPlugin @rollup/plugin-typescript --bundleConfigAsCjs",
|
||||
"build": "npm run rollup:preload && npm run rollup:main",
|
||||
"start": "npm run build && electron ./dist/index.js",
|
||||
"start:debug": "ELECTRON_ENABLE_LOGGING=1 npm run start",
|
||||
"postinstall": "patch-package",
|
||||
"clean": "del-cli dist && del-cli pack",
|
||||
"dist": "npm run clean && npm run build && electron-builder --win --mac --linux -p never",
|
||||
"dist:linux": "npm run clean && npm run build && electron-builder --linux -p never",
|
||||
"dist:mac": "npm run clean && npm run build && electron-builder --mac dmg:x64 -p never",
|
||||
"dist:mac:arm64": "npm run clean && npm run build && electron-builder --mac dmg:arm64 -p never",
|
||||
"dist:win": "npm run clean && npm run build && electron-builder --win -p never",
|
||||
"dist:win:x64": "npm run clean && npm run build && electron-builder --win nsis-web:x64 -p never",
|
||||
"test": "playwright test",
|
||||
"test:debug": "cross-env DEBUG=pw:*,-pw:test:protocol playwright test",
|
||||
"build": "electron-vite build",
|
||||
"vite:inspect": "pnpm clean && electron-vite build --mode development && pnpm exec serve .vite-inspect",
|
||||
"start": "electron-vite preview",
|
||||
"start:debug": "cross-env ELECTRON_ENABLE_LOGGING=1 pnpm start",
|
||||
"dev": "cross-env NODE_OPTIONS=--enable-source-maps electron-vite dev --watch",
|
||||
"dev:debug": "cross-env ELECTRON_ENABLE_LOGGING=1 pnpm dev",
|
||||
"clean": "del-cli dist && del-cli pack && del-cli .vite-inspect",
|
||||
"dist": "pnpm clean && pnpm build && pnpm electron-builder --win --mac --linux -p never",
|
||||
"dist:linux": "pnpm clean && pnpm build && pnpm electron-builder --linux -p never",
|
||||
"dist:linux:deb-arm64": "pnpm clean && pnpm build && pnpm electron-builder --linux deb:arm64 -p never",
|
||||
"dist:linux:rpm-arm64": "pnpm clean && pnpm build && pnpm electron-builder --linux rpm:arm64 -p never",
|
||||
"dist:mac": "pnpm clean && pnpm build && pnpm electron-builder --mac dmg:x64 -p never",
|
||||
"dist:mac:arm64": "pnpm clean && pnpm build && pnpm electron-builder --mac dmg:arm64 -p never",
|
||||
"dist:win": "pnpm clean && pnpm build && pnpm electron-builder --win -p never",
|
||||
"dist:win:x64": "pnpm clean && pnpm build && pnpm electron-builder --win nsis-web:x64 -p never",
|
||||
"lint": "eslint .",
|
||||
"changelog": "auto-changelog",
|
||||
"release:linux": "npm run clean && npm run build && electron-builder --linux -p always -c.snap.publish=github",
|
||||
"release:mac": "npm run clean && npm run build && electron-builder --mac -p always",
|
||||
"release:win": "npm run clean && npm run build && electron-builder --win -p always",
|
||||
"changelog": "npx --yes auto-changelog",
|
||||
"release:linux": "pnpm clean && pnpm build && pnpm electron-builder --linux -p always -c.snap.publish=github",
|
||||
"release:mac": "pnpm clean && pnpm build && pnpm electron-builder --mac -p always",
|
||||
"release:win": "pnpm clean && pnpm build && pnpm electron-builder --win -p always",
|
||||
"typecheck": "tsc -p tsconfig.json --noEmit"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=16.0.0"
|
||||
"node": ">=18.0.0",
|
||||
"pnpm": ">=8"
|
||||
},
|
||||
"pnpm": {
|
||||
"overrides": {
|
||||
"usocket": "1.0.1",
|
||||
"node-gyp": "10.2.0",
|
||||
"xml2js": "0.6.2",
|
||||
"node-fetch": "3.3.2",
|
||||
"@electron/universal": "2.0.1",
|
||||
"@babel/runtime": "7.25.6"
|
||||
},
|
||||
"patchedDependencies": {
|
||||
"vudio@2.1.1": "patches/vudio@2.1.1.patch",
|
||||
"app-builder-lib@24.13.3": "patches/app-builder-lib@24.13.3.patch"
|
||||
}
|
||||
},
|
||||
"dependencies": {
|
||||
"@cliqz/adblocker-electron": "1.26.8",
|
||||
"@cliqz/adblocker-electron": "1.27.1",
|
||||
"@cliqz/adblocker-electron-preload": "1.27.1",
|
||||
"@electron-toolkit/tsconfig": "1.0.1",
|
||||
"@electron/remote": "2.1.2",
|
||||
"@ffmpeg.wasm/core-mt": "0.12.0",
|
||||
"@ffmpeg.wasm/main": "0.12.0",
|
||||
"@foobar404/wave": "2.0.4",
|
||||
"@floating-ui/dom": "1.6.11",
|
||||
"@foobar404/wave": "2.0.5",
|
||||
"@jellybrick/electron-better-web-request": "1.0.4",
|
||||
"@jellybrick/mpris-service": "2.1.4",
|
||||
"@xhayper/discord-rpc": "1.0.23",
|
||||
"async-mutex": "0.4.0",
|
||||
"@skyra/jaro-winkler": "^1.1.1",
|
||||
"@xhayper/discord-rpc": "1.2.0",
|
||||
"async-mutex": "0.5.0",
|
||||
"butterchurn": "3.0.0-beta.4",
|
||||
"butterchurn-presets": "3.0.0-beta.4",
|
||||
"conf": "10.2.0",
|
||||
"custom-electron-prompt": "1.5.7",
|
||||
"electron-debug": "3.2.0",
|
||||
"color": "4.2.3",
|
||||
"conf": "13.0.1",
|
||||
"custom-electron-prompt": "1.5.8",
|
||||
"dbus-next": "0.10.2",
|
||||
"deepmerge-ts": "7.1.0",
|
||||
"electron-debug": "4.0.1",
|
||||
"electron-is": "3.0.0",
|
||||
"electron-localshortcut": "3.2.1",
|
||||
"electron-store": "8.1.0",
|
||||
"electron-store": "10.0.0",
|
||||
"electron-unhandled": "4.0.1",
|
||||
"electron-updater": "6.1.4",
|
||||
"electron-updater": "6.3.4",
|
||||
"fast-average-color": "9.4.0",
|
||||
"fast-equals": "5.0.1",
|
||||
"filenamify": "6.0.0",
|
||||
"howler": "2.2.4",
|
||||
"html-to-text": "9.0.5",
|
||||
"i18next": "23.15.1",
|
||||
"keyboardevent-from-electron-accelerator": "2.0.0",
|
||||
"keyboardevents-areequal": "0.2.2",
|
||||
"node-html-parser": "6.1.13",
|
||||
"node-id3": "0.2.6",
|
||||
"simple-youtube-age-restriction-bypass": "git+https://github.com/organization/Simple-YouTube-Age-Restriction-Bypass.git#v2.5.8",
|
||||
"peerjs": "1.5.4",
|
||||
"semver": "7.6.3",
|
||||
"serve": "14.2.3",
|
||||
"simple-youtube-age-restriction-bypass": "github:organization/Simple-YouTube-Age-Restriction-Bypass#v2.5.9",
|
||||
"solid-floating-ui": "0.3.1",
|
||||
"solid-js": "1.8.22",
|
||||
"solid-styled-components": "0.28.5",
|
||||
"solid-transition-group": "0.2.3",
|
||||
"ts-morph": "23.0.0",
|
||||
"vudio": "2.1.1",
|
||||
"x11": "2.3.0",
|
||||
"youtubei.js": "6.4.1",
|
||||
"ytpl": "2.3.0"
|
||||
},
|
||||
"overrides": {
|
||||
"rollup": "4.0.2",
|
||||
"node-gyp": "9.4.0",
|
||||
"xml2js": "0.6.2",
|
||||
"node-fetch": "2.7.0",
|
||||
"@electron/universal": "1.4.2",
|
||||
"@babel/runtime": "7.23.2"
|
||||
"youtubei.js": "10.4.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@playwright/test": "1.39.0",
|
||||
"@rollup/plugin-commonjs": "25.0.5",
|
||||
"@rollup/plugin-image": "3.0.3",
|
||||
"@rollup/plugin-json": "6.0.1",
|
||||
"@rollup/plugin-node-resolve": "15.2.3",
|
||||
"@rollup/plugin-terser": "0.4.4",
|
||||
"@rollup/plugin-typescript": "11.1.5",
|
||||
"@rollup/plugin-wasm": "6.2.2",
|
||||
"@total-typescript/ts-reset": "0.5.1",
|
||||
"@types/electron-localshortcut": "3.1.1",
|
||||
"@types/howler": "2.2.9",
|
||||
"@types/html-to-text": "9.0.2",
|
||||
"@typescript-eslint/eslint-plugin": "6.7.5",
|
||||
"auto-changelog": "2.4.0",
|
||||
"@playwright/test": "1.47.1",
|
||||
"@total-typescript/ts-reset": "0.6.1",
|
||||
"@types/color": "3.0.6",
|
||||
"@types/electron-localshortcut": "3.1.3",
|
||||
"@types/howler": "2.2.11",
|
||||
"@types/html-to-text": "9.0.4",
|
||||
"@types/semver": "7.5.8",
|
||||
"@types/trusted-types": "2.0.7",
|
||||
"@typescript-eslint/eslint-plugin": "8.6.0",
|
||||
"@typescript-eslint/parser": "8.6.0",
|
||||
"bufferutil": "4.0.8",
|
||||
"builtin-modules": "4.0.0",
|
||||
"cross-env": "7.0.3",
|
||||
"del-cli": "5.1.0",
|
||||
"electron": "27.0.0",
|
||||
"electron-builder": "24.6.4",
|
||||
"discord-api-types": "0.37.100",
|
||||
"electron": "32.1.0",
|
||||
"electron-builder": "24.13.3",
|
||||
"electron-devtools-installer": "3.2.0",
|
||||
"eslint": "8.51.0",
|
||||
"eslint-plugin-import": "2.28.1",
|
||||
"eslint-plugin-prettier": "5.0.1",
|
||||
"node-gyp": "9.4.0",
|
||||
"patch-package": "8.0.0",
|
||||
"playwright": "1.39.0",
|
||||
"rollup": "4.0.2",
|
||||
"rollup-plugin-copy": "3.5.0",
|
||||
"rollup-plugin-import-css": "3.3.5",
|
||||
"rollup-plugin-string": "3.0.0",
|
||||
"typescript": "5.2.2"
|
||||
"electron-vite": "2.3.0",
|
||||
"esbuild": "0.23.1",
|
||||
"eslint": "8.57.1",
|
||||
"eslint-import-resolver-exports": "1.0.0-beta.5",
|
||||
"eslint-import-resolver-typescript": "3.6.3",
|
||||
"eslint-plugin-import": "2.30.0",
|
||||
"eslint-plugin-prettier": "5.2.1",
|
||||
"glob": "11.0.0",
|
||||
"node-gyp": "10.2.0",
|
||||
"playwright": "1.47.1",
|
||||
"rollup": "4.21.3",
|
||||
"typescript": "5.6.2",
|
||||
"utf-8-validate": "6.0.4",
|
||||
"vite": "5.4.6",
|
||||
"vite-plugin-inspect": "0.8.7",
|
||||
"vite-plugin-resolve": "2.5.2",
|
||||
"vite-plugin-solid": "2.10.2",
|
||||
"ws": "8.18.0"
|
||||
},
|
||||
"auto-changelog": {
|
||||
"hideCredit": true,
|
||||
|
||||
21
patches/app-builder-lib@24.13.3.patch
Normal file
21
patches/app-builder-lib@24.13.3.patch
Normal file
@ -0,0 +1,21 @@
|
||||
diff --git a/out/targets/snap.js b/out/targets/snap.js
|
||||
index f72c36355d27cd2d69fc5fdf2d8bb2451db0287f..baae112fe25ebb49ab8e25aaa48efd6bc43b598f 100644
|
||||
--- a/out/targets/snap.js
|
||||
+++ b/out/targets/snap.js
|
||||
@@ -212,14 +212,14 @@ class SnapTarget extends core_1.Target {
|
||||
args.push("--template-url", `electron4:${snapArch}`);
|
||||
}
|
||||
await (0, builder_util_1.executeAppBuilder)(args);
|
||||
- const publishConfig = findSnapPublishConfig(this.packager.config);
|
||||
+
|
||||
await packager.info.callArtifactBuildCompleted({
|
||||
file: artifactPath,
|
||||
safeArtifactName: packager.computeSafeArtifactName(artifactName, "snap", arch, false),
|
||||
target: this,
|
||||
arch,
|
||||
packager,
|
||||
- publishConfig: publishConfig == null ? { provider: "snapStore" } : publishConfig,
|
||||
+ publishConfig: options.publish == null ? { provider: "snapStore" } : null,
|
||||
});
|
||||
}
|
||||
isElectronVersionGreaterOrEqualThan(version) {
|
||||
161
patches/eslint-plugin-import@2.29.1.patch
Normal file
161
patches/eslint-plugin-import@2.29.1.patch
Normal file
@ -0,0 +1,161 @@
|
||||
diff --git a/lib/importDeclaration.js b/lib/importDeclaration.js
|
||||
index afb4de779034cfea080825a5f4320661c48bee32..f10b0a11a39577fbd42569e6b0e768255c1ef276 100644
|
||||
--- a/lib/importDeclaration.js
|
||||
+++ b/lib/importDeclaration.js
|
||||
@@ -1,5 +1,5 @@
|
||||
-"use strict";Object.defineProperty(exports, "__esModule", { value: true });exports["default"] = importDeclaration;function importDeclaration(context) {
|
||||
- var ancestors = context.getAncestors();
|
||||
+"use strict";Object.defineProperty(exports, "__esModule", { value: true });exports["default"] = importDeclaration;function importDeclaration(context, node) {
|
||||
+ var ancestors = context.getSourceCode().getAncestors(node);
|
||||
return ancestors[ancestors.length - 1];
|
||||
}
|
||||
//# sourceMappingURL=data:application/json;charset=utf-8;base64,eyJ2ZXJzaW9uIjozLCJzb3VyY2VzIjpbIi4uL3NyYy9pbXBvcnREZWNsYXJhdGlvbi5qcyJdLCJuYW1lcyI6WyJpbXBvcnREZWNsYXJhdGlvbiIsImNvbnRleHQiLCJhbmNlc3RvcnMiLCJnZXRBbmNlc3RvcnMiLCJsZW5ndGgiXSwibWFwcGluZ3MiOiJnR0FBd0JBLGlCLENBQVQsU0FBU0EsaUJBQVQsQ0FBMkJDLE9BQTNCLEVBQW9DO0FBQ2pELE1BQU1DLFlBQVlELFFBQVFFLFlBQVIsRUFBbEI7QUFDQSxTQUFPRCxVQUFVQSxVQUFVRSxNQUFWLEdBQW1CLENBQTdCLENBQVA7QUFDRCIsImZpbGUiOiJpbXBvcnREZWNsYXJhdGlvbi5qcyIsInNvdXJjZXNDb250ZW50IjpbImV4cG9ydCBkZWZhdWx0IGZ1bmN0aW9uIGltcG9ydERlY2xhcmF0aW9uKGNvbnRleHQpIHtcbiAgY29uc3QgYW5jZXN0b3JzID0gY29udGV4dC5nZXRBbmNlc3RvcnMoKTtcbiAgcmV0dXJuIGFuY2VzdG9yc1thbmNlc3RvcnMubGVuZ3RoIC0gMV07XG59XG4iXX0=
|
||||
\ No newline at end of file
|
||||
diff --git a/lib/rules/first.js b/lib/rules/first.js
|
||||
index a77168660cf32c8c3e96f3ff4b8240a36d7de3a6..c0e00d75f9989916057fef3999eeee8d21820292 100644
|
||||
--- a/lib/rules/first.js
|
||||
+++ b/lib/rules/first.js
|
||||
@@ -66,7 +66,7 @@ module.exports = {
|
||||
}
|
||||
}
|
||||
if (nonImportCount > 0) {var _iteratorNormalCompletion = true;var _didIteratorError = false;var _iteratorError = undefined;try {
|
||||
- for (var _iterator = context.getDeclaredVariables(node)[Symbol.iterator](), _step; !(_iteratorNormalCompletion = (_step = _iterator.next()).done); _iteratorNormalCompletion = true) {var variable = _step.value;
|
||||
+ for (var _iterator = sourceCode.getDeclaredVariables(node)[Symbol.iterator](), _step; !(_iteratorNormalCompletion = (_step = _iterator.next()).done); _iteratorNormalCompletion = true) {var variable = _step.value;
|
||||
if (!shouldSort) {break;}
|
||||
var references = variable.references;
|
||||
if (references.length) {var _iteratorNormalCompletion2 = true;var _didIteratorError2 = false;var _iteratorError2 = undefined;try {
|
||||
diff --git a/lib/rules/namespace.js b/lib/rules/namespace.js
|
||||
index 574d89a60d15c7e0e712956ea6a3ad2d0eac7f08..82e7cb3cff4246592d762cce86323f2b72de92e4 100644
|
||||
--- a/lib/rules/namespace.js
|
||||
+++ b/lib/rules/namespace.js
|
||||
@@ -86,7 +86,7 @@ module.exports = {
|
||||
|
||||
// same as above, but does not add names to local map
|
||||
ExportNamespaceSpecifier: function () {function ExportNamespaceSpecifier(namespace) {
|
||||
- var declaration = (0, _importDeclaration2['default'])(context);
|
||||
+ var declaration = (0, _importDeclaration2['default'])(context, namespace);
|
||||
|
||||
var imports = _ExportMap2['default'].get(declaration.source.value, context);
|
||||
if (imports == null) {return null;}
|
||||
diff --git a/lib/rules/newline-after-import.js b/lib/rules/newline-after-import.js
|
||||
index 6cc15686464a17803a0b976c35b99627cdbfabee..520eec6d9a375527ab72c459960fe4416c046c17 100644
|
||||
--- a/lib/rules/newline-after-import.js
|
||||
+++ b/lib/rules/newline-after-import.js
|
||||
@@ -194,7 +194,7 @@ module.exports = {
|
||||
}return CallExpression;}(),
|
||||
'Program:exit': function () {function ProgramExit() {
|
||||
log('exit processing for', context.getPhysicalFilename ? context.getPhysicalFilename() : context.getFilename());
|
||||
- var scopeBody = getScopeBody(context.getScope());
|
||||
+ var scopeBody = getScopeBody(context.getSourceCode().getScope(node));
|
||||
log('got scope:', scopeBody);
|
||||
|
||||
requireCalls.forEach(function (node, index) {
|
||||
diff --git a/lib/rules/no-amd.js b/lib/rules/no-amd.js
|
||||
index 7ac108bf812ca4f78bfa6fe5ae8b9cf38e2ff497..346c3105dc70f72c4d76fcc6b96b946d1d4ec6d5 100644
|
||||
--- a/lib/rules/no-amd.js
|
||||
+++ b/lib/rules/no-amd.js
|
||||
@@ -23,7 +23,7 @@ module.exports = {
|
||||
create: function () {function create(context) {
|
||||
return {
|
||||
CallExpression: function () {function CallExpression(node) {
|
||||
- if (context.getScope().type !== 'module') {return;}
|
||||
+ if (context.getSourceCode().getScope(node).type !== 'module') {return;}
|
||||
|
||||
if (node.callee.type !== 'Identifier') {return;}
|
||||
if (node.callee.name !== 'require' && node.callee.name !== 'define') {return;}
|
||||
diff --git a/lib/rules/no-commonjs.js b/lib/rules/no-commonjs.js
|
||||
index befeff0026d61d3ac1e6bbcea29f5c471dc1d353..e91c5ed34e968d5867e884ea800e166cda345aef 100644
|
||||
--- a/lib/rules/no-commonjs.js
|
||||
+++ b/lib/rules/no-commonjs.js
|
||||
@@ -107,7 +107,7 @@ module.exports = {
|
||||
|
||||
// exports.
|
||||
if (node.object.name === 'exports') {
|
||||
- var isInScope = context.getScope().
|
||||
+ var isInScope = context.getSourceCode().getScope(node).
|
||||
variables.
|
||||
some(function (variable) {return variable.name === 'exports';});
|
||||
if (!isInScope) {
|
||||
@@ -117,7 +117,7 @@ module.exports = {
|
||||
|
||||
}return MemberExpression;}(),
|
||||
CallExpression: function () {function CallExpression(call) {
|
||||
- if (!validateScope(context.getScope())) {return;}
|
||||
+ if (!validateScope(context.getSourceCode().getScope(call))) {return;}
|
||||
|
||||
if (call.callee.type !== 'Identifier') {return;}
|
||||
if (call.callee.name !== 'require') {return;}
|
||||
diff --git a/lib/rules/no-mutable-exports.js b/lib/rules/no-mutable-exports.js
|
||||
index 40bd1b4cfa95d41732bb13bba0ed1969a91cc7ff..8a25abfbfadb299204b36a6cbf283259bcc2e790 100644
|
||||
--- a/lib/rules/no-mutable-exports.js
|
||||
+++ b/lib/rules/no-mutable-exports.js
|
||||
@@ -32,7 +32,7 @@ module.exports = {
|
||||
}
|
||||
|
||||
function handleExportDefault(node) {
|
||||
- var scope = context.getScope();
|
||||
+ var scope = context.getSourceCode().getScope(node);
|
||||
|
||||
if (node.declaration.name) {
|
||||
checkDeclarationsInScope(scope, node.declaration.name);
|
||||
@@ -40,7 +40,7 @@ module.exports = {
|
||||
}
|
||||
|
||||
function handleExportNamed(node) {
|
||||
- var scope = context.getScope();
|
||||
+ var scope = context.getSourceCode().getScope(node);
|
||||
|
||||
if (node.declaration) {
|
||||
checkDeclaration(node.declaration);
|
||||
diff --git a/lib/rules/no-named-as-default-member.js b/lib/rules/no-named-as-default-member.js
|
||||
index 0c15051e027ad7d1d45f1b51c20be1c000b0af01..5b3d6ba415511b7f9f83a52e1acfebe5a1045a7b 100644
|
||||
--- a/lib/rules/no-named-as-default-member.js
|
||||
+++ b/lib/rules/no-named-as-default-member.js
|
||||
@@ -35,7 +35,7 @@ module.exports = {
|
||||
|
||||
return {
|
||||
ImportDefaultSpecifier: function () {function ImportDefaultSpecifier(node) {
|
||||
- var declaration = (0, _importDeclaration2['default'])(context);
|
||||
+ var declaration = (0, _importDeclaration2['default'])(context, node);
|
||||
var exportMap = _ExportMap2['default'].get(declaration.source.value, context);
|
||||
if (exportMap == null) {return;}
|
||||
|
||||
diff --git a/lib/rules/no-named-as-default.js b/lib/rules/no-named-as-default.js
|
||||
index 63378a33a1c7da004c57a524cec1a1cddf23e210..c81b1f93b11628676158b79f1c4015911943cc7d 100644
|
||||
--- a/lib/rules/no-named-as-default.js
|
||||
+++ b/lib/rules/no-named-as-default.js
|
||||
@@ -18,7 +18,7 @@ module.exports = {
|
||||
// #566: default is a valid specifier
|
||||
if (defaultSpecifier[nameKey].name === 'default') {return;}
|
||||
|
||||
- var declaration = (0, _importDeclaration2['default'])(context);
|
||||
+ var declaration = (0, _importDeclaration2['default'])(context, defaultSpecifier);
|
||||
|
||||
var imports = _ExportMap2['default'].get(declaration.source.value, context);
|
||||
if (imports == null) {return;}
|
||||
diff --git a/lib/rules/no-namespace.js b/lib/rules/no-namespace.js
|
||||
index 2b0c783adea788101b779b17f977bbcb582cfd3f..a7f7b202ac7c4a342febef2a993586c4cc84fc7a 100644
|
||||
--- a/lib/rules/no-namespace.js
|
||||
+++ b/lib/rules/no-namespace.js
|
||||
@@ -43,7 +43,7 @@ var _docsUrl = require('../docsUrl');var _docsUrl2 = _interopRequireDefault(_doc
|
||||
return;
|
||||
}
|
||||
|
||||
- var scopeVariables = context.getScope().variables;
|
||||
+ var scopeVariables = context.getSourceCode().getScope(node).variables;
|
||||
var namespaceVariable = scopeVariables.find(function (variable) {return variable.defs[0].node === node;});
|
||||
var namespaceReferences = namespaceVariable.references;
|
||||
var namespaceIdentifiers = namespaceReferences.map(function (reference) {return reference.identifier;});
|
||||
diff --git a/package.json b/package.json
|
||||
index 5c0af48543483a21791fa23a4a583071d3551772..5deeac3d0accc3878ef0fc93dfb52a8ca7c46e84 100644
|
||||
--- a/package.json
|
||||
+++ b/package.json
|
||||
@@ -72,7 +72,7 @@
|
||||
"chai": "^4.3.10",
|
||||
"cross-env": "^4.0.0",
|
||||
"escope": "^3.6.0",
|
||||
- "eslint": "^2 || ^3 || ^4 || ^5 || ^6 || ^7.2.0 || ^8",
|
||||
+ "eslint": "^2 || ^3 || ^4 || ^5 || ^6 || ^7.2.0 || ^8 || ^9",
|
||||
"eslint-doc-generator": "^1.6.1",
|
||||
"eslint-import-resolver-node": "file:./resolvers/node",
|
||||
"eslint-import-resolver-typescript": "^1.0.2 || ^1.1.1",
|
||||
20
patches/vudio@2.1.1.patch
Normal file
20
patches/vudio@2.1.1.patch
Normal file
@ -0,0 +1,20 @@
|
||||
diff --git a/umd/vudio.js b/umd/vudio.js
|
||||
index d0d1127e57125ad4e77442af2db4a26998c7b385..c0b66bd4327c65c31dc6e588bfa4ae6ec70bd3b8 100644
|
||||
--- a/umd/vudio.js
|
||||
+++ b/umd/vudio.js
|
||||
@@ -147,7 +147,6 @@
|
||||
|
||||
source.connect(this.analyser);
|
||||
this.analyser.fftSize = this.option.accuracy * 2;
|
||||
- this.analyser.connect(audioContext.destination);
|
||||
|
||||
this.freqByteData = new Uint8Array(this.analyser.frequencyBinCount);
|
||||
|
||||
@@ -207,7 +206,6 @@
|
||||
|
||||
source.connect(this.analyser);
|
||||
this.analyser.fftSize = this.option.accuracy * 2;
|
||||
- this.analyser.connect(audioContext.destination);
|
||||
},
|
||||
|
||||
__rebuildData : function (freqByteData, horizontalAlign) {
|
||||
@ -1,38 +0,0 @@
|
||||
diff --git a/node_modules/youtubei.js/bundle/node.cjs b/node_modules/youtubei.js/bundle/node.cjs
|
||||
index 7e3072e..bf5be6a 100644
|
||||
--- a/node_modules/youtubei.js/bundle/node.cjs
|
||||
+++ b/node_modules/youtubei.js/bundle/node.cjs
|
||||
@@ -16969,7 +16969,13 @@ var _Cache_createCache;
|
||||
var meta_url = import_meta.url;
|
||||
var is_cjs = !meta_url;
|
||||
var __dirname__ = is_cjs ? __dirname : import_path.default.dirname((0, import_url.fileURLToPath)(meta_url));
|
||||
-var package_json = JSON.parse((0, import_fs.readFileSync)(import_path.default.resolve(__dirname__, is_cjs ? "../package.json" : "../../package.json"), "utf-8"));
|
||||
+var package_json = {
|
||||
+ homepage: "https://github.com/LuanRT/YouTube.js#readme",
|
||||
+ version: "6.4.1",
|
||||
+ bugs: {
|
||||
+ url: "https://github.com/LuanRT/YouTube.js/issues"
|
||||
+ }
|
||||
+};
|
||||
var repo_url = (_a3 = package_json.homepage) === null || _a3 === void 0 ? void 0 : _a3.split("#")[0];
|
||||
var Cache = class {
|
||||
constructor(persistent = false, persistent_directory) {
|
||||
diff --git a/node_modules/youtubei.js/dist/src/platform/node.js b/node_modules/youtubei.js/dist/src/platform/node.js
|
||||
index 0e1b2ca..17b437c 100644
|
||||
--- a/node_modules/youtubei.js/dist/src/platform/node.js
|
||||
+++ b/node_modules/youtubei.js/dist/src/platform/node.js
|
||||
@@ -16,7 +16,13 @@ import evaluate from './jsruntime/jinter.js';
|
||||
const meta_url = import.meta.url;
|
||||
const is_cjs = !meta_url;
|
||||
const __dirname__ = is_cjs ? __dirname : path.dirname(fileURLToPath(meta_url));
|
||||
-const package_json = JSON.parse(readFileSync(path.resolve(__dirname__, is_cjs ? '../package.json' : '../../package.json'), 'utf-8'));
|
||||
+const package_json = {
|
||||
+ homepage: "https://github.com/LuanRT/YouTube.js#readme",
|
||||
+ version: "6.4.1",
|
||||
+ bugs: {
|
||||
+ url: "https://github.com/LuanRT/YouTube.js/issues"
|
||||
+ }
|
||||
+};
|
||||
const repo_url = (_a = package_json.homepage) === null || _a === void 0 ? void 0 : _a.split('#')[0];
|
||||
class Cache {
|
||||
constructor(persistent = false, persistent_directory) {
|
||||
@ -1,19 +0,0 @@
|
||||
import { BrowserWindow } from 'electron';
|
||||
|
||||
import { loadAdBlockerEngine } from './blocker';
|
||||
import { shouldUseBlocklists } from './config';
|
||||
|
||||
import type { ConfigType } from '../../config/dynamic';
|
||||
|
||||
type AdBlockOptions = ConfigType<'adblocker'>;
|
||||
|
||||
export default async (win: BrowserWindow, options: AdBlockOptions) => {
|
||||
if (shouldUseBlocklists()) {
|
||||
await loadAdBlockerEngine(
|
||||
win.webContents.session,
|
||||
options.cache,
|
||||
options.additionalBlockLists,
|
||||
options.disableDefaultLists,
|
||||
);
|
||||
}
|
||||
};
|
||||
@ -1,72 +0,0 @@
|
||||
// Used for caching
|
||||
import path from 'node:path';
|
||||
import fs, { promises } from 'node:fs';
|
||||
|
||||
import { ElectronBlocker } from '@cliqz/adblocker-electron';
|
||||
import { app, net } from 'electron';
|
||||
|
||||
const SOURCES = [
|
||||
'https://raw.githubusercontent.com/kbinani/adblock-youtube-ads/master/signed.txt',
|
||||
// UBlock Origin
|
||||
'https://raw.githubusercontent.com/uBlockOrigin/uAssets/master/filters/filters.txt',
|
||||
'https://raw.githubusercontent.com/uBlockOrigin/uAssets/master/filters/filters-2020.txt',
|
||||
'https://raw.githubusercontent.com/uBlockOrigin/uAssets/master/filters/filters-2021.txt',
|
||||
'https://raw.githubusercontent.com/uBlockOrigin/uAssets/master/filters/filters-2022.txt',
|
||||
'https://raw.githubusercontent.com/uBlockOrigin/uAssets/master/filters/filters-2023.txt',
|
||||
// Fanboy Annoyances
|
||||
'https://secure.fanboy.co.nz/fanboy-annoyance_ubo.txt',
|
||||
];
|
||||
|
||||
export const loadAdBlockerEngine = async (
|
||||
session: Electron.Session | undefined = undefined,
|
||||
cache = true,
|
||||
additionalBlockLists = [],
|
||||
disableDefaultLists: boolean | unknown[] = false,
|
||||
) => {
|
||||
// Only use cache if no additional blocklists are passed
|
||||
let cacheDirectory: string;
|
||||
if (app.isPackaged) {
|
||||
cacheDirectory = path.join(app.getPath('userData'), 'adblock_cache');
|
||||
} else {
|
||||
cacheDirectory = path.resolve(__dirname, 'adblock_cache');
|
||||
}
|
||||
if (!fs.existsSync(cacheDirectory)) {
|
||||
fs.mkdirSync(cacheDirectory);
|
||||
}
|
||||
const cachingOptions
|
||||
= cache && additionalBlockLists.length === 0
|
||||
? {
|
||||
path: path.join(cacheDirectory, 'adblocker-engine.bin'),
|
||||
read: promises.readFile,
|
||||
write: promises.writeFile,
|
||||
}
|
||||
: undefined;
|
||||
const lists = [
|
||||
...(
|
||||
(disableDefaultLists && !Array.isArray(disableDefaultLists)) ||
|
||||
(Array.isArray(disableDefaultLists) && disableDefaultLists.length > 0) ? [] : SOURCES
|
||||
),
|
||||
...additionalBlockLists,
|
||||
];
|
||||
|
||||
try {
|
||||
const blocker = await ElectronBlocker.fromLists(
|
||||
(url: string) => net.fetch(url),
|
||||
lists,
|
||||
{
|
||||
// When generating the engine for caching, do not load network filters
|
||||
// So that enhancing the session works as expected
|
||||
// Allowing to define multiple webRequest listeners
|
||||
loadNetworkFilters: session !== undefined,
|
||||
},
|
||||
cachingOptions,
|
||||
);
|
||||
if (session) {
|
||||
blocker.enableBlockingInSession(session);
|
||||
}
|
||||
} catch (error) {
|
||||
console.log('Error loading adBlocker engine', error);
|
||||
}
|
||||
};
|
||||
|
||||
export default { loadAdBlockerEngine };
|
||||
@ -1,15 +0,0 @@
|
||||
/* eslint-disable @typescript-eslint/await-thenable */
|
||||
/* renderer */
|
||||
|
||||
import { blockers } from './blocker-types';
|
||||
|
||||
import { PluginConfig } from '../../config/dynamic';
|
||||
|
||||
const config = new PluginConfig('adblocker', { enableFront: true });
|
||||
|
||||
export const shouldUseBlocklists = () => config.get('blocker') !== blockers.InPlayer;
|
||||
|
||||
export default Object.assign(config, {
|
||||
shouldUseBlocklists,
|
||||
blockers,
|
||||
});
|
||||
3
plugins/adblocker/inject.d.ts
vendored
3
plugins/adblocker/inject.d.ts
vendored
@ -1,3 +0,0 @@
|
||||
const inject: () => void;
|
||||
|
||||
export default inject;
|
||||
@ -1,435 +0,0 @@
|
||||
/* eslint-disable */
|
||||
|
||||
// Source: https://addons.mozilla.org/en-US/firefox/addon/adblock-for-youtube/
|
||||
// https://robwu.nl/crxviewer/?crx=https%3A%2F%2Faddons.mozilla.org%2Fen-US%2Ffirefox%2Faddon%2Fadblock-for-youtube%2F
|
||||
|
||||
/*
|
||||
Parts of this code is derived from set-constant.js:
|
||||
https://github.com/gorhill/uBlock/blob/5de0ce975753b7565759ac40983d31978d1f84ca/assets/resources/scriptlets.js#L704
|
||||
*/
|
||||
module.exports = () => {
|
||||
{
|
||||
const pruner = function (o) {
|
||||
delete o.playerAds;
|
||||
delete o.adPlacements;
|
||||
//
|
||||
if (o.playerResponse) {
|
||||
delete o.playerResponse.playerAds;
|
||||
delete o.playerResponse.adPlacements;
|
||||
}
|
||||
|
||||
//
|
||||
return o;
|
||||
};
|
||||
|
||||
JSON.parse = new Proxy(JSON.parse, {
|
||||
apply() {
|
||||
return pruner(Reflect.apply(...arguments));
|
||||
},
|
||||
});
|
||||
|
||||
Response.prototype.json = new Proxy(Response.prototype.json, {
|
||||
apply() {
|
||||
return Reflect.apply(...arguments).then((o) => pruner(o));
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
(function () {
|
||||
let cValue = 'undefined';
|
||||
const chain = 'playerResponse.adPlacements';
|
||||
const thisScript = document.currentScript;
|
||||
//
|
||||
switch (cValue) {
|
||||
case 'null': {
|
||||
cValue = null;
|
||||
break;
|
||||
}
|
||||
|
||||
case "''": {
|
||||
cValue = '';
|
||||
break;
|
||||
}
|
||||
|
||||
case 'true': {
|
||||
cValue = true;
|
||||
break;
|
||||
}
|
||||
|
||||
case 'false': {
|
||||
cValue = false;
|
||||
break;
|
||||
}
|
||||
|
||||
case 'undefined': {
|
||||
cValue = undefined;
|
||||
break;
|
||||
}
|
||||
|
||||
case 'noopFunc': {
|
||||
cValue = function () {
|
||||
};
|
||||
|
||||
break;
|
||||
}
|
||||
|
||||
case 'trueFunc': {
|
||||
cValue = function () {
|
||||
return true;
|
||||
};
|
||||
|
||||
break;
|
||||
}
|
||||
|
||||
case 'falseFunc': {
|
||||
cValue = function () {
|
||||
return false;
|
||||
};
|
||||
|
||||
break;
|
||||
}
|
||||
|
||||
default: {
|
||||
if (/^\d+$/.test(cValue)) {
|
||||
cValue = Number.parseFloat(cValue);
|
||||
//
|
||||
if (isNaN(cValue)) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (Math.abs(cValue) > 0x7F_FF) {
|
||||
return;
|
||||
}
|
||||
} else {
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
//
|
||||
let aborted = false;
|
||||
const mustAbort = function (v) {
|
||||
if (aborted) {
|
||||
return true;
|
||||
}
|
||||
|
||||
aborted
|
||||
= v !== undefined
|
||||
&& v !== null
|
||||
&& cValue !== undefined
|
||||
&& cValue !== null
|
||||
&& typeof v !== typeof cValue;
|
||||
return aborted;
|
||||
};
|
||||
|
||||
/*
|
||||
Support multiple trappers for the same property:
|
||||
https://github.com/uBlockOrigin/uBlock-issues/issues/156
|
||||
*/
|
||||
|
||||
const trapProp = function (owner, prop, configurable, handler) {
|
||||
if (handler.init(owner[prop]) === false) {
|
||||
return;
|
||||
}
|
||||
|
||||
//
|
||||
const odesc = Object.getOwnPropertyDescriptor(owner, prop);
|
||||
let previousGetter;
|
||||
let previousSetter;
|
||||
if (odesc instanceof Object) {
|
||||
if (odesc.configurable === false) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (odesc.get instanceof Function) {
|
||||
previousGetter = odesc.get;
|
||||
}
|
||||
|
||||
if (odesc.set instanceof Function) {
|
||||
previousSetter = odesc.set;
|
||||
}
|
||||
}
|
||||
|
||||
//
|
||||
Object.defineProperty(owner, prop, {
|
||||
configurable,
|
||||
get() {
|
||||
if (previousGetter !== undefined) {
|
||||
previousGetter();
|
||||
}
|
||||
|
||||
//
|
||||
return handler.getter();
|
||||
},
|
||||
set(a) {
|
||||
if (previousSetter !== undefined) {
|
||||
previousSetter(a);
|
||||
}
|
||||
|
||||
//
|
||||
handler.setter(a);
|
||||
},
|
||||
});
|
||||
};
|
||||
|
||||
const trapChain = function (owner, chain) {
|
||||
const pos = chain.indexOf('.');
|
||||
if (pos === -1) {
|
||||
trapProp(owner, chain, false, {
|
||||
v: undefined,
|
||||
getter() {
|
||||
return document.currentScript === thisScript ? this.v : cValue;
|
||||
},
|
||||
setter(a) {
|
||||
if (mustAbort(a) === false) {
|
||||
return;
|
||||
}
|
||||
|
||||
cValue = a;
|
||||
},
|
||||
init(v) {
|
||||
if (mustAbort(v)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
//
|
||||
this.v = v;
|
||||
return true;
|
||||
},
|
||||
});
|
||||
//
|
||||
return;
|
||||
}
|
||||
|
||||
//
|
||||
const prop = chain.slice(0, pos);
|
||||
const v = owner[prop];
|
||||
//
|
||||
chain = chain.slice(pos + 1);
|
||||
if (v instanceof Object || (typeof v === 'object' && v !== null)) {
|
||||
trapChain(v, chain);
|
||||
return;
|
||||
}
|
||||
|
||||
//
|
||||
trapProp(owner, prop, true, {
|
||||
v: undefined,
|
||||
getter() {
|
||||
return this.v;
|
||||
},
|
||||
setter(a) {
|
||||
this.v = a;
|
||||
if (a instanceof Object) {
|
||||
trapChain(a, chain);
|
||||
}
|
||||
},
|
||||
init(v) {
|
||||
this.v = v;
|
||||
return true;
|
||||
},
|
||||
});
|
||||
};
|
||||
|
||||
//
|
||||
trapChain(window, chain);
|
||||
})();
|
||||
|
||||
(function () {
|
||||
let cValue = 'undefined';
|
||||
const thisScript = document.currentScript;
|
||||
const chain = 'ytInitialPlayerResponse.adPlacements';
|
||||
//
|
||||
switch (cValue) {
|
||||
case 'null': {
|
||||
cValue = null;
|
||||
break;
|
||||
}
|
||||
|
||||
case "''": {
|
||||
cValue = '';
|
||||
break;
|
||||
}
|
||||
|
||||
case 'true': {
|
||||
cValue = true;
|
||||
break;
|
||||
}
|
||||
|
||||
case 'false': {
|
||||
cValue = false;
|
||||
break;
|
||||
}
|
||||
|
||||
case 'undefined': {
|
||||
cValue = undefined;
|
||||
break;
|
||||
}
|
||||
|
||||
case 'noopFunc': {
|
||||
cValue = function () {
|
||||
};
|
||||
|
||||
break;
|
||||
}
|
||||
|
||||
case 'trueFunc': {
|
||||
cValue = function () {
|
||||
return true;
|
||||
};
|
||||
|
||||
break;
|
||||
}
|
||||
|
||||
case 'falseFunc': {
|
||||
cValue = function () {
|
||||
return false;
|
||||
};
|
||||
|
||||
break;
|
||||
}
|
||||
|
||||
default: {
|
||||
if (/^\d+$/.test(cValue)) {
|
||||
cValue = Number.parseFloat(cValue);
|
||||
//
|
||||
if (isNaN(cValue)) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (Math.abs(cValue) > 0x7F_FF) {
|
||||
return;
|
||||
}
|
||||
} else {
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
//
|
||||
let aborted = false;
|
||||
const mustAbort = function (v) {
|
||||
if (aborted) {
|
||||
return true;
|
||||
}
|
||||
|
||||
aborted
|
||||
= v !== undefined
|
||||
&& v !== null
|
||||
&& cValue !== undefined
|
||||
&& cValue !== null
|
||||
&& typeof v !== typeof cValue;
|
||||
return aborted;
|
||||
};
|
||||
|
||||
/*
|
||||
Support multiple trappers for the same property:
|
||||
https://github.com/uBlockOrigin/uBlock-issues/issues/156
|
||||
*/
|
||||
|
||||
const trapProp = function (owner, prop, configurable, handler) {
|
||||
if (handler.init(owner[prop]) === false) {
|
||||
return;
|
||||
}
|
||||
|
||||
//
|
||||
const odesc = Object.getOwnPropertyDescriptor(owner, prop);
|
||||
let previousGetter;
|
||||
let previousSetter;
|
||||
if (odesc instanceof Object) {
|
||||
if (odesc.configurable === false) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (odesc.get instanceof Function) {
|
||||
previousGetter = odesc.get;
|
||||
}
|
||||
|
||||
if (odesc.set instanceof Function) {
|
||||
previousSetter = odesc.set;
|
||||
}
|
||||
}
|
||||
|
||||
//
|
||||
Object.defineProperty(owner, prop, {
|
||||
configurable,
|
||||
get() {
|
||||
if (previousGetter !== undefined) {
|
||||
previousGetter();
|
||||
}
|
||||
|
||||
//
|
||||
return handler.getter();
|
||||
},
|
||||
set(a) {
|
||||
if (previousSetter !== undefined) {
|
||||
previousSetter(a);
|
||||
}
|
||||
|
||||
//
|
||||
handler.setter(a);
|
||||
},
|
||||
});
|
||||
};
|
||||
|
||||
const trapChain = function (owner, chain) {
|
||||
const pos = chain.indexOf('.');
|
||||
if (pos === -1) {
|
||||
trapProp(owner, chain, false, {
|
||||
v: undefined,
|
||||
getter() {
|
||||
return document.currentScript === thisScript ? this.v : cValue;
|
||||
},
|
||||
setter(a) {
|
||||
if (mustAbort(a) === false) {
|
||||
return;
|
||||
}
|
||||
|
||||
cValue = a;
|
||||
},
|
||||
init(v) {
|
||||
if (mustAbort(v)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
//
|
||||
this.v = v;
|
||||
return true;
|
||||
},
|
||||
});
|
||||
//
|
||||
return;
|
||||
}
|
||||
|
||||
//
|
||||
const prop = chain.slice(0, pos);
|
||||
const v = owner[prop];
|
||||
//
|
||||
chain = chain.slice(pos + 1);
|
||||
if (v instanceof Object || (typeof v === 'object' && v !== null)) {
|
||||
trapChain(v, chain);
|
||||
return;
|
||||
}
|
||||
|
||||
//
|
||||
trapProp(owner, prop, true, {
|
||||
v: undefined,
|
||||
getter() {
|
||||
return this.v;
|
||||
},
|
||||
setter(a) {
|
||||
this.v = a;
|
||||
if (a instanceof Object) {
|
||||
trapChain(a, chain);
|
||||
}
|
||||
},
|
||||
init(v) {
|
||||
this.v = v;
|
||||
return true;
|
||||
},
|
||||
});
|
||||
};
|
||||
|
||||
//
|
||||
trapChain(window, chain);
|
||||
})();
|
||||
};
|
||||
@ -1,21 +0,0 @@
|
||||
import config from './config';
|
||||
|
||||
import { blockers } from './blocker-types';
|
||||
|
||||
import { MenuTemplate } from '../../menu';
|
||||
|
||||
export default (): MenuTemplate => {
|
||||
return [
|
||||
{
|
||||
label: 'Blocker',
|
||||
submenu: Object.values(blockers).map((blocker: string) => ({
|
||||
label: blocker,
|
||||
type: 'radio',
|
||||
checked: (config.get('blocker') || blockers.WithBlocklists) === blocker,
|
||||
click() {
|
||||
config.set('blocker', blocker);
|
||||
},
|
||||
})),
|
||||
},
|
||||
];
|
||||
};
|
||||
@ -1,15 +0,0 @@
|
||||
import config, { shouldUseBlocklists } from './config';
|
||||
import inject from './inject';
|
||||
import injectCliqzPreload from './inject-cliqz-preload';
|
||||
|
||||
import { blockers } from './blocker-types';
|
||||
|
||||
export default async () => {
|
||||
if (shouldUseBlocklists()) {
|
||||
// Preload adblocker to inject scripts/styles
|
||||
await injectCliqzPreload();
|
||||
// eslint-disable-next-line @typescript-eslint/await-thenable
|
||||
} else if ((config.get('blocker')) === blockers.InPlayer) {
|
||||
inject();
|
||||
}
|
||||
};
|
||||
@ -1,9 +0,0 @@
|
||||
import { BrowserWindow } from 'electron';
|
||||
|
||||
import style from './style.css';
|
||||
|
||||
import { injectCSS } from '../utils';
|
||||
|
||||
export default (win: BrowserWindow) => {
|
||||
injectCSS(win.webContents, style);
|
||||
};
|
||||
@ -1,127 +0,0 @@
|
||||
import { FastAverageColor } from 'fast-average-color';
|
||||
|
||||
import { ConfigType } from '../../config/dynamic';
|
||||
|
||||
function hexToHSL(H: string) {
|
||||
// Convert hex to RGB first
|
||||
let r = 0;
|
||||
let g = 0;
|
||||
let b = 0;
|
||||
if (H.length == 4) {
|
||||
r = Number('0x' + H[1] + H[1]);
|
||||
g = Number('0x' + H[2] + H[2]);
|
||||
b = Number('0x' + H[3] + H[3]);
|
||||
} else if (H.length == 7) {
|
||||
r = Number('0x' + H[1] + H[2]);
|
||||
g = Number('0x' + H[3] + H[4]);
|
||||
b = Number('0x' + H[5] + H[6]);
|
||||
}
|
||||
// Then to HSL
|
||||
r /= 255;
|
||||
g /= 255;
|
||||
b /= 255;
|
||||
const cmin = Math.min(r, g, b);
|
||||
const cmax = Math.max(r, g, b);
|
||||
const delta = cmax - cmin;
|
||||
let h: number;
|
||||
let s: number;
|
||||
let l: number;
|
||||
|
||||
if (delta == 0) {
|
||||
h = 0;
|
||||
} else if (cmax == r) {
|
||||
h = ((g - b) / delta) % 6;
|
||||
} else if (cmax == g) {
|
||||
h = ((b - r) / delta) + 2;
|
||||
} else {
|
||||
h = ((r - g) / delta) + 4;
|
||||
}
|
||||
|
||||
h = Math.round(h * 60);
|
||||
|
||||
if (h < 0) {
|
||||
h += 360;
|
||||
}
|
||||
|
||||
l = (cmax + cmin) / 2;
|
||||
s = delta == 0 ? 0 : delta / (1 - Math.abs((2 * l) - 1));
|
||||
s = +(s * 100).toFixed(1);
|
||||
l = +(l * 100).toFixed(1);
|
||||
|
||||
//return "hsl(" + h + "," + s + "%," + l + "%)";
|
||||
return [h,s,l];
|
||||
}
|
||||
|
||||
let hue = 0;
|
||||
let saturation = 0;
|
||||
let lightness = 0;
|
||||
|
||||
function changeElementColor(element: HTMLElement | null, hue: number, saturation: number, lightness: number){
|
||||
if (element) {
|
||||
element.style.backgroundColor = `hsl(${hue}, ${saturation}%, ${lightness}%)`;
|
||||
}
|
||||
}
|
||||
|
||||
export default (_: ConfigType<'album-color-theme'>) => {
|
||||
// updated elements
|
||||
const playerPage = document.querySelector<HTMLElement>('#player-page');
|
||||
const navBarBackground = document.querySelector<HTMLElement>('#nav-bar-background');
|
||||
const ytmusicPlayerBar = document.querySelector<HTMLElement>('ytmusic-player-bar');
|
||||
const playerBarBackground = document.querySelector<HTMLElement>('#player-bar-background');
|
||||
const sidebarBig = document.querySelector<HTMLElement>('#guide-wrapper');
|
||||
const sidebarSmall = document.querySelector<HTMLElement>('#mini-guide-background');
|
||||
const ytmusicAppLayout = document.querySelector<HTMLElement>('#layout');
|
||||
|
||||
const observer = new MutationObserver((mutationsList) => {
|
||||
for (const mutation of mutationsList) {
|
||||
if (mutation.type === 'attributes') {
|
||||
const isPageOpen = ytmusicAppLayout?.hasAttribute('player-page-open');
|
||||
if (isPageOpen) {
|
||||
changeElementColor(sidebarSmall, hue, saturation, lightness - 30);
|
||||
} else {
|
||||
if (sidebarSmall) {
|
||||
sidebarSmall.style.backgroundColor = 'black';
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
if (playerPage) {
|
||||
observer.observe(playerPage, { attributes: true });
|
||||
}
|
||||
|
||||
document.addEventListener('apiLoaded', (apiEvent) => {
|
||||
const fastAverageColor = new FastAverageColor();
|
||||
|
||||
apiEvent.detail.addEventListener('videodatachange', (name: string) => {
|
||||
if (name === 'dataloaded') {
|
||||
const playerResponse = apiEvent.detail.getPlayerResponse();
|
||||
const thumbnail = playerResponse?.videoDetails?.thumbnail?.thumbnails?.at(0);
|
||||
if (thumbnail) {
|
||||
fastAverageColor.getColorAsync(thumbnail.url)
|
||||
.then((albumColor) => {
|
||||
if (albumColor) {
|
||||
[hue, saturation, lightness] = hexToHSL(albumColor.hex);
|
||||
changeElementColor(playerPage, hue, saturation, lightness - 30);
|
||||
changeElementColor(navBarBackground, hue, saturation, lightness - 15);
|
||||
changeElementColor(ytmusicPlayerBar, hue, saturation, lightness - 15);
|
||||
changeElementColor(playerBarBackground, hue, saturation, lightness - 15);
|
||||
changeElementColor(sidebarBig, hue, saturation, lightness - 15);
|
||||
if (ytmusicAppLayout?.hasAttribute('player-page-open')) {
|
||||
changeElementColor(sidebarSmall, hue, saturation, lightness - 30);
|
||||
}
|
||||
const ytRightClickList = document.querySelector<HTMLElement>('tp-yt-paper-listbox');
|
||||
changeElementColor(ytRightClickList, hue, saturation, lightness - 15);
|
||||
} else {
|
||||
if (playerPage) {
|
||||
playerPage.style.backgroundColor = '#000000';
|
||||
}
|
||||
}
|
||||
})
|
||||
.catch((e) => console.error(e));
|
||||
}
|
||||
}
|
||||
});
|
||||
});
|
||||
};
|
||||
@ -1,29 +0,0 @@
|
||||
yt-page-navigation-progress {
|
||||
--yt-page-navigation-container-color: #00000046 !important;
|
||||
--yt-page-navigation-progress-color: white !important;
|
||||
}
|
||||
|
||||
#player-page {
|
||||
transition: transform 300ms,background-color 300ms cubic-bezier(0.2,0,0.6,1) !important;
|
||||
}
|
||||
|
||||
#nav-bar-background {
|
||||
transition: opacity 200ms,background-color 300ms cubic-bezier(0.2,0,0.6,1) !important;
|
||||
}
|
||||
|
||||
#mini-guide-background {
|
||||
transition: opacity 200ms,background-color 300ms cubic-bezier(0.2,0,0.6,1) !important;
|
||||
border-right: 0px !important;
|
||||
}
|
||||
|
||||
#guide-wrapper {
|
||||
transition: opacity 200ms,background-color 300ms cubic-bezier(0.2,0,0.6,1) !important;
|
||||
}
|
||||
|
||||
#img, #player, .song-media-controls.style-scope.ytmusic-player {
|
||||
border-radius: 2% !important;
|
||||
}
|
||||
|
||||
#items {
|
||||
border-radius: 10px !important;
|
||||
}
|
||||
@ -1,10 +0,0 @@
|
||||
import { BrowserWindow } from 'electron';
|
||||
|
||||
import style from './style.css';
|
||||
|
||||
import { injectCSS } from '../utils';
|
||||
|
||||
|
||||
export default (win: BrowserWindow) => {
|
||||
injectCSS(win.webContents, style);
|
||||
};
|
||||
@ -1,138 +0,0 @@
|
||||
import { ConfigType } from '../../config/dynamic';
|
||||
|
||||
export default (_: ConfigType<'ambient-mode'>) => {
|
||||
const interpolationTime = 3000; // interpolation time (ms)
|
||||
const framerate = 30; // frame
|
||||
const qualityRatio = 50; // width size (pixel)
|
||||
|
||||
let unregister: (() => void) | null = null;
|
||||
|
||||
const injectBlurVideo = (): (() => void) | null => {
|
||||
const songVideo = document.querySelector<HTMLDivElement>('#song-video');
|
||||
const video = document.querySelector<HTMLVideoElement>('#song-video .html5-video-container > video');
|
||||
const wrapper = document.querySelector('#song-video > .player-wrapper');
|
||||
|
||||
if (!songVideo) return null;
|
||||
if (!video) return null;
|
||||
if (!wrapper) return null;
|
||||
|
||||
const blurCanvas = document.createElement('canvas');
|
||||
blurCanvas.classList.add('html5-blur-canvas');
|
||||
|
||||
const context = blurCanvas.getContext('2d', { willReadFrequently: true });
|
||||
|
||||
/* effect */
|
||||
let lastEffectWorkId: number | null = null;
|
||||
let lastImageData: ImageData | null = null;
|
||||
|
||||
const onSync = () => {
|
||||
if (typeof lastEffectWorkId === 'number') cancelAnimationFrame(lastEffectWorkId);
|
||||
|
||||
lastEffectWorkId = requestAnimationFrame(() => {
|
||||
if (!context) return;
|
||||
|
||||
const width = qualityRatio;
|
||||
let height = Math.max(Math.floor(blurCanvas.height / blurCanvas.width * width), 1);
|
||||
if (!Number.isFinite(height)) height = width;
|
||||
|
||||
context.globalAlpha = 1;
|
||||
if (lastImageData) {
|
||||
const frameOffset = (1 / framerate) * (1000 / interpolationTime);
|
||||
context.globalAlpha = 1 - (frameOffset * 2); // because of alpha value must be < 1
|
||||
context.putImageData(lastImageData, 0, 0);
|
||||
context.globalAlpha = frameOffset;
|
||||
}
|
||||
context.drawImage(video, 0, 0, width, height);
|
||||
|
||||
const nowImageData = context.getImageData(0, 0, width, height);
|
||||
lastImageData = nowImageData;
|
||||
|
||||
lastEffectWorkId = null;
|
||||
});
|
||||
};
|
||||
|
||||
const applyVideoAttributes = () => {
|
||||
const rect = video.getBoundingClientRect();
|
||||
|
||||
const newWidth = Math.floor(video.width || rect.width);
|
||||
const newHeight = Math.floor(video.height || rect.height);
|
||||
|
||||
if (newWidth === 0 || newHeight === 0) return;
|
||||
|
||||
blurCanvas.width = qualityRatio;
|
||||
blurCanvas.height = Math.floor(newHeight / newWidth * qualityRatio);
|
||||
blurCanvas.style.width = `${newWidth}px`;
|
||||
blurCanvas.style.height = `${newHeight}px`;
|
||||
};
|
||||
|
||||
const observer = new MutationObserver((mutations) => {
|
||||
mutations.forEach((mutation) => {
|
||||
if (mutation.type === 'attributes') {
|
||||
applyVideoAttributes();
|
||||
}
|
||||
});
|
||||
});
|
||||
const resizeObserver = new ResizeObserver(() => {
|
||||
applyVideoAttributes();
|
||||
});
|
||||
|
||||
/* hooking */
|
||||
let canvasInterval: NodeJS.Timeout | null = null;
|
||||
canvasInterval = setInterval(onSync, Math.max(1, Math.ceil(1000 / framerate)));
|
||||
applyVideoAttributes();
|
||||
observer.observe(songVideo, { attributes: true });
|
||||
resizeObserver.observe(songVideo);
|
||||
window.addEventListener('resize', applyVideoAttributes);
|
||||
|
||||
const onPause = () => {
|
||||
if (canvasInterval) clearInterval(canvasInterval);
|
||||
canvasInterval = null;
|
||||
};
|
||||
const onPlay = () => {
|
||||
if (canvasInterval) clearInterval(canvasInterval);
|
||||
canvasInterval = setInterval(onSync, Math.max(1, Math.ceil(1000 / framerate)));
|
||||
};
|
||||
songVideo.addEventListener('pause', onPause);
|
||||
songVideo.addEventListener('play', onPlay);
|
||||
|
||||
/* injecting */
|
||||
wrapper.prepend(blurCanvas);
|
||||
|
||||
/* cleanup */
|
||||
return () => {
|
||||
if (canvasInterval) clearInterval(canvasInterval);
|
||||
|
||||
songVideo.removeEventListener('pause', onPause);
|
||||
songVideo.removeEventListener('play', onPlay);
|
||||
|
||||
observer.disconnect();
|
||||
resizeObserver.disconnect();
|
||||
window.removeEventListener('resize', applyVideoAttributes);
|
||||
|
||||
wrapper.removeChild(blurCanvas);
|
||||
};
|
||||
};
|
||||
|
||||
|
||||
const playerPage = document.querySelector<HTMLElement>('#player-page');
|
||||
const ytmusicAppLayout = document.querySelector<HTMLElement>('#layout');
|
||||
|
||||
const observer = new MutationObserver((mutationsList) => {
|
||||
for (const mutation of mutationsList) {
|
||||
if (mutation.type === 'attributes') {
|
||||
const isPageOpen = ytmusicAppLayout?.hasAttribute('player-page-open');
|
||||
if (isPageOpen) {
|
||||
unregister?.();
|
||||
unregister = injectBlurVideo() ?? null;
|
||||
} else {
|
||||
unregister?.();
|
||||
unregister = null;
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
if (playerPage) {
|
||||
observer.observe(playerPage, { attributes: true });
|
||||
}
|
||||
};
|
||||
@ -1,7 +0,0 @@
|
||||
#song-video canvas.html5-blur-canvas{
|
||||
position: absolute;
|
||||
left: 0;
|
||||
top: 0;
|
||||
|
||||
filter: blur(100px);
|
||||
}
|
||||
@ -1,17 +0,0 @@
|
||||
export default () =>
|
||||
document.addEventListener('audioCanPlay', (e) => {
|
||||
const { audioContext } = e.detail;
|
||||
|
||||
const compressor = audioContext.createDynamicsCompressor();
|
||||
compressor.threshold.value = -50;
|
||||
compressor.ratio.value = 12;
|
||||
compressor.knee.value = 40;
|
||||
compressor.attack.value = 0;
|
||||
compressor.release.value = 0.25;
|
||||
|
||||
e.detail.audioSource.connect(compressor);
|
||||
compressor.connect(audioContext.destination);
|
||||
}, {
|
||||
once: true, // Only create the audio compressor once, not on each video
|
||||
passive: true,
|
||||
});
|
||||
@ -1,9 +0,0 @@
|
||||
import { BrowserWindow } from 'electron';
|
||||
|
||||
import style from './style.css';
|
||||
|
||||
import { injectCSS } from '../utils';
|
||||
|
||||
export default (win: BrowserWindow) => {
|
||||
injectCSS(win.webContents, style);
|
||||
};
|
||||
@ -1,10 +0,0 @@
|
||||
#nav-bar-background,
|
||||
#header.ytmusic-item-section-renderer,
|
||||
ytmusic-tabs {
|
||||
background: rgba(0, 0, 0, 0.3) !important;
|
||||
backdrop-filter: blur(8px) !important;
|
||||
}
|
||||
|
||||
#nav-bar-divider {
|
||||
display: none !important;
|
||||
}
|
||||
@ -1,4 +0,0 @@
|
||||
export default async () => {
|
||||
// See https://github.com/zerodytrash/Simple-YouTube-Age-Restriction-Bypass#userscript
|
||||
await import('simple-youtube-age-restriction-bypass');
|
||||
};
|
||||
@ -1,19 +0,0 @@
|
||||
import { BrowserWindow, ipcMain } from 'electron';
|
||||
import prompt from 'custom-electron-prompt';
|
||||
|
||||
import promptOptions from '../../providers/prompt-options';
|
||||
|
||||
export default (win: BrowserWindow) => {
|
||||
ipcMain.handle('captionsSelector', async (_, captionLabels: Record<string, string>, currentIndex: string) => await prompt(
|
||||
{
|
||||
title: 'Choose Caption',
|
||||
label: `Current Caption: ${captionLabels[currentIndex] || 'None'}`,
|
||||
type: 'select',
|
||||
value: currentIndex,
|
||||
selectOptions: captionLabels,
|
||||
resizable: true,
|
||||
...promptOptions(),
|
||||
},
|
||||
win,
|
||||
));
|
||||
};
|
||||
@ -1,4 +0,0 @@
|
||||
import { PluginConfig } from '../../config/dynamic';
|
||||
|
||||
const config = new PluginConfig('captions-selector', { enableFront: true });
|
||||
export default config;
|
||||
@ -1,101 +0,0 @@
|
||||
/* eslint-disable @typescript-eslint/await-thenable */
|
||||
/* renderer */
|
||||
|
||||
import { ipcRenderer } from 'electron';
|
||||
|
||||
import configProvider from './config';
|
||||
|
||||
import CaptionsSettingsButtonHTML from './templates/captions-settings-template.html';
|
||||
|
||||
import { ElementFromHtml } from '../utils';
|
||||
import { YoutubePlayer } from '../../types/youtube-player';
|
||||
|
||||
import type { ConfigType } from '../../config/dynamic';
|
||||
|
||||
interface LanguageOptions {
|
||||
displayName: string;
|
||||
id: string | null;
|
||||
is_default: boolean;
|
||||
is_servable: boolean;
|
||||
is_translateable: boolean;
|
||||
kind: string;
|
||||
languageCode: string; // 2 length
|
||||
languageName: string;
|
||||
name: string | null;
|
||||
vss_id: string;
|
||||
}
|
||||
|
||||
let config: ConfigType<'captions-selector'>;
|
||||
|
||||
const $ = <Element extends HTMLElement>(selector: string): Element => document.querySelector(selector)!;
|
||||
|
||||
const captionsSettingsButton = ElementFromHtml(CaptionsSettingsButtonHTML);
|
||||
|
||||
export default async () => {
|
||||
// RENDERER
|
||||
config = await configProvider.getAll();
|
||||
|
||||
configProvider.subscribeAll((newConfig) => {
|
||||
config = newConfig;
|
||||
});
|
||||
document.addEventListener('apiLoaded', (event) => setup(event.detail), { once: true, passive: true });
|
||||
};
|
||||
|
||||
function setup(api: YoutubePlayer) {
|
||||
$('.right-controls-buttons').append(captionsSettingsButton);
|
||||
|
||||
let captionTrackList = api.getOption<LanguageOptions[]>('captions', 'tracklist') ?? [];
|
||||
|
||||
$('video').addEventListener('srcChanged', () => {
|
||||
if (config.disableCaptions) {
|
||||
setTimeout(() => api.unloadModule('captions'), 100);
|
||||
captionsSettingsButton.style.display = 'none';
|
||||
return;
|
||||
}
|
||||
|
||||
api.loadModule('captions');
|
||||
|
||||
setTimeout(() => {
|
||||
captionTrackList = api.getOption('captions', 'tracklist') ?? [];
|
||||
|
||||
if (config.autoload && config.lastCaptionsCode) {
|
||||
api.setOption('captions', 'track', {
|
||||
languageCode: config.lastCaptionsCode,
|
||||
});
|
||||
}
|
||||
|
||||
captionsSettingsButton.style.display = captionTrackList?.length
|
||||
? 'inline-block'
|
||||
: 'none';
|
||||
}, 250);
|
||||
});
|
||||
|
||||
captionsSettingsButton.addEventListener('click', async () => {
|
||||
if (captionTrackList?.length) {
|
||||
const currentCaptionTrack = api.getOption<LanguageOptions>('captions', 'track')!;
|
||||
let currentIndex = currentCaptionTrack
|
||||
? captionTrackList.indexOf(captionTrackList.find((track) => track.languageCode === currentCaptionTrack.languageCode)!)
|
||||
: null;
|
||||
|
||||
const captionLabels = [
|
||||
...captionTrackList.map((track) => track.displayName),
|
||||
'None',
|
||||
];
|
||||
|
||||
currentIndex = await ipcRenderer.invoke('captionsSelector', captionLabels, currentIndex) as number;
|
||||
if (currentIndex === null) {
|
||||
return;
|
||||
}
|
||||
|
||||
const newCaptions = captionTrackList[currentIndex];
|
||||
configProvider.set('lastCaptionsCode', newCaptions?.languageCode);
|
||||
if (newCaptions) {
|
||||
api.setOption('captions', 'track', { languageCode: newCaptions.languageCode });
|
||||
} else {
|
||||
api.setOption('captions', 'track', {});
|
||||
}
|
||||
|
||||
setTimeout(() => api.playVideo());
|
||||
}
|
||||
});
|
||||
}
|
||||
@ -1,22 +0,0 @@
|
||||
import config from './config';
|
||||
|
||||
import { MenuTemplate } from '../../menu';
|
||||
|
||||
export default (): MenuTemplate => [
|
||||
{
|
||||
label: 'Automatically select last used caption',
|
||||
type: 'checkbox',
|
||||
checked: config.get('autoload'),
|
||||
click(item) {
|
||||
config.set('autoload', item.checked);
|
||||
},
|
||||
},
|
||||
{
|
||||
label: 'No captions by default',
|
||||
type: 'checkbox',
|
||||
checked: config.get('disableCaptions'),
|
||||
click(item) {
|
||||
config.set('disableCaptions', item.checked);
|
||||
},
|
||||
},
|
||||
];
|
||||
@ -1,17 +0,0 @@
|
||||
<tp-yt-paper-icon-button aria-disabled="false" aria-label="Open captions selector"
|
||||
class="player-captions-button style-scope ytmusic-player" icon="yt-icons:subtitles"
|
||||
role="button" tabindex="0"
|
||||
title="Open captions selector">
|
||||
<tp-yt-iron-icon class="style-scope tp-yt-paper-icon-button" id="icon">
|
||||
<svg class="style-scope yt-icon"
|
||||
focusable="false" preserveAspectRatio="xMidYMid meet"
|
||||
style="pointer-events: none; display: block; width: 100%; height: 100%;"
|
||||
viewBox="0 0 24 24">
|
||||
<g class="style-scope yt-icon">
|
||||
<path
|
||||
class="style-scope tp-yt-iron-icon"
|
||||
d="M20 4H4c-1.103 0-2 .897-2 2v12c0 1.103.897 2 2 2h16c1.103 0 2-.897 2-2V6c0-1.103-.897-2-2-2zm-9 6H8v4h3v2H8c-1.103 0-2-.897-2-2v-4c0-1.103.897-2 2-2h3v2zm7 0h-3v4h3v2h-3c-1.103 0-2-.897-2-2v-4c0-1.103.897-2 2-2h3v2z"></path>
|
||||
</g>
|
||||
</svg>
|
||||
</tp-yt-iron-icon>
|
||||
</tp-yt-paper-icon-button>
|
||||
@ -1,10 +0,0 @@
|
||||
export default () => {
|
||||
const compactSidebar = document.querySelector('#mini-guide');
|
||||
const isCompactSidebarDisabled
|
||||
= compactSidebar === null
|
||||
|| window.getComputedStyle(compactSidebar).display === 'none';
|
||||
|
||||
if (isCompactSidebarDisabled) {
|
||||
document.querySelector<HTMLButtonElement>('#button')?.click();
|
||||
}
|
||||
};
|
||||
@ -1,11 +0,0 @@
|
||||
import { ipcMain } from 'electron';
|
||||
import { Innertube } from 'youtubei.js';
|
||||
|
||||
export default async () => {
|
||||
const yt = await Innertube.create();
|
||||
|
||||
ipcMain.handle('audio-url', async (_, videoID: string) => {
|
||||
const info = await yt.getBasicInfo(videoID);
|
||||
return info.streaming_data?.formats[0].decipher(yt.session.player);
|
||||
});
|
||||
};
|
||||
@ -1,4 +0,0 @@
|
||||
import { PluginConfig } from '../../config/dynamic';
|
||||
|
||||
const config = new PluginConfig('crossfade', { enableFront: true });
|
||||
export default config;
|
||||
@ -1,164 +0,0 @@
|
||||
/* eslint-disable @typescript-eslint/await-thenable */
|
||||
/* renderer */
|
||||
|
||||
import { ipcRenderer } from 'electron';
|
||||
import { Howl } from 'howler';
|
||||
|
||||
// Extracted from https://github.com/bitfasching/VolumeFader
|
||||
import { VolumeFader } from './fader';
|
||||
|
||||
import configProvider from './config';
|
||||
|
||||
import defaultConfigs from '../../config/defaults';
|
||||
|
||||
import type { ConfigType } from '../../config/dynamic';
|
||||
|
||||
let transitionAudio: Howl; // Howler audio used to fade out the current music
|
||||
let firstVideo = true;
|
||||
let waitForTransition: Promise<unknown>;
|
||||
|
||||
const defaultConfig = defaultConfigs.plugins.crossfade;
|
||||
|
||||
let config: ConfigType<'crossfade'>;
|
||||
|
||||
const configGetNumber = (key: keyof ConfigType<'crossfade'>): number => Number(config[key]) || (defaultConfig[key] as number);
|
||||
|
||||
const getStreamURL = async (videoID: string) => ipcRenderer.invoke('audio-url', videoID) as Promise<string>;
|
||||
|
||||
const getVideoIDFromURL = (url: string) => new URLSearchParams(url.split('?')?.at(-1)).get('v');
|
||||
|
||||
const isReadyToCrossfade = () => transitionAudio && transitionAudio.state() === 'loaded';
|
||||
|
||||
const watchVideoIDChanges = (cb: (id: string) => void) => {
|
||||
window.navigation.addEventListener('navigate', (event) => {
|
||||
const currentVideoID = getVideoIDFromURL(
|
||||
(event.currentTarget as Navigation).currentEntry?.url ?? '',
|
||||
);
|
||||
const nextVideoID = getVideoIDFromURL(event.destination.url ?? '');
|
||||
|
||||
if (
|
||||
nextVideoID
|
||||
&& currentVideoID
|
||||
&& (firstVideo || nextVideoID !== currentVideoID)
|
||||
) {
|
||||
if (isReadyToCrossfade()) {
|
||||
crossfade(() => {
|
||||
cb(nextVideoID);
|
||||
});
|
||||
} else {
|
||||
cb(nextVideoID);
|
||||
firstVideo = false;
|
||||
}
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
const createAudioForCrossfade = (url: string) => {
|
||||
if (transitionAudio) {
|
||||
transitionAudio.unload();
|
||||
}
|
||||
|
||||
transitionAudio = new Howl({
|
||||
src: url,
|
||||
html5: true,
|
||||
volume: 0,
|
||||
});
|
||||
syncVideoWithTransitionAudio();
|
||||
};
|
||||
|
||||
const syncVideoWithTransitionAudio = () => {
|
||||
const video = document.querySelector('video')!;
|
||||
|
||||
const videoFader = new VolumeFader(video, {
|
||||
fadeScaling: configGetNumber('fadeScaling'),
|
||||
fadeDuration: configGetNumber('fadeInDuration'),
|
||||
});
|
||||
|
||||
transitionAudio.play();
|
||||
transitionAudio.seek(video.currentTime);
|
||||
|
||||
video.addEventListener('seeking', () => {
|
||||
transitionAudio.seek(video.currentTime);
|
||||
});
|
||||
|
||||
video.addEventListener('pause', () => {
|
||||
transitionAudio.pause();
|
||||
});
|
||||
|
||||
video.addEventListener('play', () => {
|
||||
transitionAudio.play();
|
||||
transitionAudio.seek(video.currentTime);
|
||||
|
||||
// Fade in
|
||||
const videoVolume = video.volume;
|
||||
video.volume = 0;
|
||||
videoFader.fadeTo(videoVolume);
|
||||
});
|
||||
|
||||
// Exit just before the end for the transition
|
||||
const transitionBeforeEnd = () => {
|
||||
if (
|
||||
video.currentTime >= video.duration - configGetNumber('secondsBeforeEnd')
|
||||
&& isReadyToCrossfade()
|
||||
) {
|
||||
video.removeEventListener('timeupdate', transitionBeforeEnd);
|
||||
|
||||
// Go to next video - XXX: does not support "repeat 1" mode
|
||||
document.querySelector<HTMLButtonElement>('.next-button')?.click();
|
||||
}
|
||||
};
|
||||
|
||||
video.addEventListener('timeupdate', transitionBeforeEnd);
|
||||
};
|
||||
|
||||
const onApiLoaded = () => {
|
||||
watchVideoIDChanges(async (videoID) => {
|
||||
await waitForTransition;
|
||||
const url = await getStreamURL(videoID);
|
||||
if (!url) {
|
||||
return;
|
||||
}
|
||||
|
||||
await createAudioForCrossfade(url);
|
||||
});
|
||||
};
|
||||
|
||||
const crossfade = (cb: () => void) => {
|
||||
if (!isReadyToCrossfade()) {
|
||||
cb();
|
||||
return;
|
||||
}
|
||||
|
||||
let resolveTransition: () => void;
|
||||
waitForTransition = new Promise<void>((resolve) => {
|
||||
resolveTransition = resolve;
|
||||
});
|
||||
|
||||
const video = document.querySelector('video')!;
|
||||
|
||||
const fader = new VolumeFader(transitionAudio._sounds[0]._node, {
|
||||
initialVolume: video.volume,
|
||||
fadeScaling: configGetNumber('fadeScaling'),
|
||||
fadeDuration: configGetNumber('fadeOutDuration'),
|
||||
});
|
||||
|
||||
// Fade out the music
|
||||
video.volume = 0;
|
||||
fader.fadeOut(() => {
|
||||
resolveTransition();
|
||||
cb();
|
||||
});
|
||||
};
|
||||
|
||||
export default async () => {
|
||||
config = await configProvider.getAll();
|
||||
|
||||
configProvider.subscribeAll((newConfig) => {
|
||||
config = newConfig;
|
||||
});
|
||||
|
||||
document.addEventListener('apiLoaded', onApiLoaded, {
|
||||
once: true,
|
||||
passive: true,
|
||||
});
|
||||
};
|
||||
@ -1,86 +0,0 @@
|
||||
import prompt from 'custom-electron-prompt';
|
||||
|
||||
import { BrowserWindow } from 'electron';
|
||||
|
||||
import config from './config';
|
||||
|
||||
import promptOptions from '../../providers/prompt-options';
|
||||
import configOptions from '../../config/defaults';
|
||||
|
||||
import { MenuTemplate } from '../../menu';
|
||||
|
||||
import type { ConfigType } from '../../config/dynamic';
|
||||
|
||||
const defaultOptions = configOptions.plugins.crossfade;
|
||||
|
||||
export default (win: BrowserWindow): MenuTemplate => [
|
||||
{
|
||||
label: 'Advanced',
|
||||
async click() {
|
||||
const newOptions = await promptCrossfadeValues(win, config.getAll());
|
||||
if (newOptions) {
|
||||
config.setAll(newOptions);
|
||||
}
|
||||
},
|
||||
},
|
||||
];
|
||||
|
||||
async function promptCrossfadeValues(win: BrowserWindow, options: ConfigType<'crossfade'>): Promise<Partial<ConfigType<'crossfade'>> | undefined> {
|
||||
const res = await prompt(
|
||||
{
|
||||
title: 'Crossfade Options',
|
||||
type: 'multiInput',
|
||||
multiInputOptions: [
|
||||
{
|
||||
label: 'Fade in duration (ms)',
|
||||
value: options.fadeInDuration || defaultOptions.fadeInDuration,
|
||||
inputAttrs: {
|
||||
type: 'number',
|
||||
required: true,
|
||||
min: '0',
|
||||
step: '100',
|
||||
},
|
||||
},
|
||||
{
|
||||
label: 'Fade out duration (ms)',
|
||||
value: options.fadeOutDuration || defaultOptions.fadeOutDuration,
|
||||
inputAttrs: {
|
||||
type: 'number',
|
||||
required: true,
|
||||
min: '0',
|
||||
step: '100',
|
||||
},
|
||||
},
|
||||
{
|
||||
label: 'Crossfade x seconds before end',
|
||||
value:
|
||||
options.secondsBeforeEnd || defaultOptions.secondsBeforeEnd,
|
||||
inputAttrs: {
|
||||
type: 'number',
|
||||
required: true,
|
||||
min: '0',
|
||||
},
|
||||
},
|
||||
{
|
||||
label: 'Fade scaling',
|
||||
selectOptions: { linear: 'Linear', logarithmic: 'Logarithmic' },
|
||||
value: options.fadeScaling || defaultOptions.fadeScaling,
|
||||
},
|
||||
],
|
||||
resizable: true,
|
||||
height: 360,
|
||||
...promptOptions(),
|
||||
},
|
||||
win,
|
||||
).catch(console.error);
|
||||
if (!res) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
return {
|
||||
fadeInDuration: Number(res[0]),
|
||||
fadeOutDuration: Number(res[1]),
|
||||
secondsBeforeEnd: Number(res[2]),
|
||||
fadeScaling: res[3],
|
||||
};
|
||||
}
|
||||
@ -1,23 +0,0 @@
|
||||
import type { ConfigType } from '../../config/dynamic';
|
||||
|
||||
export default (options: ConfigType<'disable-autoplay'>) => {
|
||||
const timeUpdateListener = (e: Event) => {
|
||||
if (e.target instanceof HTMLVideoElement) {
|
||||
e.target.pause();
|
||||
}
|
||||
};
|
||||
|
||||
document.addEventListener('apiLoaded', (apiEvent) => {
|
||||
const eventListener = (name: string) => {
|
||||
if (options.applyOnce) {
|
||||
apiEvent.detail.removeEventListener('videodatachange', eventListener);
|
||||
}
|
||||
|
||||
if (name === 'dataloaded') {
|
||||
apiEvent.detail.pauseVideo();
|
||||
document.querySelector<HTMLVideoElement>('video')?.addEventListener('timeupdate', timeUpdateListener, { once: true });
|
||||
}
|
||||
};
|
||||
apiEvent.detail.addEventListener('videodatachange', eventListener);
|
||||
}, { once: true, passive: true });
|
||||
};
|
||||
@ -1,20 +0,0 @@
|
||||
import { BrowserWindow } from 'electron';
|
||||
|
||||
import { setMenuOptions } from '../../config/plugins';
|
||||
|
||||
import { MenuTemplate } from '../../menu';
|
||||
|
||||
import type { ConfigType } from '../../config/dynamic';
|
||||
|
||||
export default (_: BrowserWindow, options: ConfigType<'disable-autoplay'>): MenuTemplate => [
|
||||
{
|
||||
label: 'Applies only on startup',
|
||||
type: 'checkbox',
|
||||
checked: options.applyOnce,
|
||||
click() {
|
||||
setMenuOptions('disable-autoplay', {
|
||||
applyOnce: !options.applyOnce,
|
||||
});
|
||||
}
|
||||
}
|
||||
];
|
||||
@ -1,220 +0,0 @@
|
||||
import { app, dialog, ipcMain } from 'electron';
|
||||
import { Client as DiscordClient } from '@xhayper/discord-rpc';
|
||||
import { dev } from 'electron-is';
|
||||
|
||||
import { SetActivity } from '@xhayper/discord-rpc/dist/structures/ClientUser';
|
||||
|
||||
import registerCallback, { type SongInfoCallback, type SongInfo } from '../../providers/song-info';
|
||||
|
||||
import type { ConfigType } from '../../config/dynamic';
|
||||
|
||||
// Application ID registered by @Zo-Bro-23
|
||||
const clientId = '1043858434585526382';
|
||||
|
||||
export interface Info {
|
||||
rpc: DiscordClient;
|
||||
ready: boolean;
|
||||
autoReconnect: boolean;
|
||||
lastSongInfo?: SongInfo;
|
||||
}
|
||||
|
||||
const info: Info = {
|
||||
rpc: new DiscordClient({
|
||||
clientId,
|
||||
}),
|
||||
ready: false,
|
||||
autoReconnect: true,
|
||||
lastSongInfo: undefined,
|
||||
};
|
||||
|
||||
/**
|
||||
* @type {(() => void)[]}
|
||||
*/
|
||||
const refreshCallbacks: (() => void)[] = [];
|
||||
|
||||
const resetInfo = () => {
|
||||
info.ready = false;
|
||||
clearTimeout(clearActivity);
|
||||
if (dev()) {
|
||||
console.log('discord disconnected');
|
||||
}
|
||||
|
||||
for (const cb of refreshCallbacks) {
|
||||
cb();
|
||||
}
|
||||
};
|
||||
|
||||
const connectTimeout = () => new Promise((resolve, reject) => setTimeout(() => {
|
||||
if (!info.autoReconnect || info.rpc.isConnected) {
|
||||
return;
|
||||
}
|
||||
|
||||
info.rpc.login().then(resolve).catch(reject);
|
||||
}, 5000));
|
||||
|
||||
const connectRecursive = () => {
|
||||
if (!info.autoReconnect || info.rpc.isConnected) {
|
||||
return;
|
||||
}
|
||||
|
||||
connectTimeout().catch(connectRecursive);
|
||||
};
|
||||
|
||||
let window: Electron.BrowserWindow;
|
||||
export const connect = (showError = false) => {
|
||||
if (info.rpc.isConnected) {
|
||||
if (dev()) {
|
||||
console.log('Attempted to connect with active connection');
|
||||
}
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
info.ready = false;
|
||||
|
||||
// Startup the rpc client
|
||||
info.rpc.login().catch((error: Error) => {
|
||||
resetInfo();
|
||||
if (dev()) {
|
||||
console.error(error);
|
||||
}
|
||||
|
||||
if (info.autoReconnect) {
|
||||
connectRecursive();
|
||||
} else if (showError) {
|
||||
dialog.showMessageBox(window, {
|
||||
title: 'Connection failed',
|
||||
message: error.message || String(error),
|
||||
type: 'error',
|
||||
});
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
let clearActivity: NodeJS.Timeout | undefined;
|
||||
let updateActivity: SongInfoCallback;
|
||||
|
||||
type DiscordOptions = ConfigType<'discord'>;
|
||||
|
||||
export default (
|
||||
win: Electron.BrowserWindow,
|
||||
options: DiscordOptions,
|
||||
) => {
|
||||
info.rpc.on('connected', () => {
|
||||
if (dev()) {
|
||||
console.log('discord connected');
|
||||
}
|
||||
|
||||
for (const cb of refreshCallbacks) {
|
||||
cb();
|
||||
}
|
||||
});
|
||||
|
||||
info.rpc.on('ready', () => {
|
||||
info.ready = true;
|
||||
if (info.lastSongInfo) {
|
||||
updateActivity(info.lastSongInfo);
|
||||
}
|
||||
});
|
||||
|
||||
info.rpc.on('disconnected', () => {
|
||||
resetInfo();
|
||||
|
||||
if (info.autoReconnect) {
|
||||
connectTimeout();
|
||||
}
|
||||
});
|
||||
|
||||
info.autoReconnect = options.autoReconnect;
|
||||
|
||||
window = win;
|
||||
// We get multiple events
|
||||
// Next song: PAUSE(n), PAUSE(n+1), PLAY(n+1)
|
||||
// Skip time: PAUSE(N), PLAY(N)
|
||||
updateActivity = (songInfo) => {
|
||||
if (songInfo.title.length === 0 && songInfo.artist.length === 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
info.lastSongInfo = songInfo;
|
||||
|
||||
// Stop the clear activity timout
|
||||
clearTimeout(clearActivity);
|
||||
|
||||
// Stop early if discord connection is not ready
|
||||
// do this after clearTimeout to avoid unexpected clears
|
||||
if (!info.rpc || !info.ready) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Clear directly if timeout is 0
|
||||
if (songInfo.isPaused && options.activityTimoutEnabled && options.activityTimoutTime === 0) {
|
||||
info.rpc.user?.clearActivity().catch(console.error);
|
||||
return;
|
||||
}
|
||||
|
||||
// Song information changed, so lets update the rich presence
|
||||
// @see https://discord.com/developers/docs/topics/gateway#activity-object
|
||||
// not all options are transfered through https://github.com/discordjs/RPC/blob/6f83d8d812c87cb7ae22064acd132600407d7d05/src/client.js#L518-530
|
||||
const activityInfo: SetActivity = {
|
||||
details: songInfo.title,
|
||||
state: songInfo.artist,
|
||||
largeImageKey: songInfo.imageSrc ?? '',
|
||||
largeImageText: songInfo.album ?? '',
|
||||
buttons: [
|
||||
...(options.listenAlong ? [{ label: 'Listen Along', url: songInfo.url ?? '' }] : []),
|
||||
...(options.hideGitHubButton ? [] : [{ label: 'View App On GitHub', url: 'https://github.com/th-ch/youtube-music' }]),
|
||||
],
|
||||
};
|
||||
|
||||
if (songInfo.isPaused) {
|
||||
// Add a paused icon to show that the song is paused
|
||||
activityInfo.smallImageKey = 'paused';
|
||||
activityInfo.smallImageText = 'Paused';
|
||||
// Set start the timer so the activity gets cleared after a while if enabled
|
||||
if (options.activityTimoutEnabled) {
|
||||
clearActivity = setTimeout(() => info.rpc.user?.clearActivity().catch(console.error), options.activityTimoutTime ?? 10_000);
|
||||
}
|
||||
} else if (!options.hideDurationLeft) {
|
||||
// Add the start and end time of the song
|
||||
const songStartTime = Date.now() - ((songInfo.elapsedSeconds ?? 0) * 1000);
|
||||
activityInfo.startTimestamp = songStartTime;
|
||||
activityInfo.endTimestamp
|
||||
= songStartTime + (songInfo.songDuration * 1000);
|
||||
}
|
||||
|
||||
info.rpc.user?.setActivity(activityInfo).catch(console.error);
|
||||
};
|
||||
|
||||
// If the page is ready, register the callback
|
||||
win.once('ready-to-show', () => {
|
||||
let lastSongInfo: SongInfo;
|
||||
registerCallback((songInfo) => {
|
||||
lastSongInfo = songInfo;
|
||||
updateActivity(songInfo);
|
||||
});
|
||||
connect();
|
||||
let lastSent = Date.now();
|
||||
ipcMain.on('timeChanged', (_, t: number) => {
|
||||
const currentTime = Date.now();
|
||||
// if lastSent is more than 5 seconds ago, send the new time
|
||||
if (currentTime - lastSent > 5000) {
|
||||
lastSent = currentTime;
|
||||
lastSongInfo.elapsedSeconds = t;
|
||||
updateActivity(lastSongInfo);
|
||||
}
|
||||
});
|
||||
});
|
||||
app.on('window-all-closed', clear);
|
||||
};
|
||||
|
||||
export const clear = () => {
|
||||
if (info.rpc) {
|
||||
info.rpc.user?.clearActivity();
|
||||
}
|
||||
|
||||
clearTimeout(clearActivity);
|
||||
};
|
||||
|
||||
export const registerRefresh = (cb: () => void) => refreshCallbacks.push(cb);
|
||||
export const isConnected = () => info.rpc !== null;
|
||||
@ -1,98 +0,0 @@
|
||||
import prompt from 'custom-electron-prompt';
|
||||
|
||||
import { clear, connect, isConnected, registerRefresh } from './back';
|
||||
|
||||
import { setMenuOptions } from '../../config/plugins';
|
||||
import promptOptions from '../../providers/prompt-options';
|
||||
import { singleton } from '../../providers/decorators';
|
||||
import { MenuTemplate } from '../../menu';
|
||||
|
||||
import type { ConfigType } from '../../config/dynamic';
|
||||
|
||||
const registerRefreshOnce = singleton((refreshMenu: () => void) => {
|
||||
registerRefresh(refreshMenu);
|
||||
});
|
||||
|
||||
type DiscordOptions = ConfigType<'discord'>;
|
||||
|
||||
export default (win: Electron.BrowserWindow, options: DiscordOptions, refreshMenu: () => void): MenuTemplate => {
|
||||
registerRefreshOnce(refreshMenu);
|
||||
|
||||
return [
|
||||
{
|
||||
label: isConnected() ? 'Connected' : 'Reconnect',
|
||||
enabled: !isConnected(),
|
||||
click: () => connect(),
|
||||
},
|
||||
{
|
||||
label: 'Auto reconnect',
|
||||
type: 'checkbox',
|
||||
checked: options.autoReconnect,
|
||||
click(item: Electron.MenuItem) {
|
||||
options.autoReconnect = item.checked;
|
||||
setMenuOptions('discord', options);
|
||||
},
|
||||
},
|
||||
{
|
||||
label: 'Clear activity',
|
||||
click: clear,
|
||||
},
|
||||
{
|
||||
label: 'Clear activity after timeout',
|
||||
type: 'checkbox',
|
||||
checked: options.activityTimoutEnabled,
|
||||
click(item: Electron.MenuItem) {
|
||||
options.activityTimoutEnabled = item.checked;
|
||||
setMenuOptions('discord', options);
|
||||
},
|
||||
},
|
||||
{
|
||||
label: 'Listen Along',
|
||||
type: 'checkbox',
|
||||
checked: options.listenAlong,
|
||||
click(item: Electron.MenuItem) {
|
||||
options.listenAlong = item.checked;
|
||||
setMenuOptions('discord', options);
|
||||
},
|
||||
},
|
||||
{
|
||||
label: 'Hide GitHub link Button',
|
||||
type: 'checkbox',
|
||||
checked: options.hideGitHubButton,
|
||||
click(item: Electron.MenuItem) {
|
||||
options.hideGitHubButton = item.checked;
|
||||
setMenuOptions('discord', options);
|
||||
},
|
||||
},
|
||||
{
|
||||
label: 'Hide duration left',
|
||||
type: 'checkbox',
|
||||
checked: options.hideDurationLeft,
|
||||
click(item: Electron.MenuItem) {
|
||||
options.hideDurationLeft = item.checked;
|
||||
setMenuOptions('discord', options);
|
||||
},
|
||||
},
|
||||
{
|
||||
label: 'Set inactivity timeout',
|
||||
click: () => setInactivityTimeout(win, options),
|
||||
},
|
||||
];
|
||||
};
|
||||
|
||||
async function setInactivityTimeout(win: Electron.BrowserWindow, options: DiscordOptions) {
|
||||
const output = await prompt({
|
||||
title: 'Set Inactivity Timeout',
|
||||
label: 'Enter inactivity timeout in seconds:',
|
||||
value: String(Math.round((options.activityTimoutTime ?? 0) / 1e3)),
|
||||
type: 'counter',
|
||||
counterOptions: { minimum: 0, multiFire: true },
|
||||
width: 450,
|
||||
...promptOptions(),
|
||||
}, win);
|
||||
|
||||
if (output) {
|
||||
options.activityTimoutTime = Math.round(~~output * 1e3);
|
||||
setMenuOptions('discord', options);
|
||||
}
|
||||
}
|
||||
@ -1,4 +0,0 @@
|
||||
import { PluginConfig } from '../../config/dynamic';
|
||||
|
||||
const config = new PluginConfig('downloader');
|
||||
export default config;
|
||||
@ -1,83 +0,0 @@
|
||||
import { ipcRenderer } from 'electron';
|
||||
|
||||
import downloadHTML from './templates/download.html';
|
||||
|
||||
import defaultConfig from '../../config/defaults';
|
||||
import { getSongMenu } from '../../providers/dom-elements';
|
||||
import { ElementFromHtml } from '../utils';
|
||||
import { getSongInfo } from '../../providers/song-info-front';
|
||||
|
||||
let menu: Element | null = null;
|
||||
let progress: Element | null = null;
|
||||
const downloadButton = ElementFromHtml(downloadHTML);
|
||||
|
||||
let doneFirstLoad = false;
|
||||
|
||||
const menuObserver = new MutationObserver(() => {
|
||||
if (!menu) {
|
||||
menu = getSongMenu();
|
||||
if (!menu) {
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
if (menu.contains(downloadButton)) {
|
||||
return;
|
||||
}
|
||||
|
||||
const menuUrl = document.querySelector<HTMLAnchorElement>('tp-yt-paper-listbox [tabindex="0"] #navigation-endpoint')?.href;
|
||||
if (!menuUrl?.includes('watch?') && doneFirstLoad) {
|
||||
return;
|
||||
}
|
||||
|
||||
menu.prepend(downloadButton);
|
||||
progress = document.querySelector('#ytmcustom-download');
|
||||
|
||||
if (doneFirstLoad) {
|
||||
return;
|
||||
}
|
||||
|
||||
setTimeout(() => doneFirstLoad ||= true, 500);
|
||||
});
|
||||
|
||||
// TODO: re-enable once contextIsolation is set to true
|
||||
// contextBridge.exposeInMainWorld("downloader", {
|
||||
// download: () => {
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any,@typescript-eslint/no-unsafe-member-access
|
||||
(global as any).download = () => {
|
||||
let videoUrl = getSongMenu()
|
||||
// Selector of first button which is always "Start Radio"
|
||||
?.querySelector('ytmusic-menu-navigation-item-renderer[tabindex="-1"] #navigation-endpoint')
|
||||
?.getAttribute('href');
|
||||
if (videoUrl) {
|
||||
if (videoUrl.startsWith('watch?')) {
|
||||
videoUrl = defaultConfig.url + '/' + videoUrl;
|
||||
}
|
||||
|
||||
if (videoUrl.includes('?playlist=')) {
|
||||
ipcRenderer.send('download-playlist-request', videoUrl);
|
||||
return;
|
||||
}
|
||||
} else {
|
||||
videoUrl = getSongInfo().url || window.location.href;
|
||||
}
|
||||
|
||||
ipcRenderer.send('download-song', videoUrl);
|
||||
};
|
||||
|
||||
export default () => {
|
||||
document.addEventListener('apiLoaded', () => {
|
||||
menuObserver.observe(document.querySelector('ytmusic-popup-container')!, {
|
||||
childList: true,
|
||||
subtree: true,
|
||||
});
|
||||
}, { once: true, passive: true });
|
||||
|
||||
ipcRenderer.on('downloader-feedback', (_, feedback: string) => {
|
||||
if (progress) {
|
||||
progress.innerHTML = feedback || 'Download';
|
||||
} else {
|
||||
console.warn('Cannot update progress');
|
||||
}
|
||||
});
|
||||
};
|
||||
@ -1,46 +0,0 @@
|
||||
import { dialog } from 'electron';
|
||||
|
||||
import { downloadPlaylist } from './back';
|
||||
import { defaultMenuDownloadLabel, getFolder } from './utils';
|
||||
import { DefaultPresetList } from './types';
|
||||
import config from './config';
|
||||
|
||||
import { MenuTemplate } from '../../menu';
|
||||
|
||||
export default (): MenuTemplate => [
|
||||
{
|
||||
label: defaultMenuDownloadLabel,
|
||||
click: () => downloadPlaylist(),
|
||||
},
|
||||
{
|
||||
label: 'Choose download folder',
|
||||
click() {
|
||||
const result = dialog.showOpenDialogSync({
|
||||
properties: ['openDirectory', 'createDirectory'],
|
||||
defaultPath: getFolder(config.get('downloadFolder') ?? ''),
|
||||
});
|
||||
if (result) {
|
||||
config.set('downloadFolder', result[0]);
|
||||
} // Else = user pressed cancel
|
||||
},
|
||||
},
|
||||
{
|
||||
label: 'Presets',
|
||||
submenu: Object.keys(DefaultPresetList).map((preset) => ({
|
||||
label: preset,
|
||||
type: 'radio',
|
||||
checked: config.get('selectedPreset') === preset,
|
||||
click() {
|
||||
config.set('selectedPreset', preset);
|
||||
},
|
||||
})),
|
||||
},
|
||||
{
|
||||
label: 'Skip existing files',
|
||||
type: 'checkbox',
|
||||
checked: config.get('skipExisting'),
|
||||
click(item) {
|
||||
config.set('skipExisting', item.checked);
|
||||
},
|
||||
},
|
||||
];
|
||||
@ -1,116 +0,0 @@
|
||||
export interface Preset {
|
||||
extension?: string | null;
|
||||
ffmpegArgs: string[];
|
||||
}
|
||||
|
||||
// Presets for FFmpeg
|
||||
export const DefaultPresetList: Record<string, Preset> = {
|
||||
'mp3 (256kbps)': {
|
||||
extension: 'mp3',
|
||||
ffmpegArgs: ['-b:a', '256k'],
|
||||
},
|
||||
'Source': {
|
||||
extension: undefined,
|
||||
ffmpegArgs: ['-acodec', 'copy'],
|
||||
},
|
||||
'Custom': {
|
||||
extension: null,
|
||||
ffmpegArgs: [],
|
||||
}
|
||||
};
|
||||
|
||||
export interface YouTubeFormat {
|
||||
itag: number;
|
||||
container: string;
|
||||
content: string;
|
||||
resolution: string;
|
||||
bitrate: string;
|
||||
range: string;
|
||||
vrOr3D: string;
|
||||
}
|
||||
|
||||
// converted from https://gist.github.com/sidneys/7095afe4da4ae58694d128b1034e01e2#file-youtube_format_code_itag_list-md
|
||||
export const YoutubeFormatList: YouTubeFormat[] = [
|
||||
{ itag: 5, container: 'flv', content: 'audio/video', resolution: '240p', bitrate: '-', range: '-', vrOr3D: '-' },
|
||||
{ itag: 6, container: 'flv', content: 'audio/video', resolution: '270p', bitrate: '-', range: '-', vrOr3D: '-' },
|
||||
{ itag: 17, container: '3gp', content: 'audio/video', resolution: '144p', bitrate: '-', range: '-', vrOr3D: '-' },
|
||||
{ itag: 18, container: 'mp4', content: 'audio/video', resolution: '360p', bitrate: '-', range: '-', vrOr3D: '-' },
|
||||
{ itag: 22, container: 'mp4', content: 'audio/video', resolution: '720p', bitrate: '-', range: '-', vrOr3D: '-' },
|
||||
{ itag: 34, container: 'flv', content: 'audio/video', resolution: '360p', bitrate: '-', range: '-', vrOr3D: '-' },
|
||||
{ itag: 35, container: 'flv', content: 'audio/video', resolution: '480p', bitrate: '-', range: '-', vrOr3D: '-' },
|
||||
{ itag: 36, container: '3gp', content: 'audio/video', resolution: '180p', bitrate: '-', range: '-', vrOr3D: '-' },
|
||||
{ itag: 37, container: 'mp4', content: 'audio/video', resolution: '1080p', bitrate: '-', range: '-', vrOr3D: '-' },
|
||||
{ itag: 38, container: 'mp4', content: 'audio/video', resolution: '3072p', bitrate: '-', range: '-', vrOr3D: '-' },
|
||||
{ itag: 43, container: 'webm', content: 'audio/video', resolution: '360p', bitrate: '-', range: '-', vrOr3D: '-' },
|
||||
{ itag: 44, container: 'webm', content: 'audio/video', resolution: '480p', bitrate: '-', range: '-', vrOr3D: '-' },
|
||||
{ itag: 45, container: 'webm', content: 'audio/video', resolution: '720p', bitrate: '-', range: '-', vrOr3D: '-' },
|
||||
{ itag: 46, container: 'webm', content: 'audio/video', resolution: '1080p', bitrate: '-', range: '-', vrOr3D: '-' },
|
||||
{ itag: 82, container: 'mp4', content: 'audio/video', resolution: '360p', bitrate: '-', range: '-', vrOr3D: '3D' },
|
||||
{ itag: 83, container: 'mp4', content: 'audio/video', resolution: '480p', bitrate: '-', range: '-', vrOr3D: '3D' },
|
||||
{ itag: 84, container: 'mp4', content: 'audio/video', resolution: '720p', bitrate: '-', range: '-', vrOr3D: '3D' },
|
||||
{ itag: 85, container: 'mp4', content: 'audio/video', resolution: '1080p', bitrate: '-', range: '-', vrOr3D: '3D' },
|
||||
{ itag: 91, container: 'hls', content: 'audio/video', resolution: '144p', bitrate: '-', range: '-', vrOr3D: '3D' },
|
||||
{ itag: 92, container: 'hls', content: 'audio/video', resolution: '240p', bitrate: '-', range: '-', vrOr3D: '3D' },
|
||||
{ itag: 93, container: 'hls', content: 'audio/video', resolution: '360p', bitrate: '-', range: '-', vrOr3D: '3D' },
|
||||
{ itag: 94, container: 'hls', content: 'audio/video', resolution: '480p', bitrate: '-', range: '-', vrOr3D: '3D' },
|
||||
{ itag: 95, container: 'hls', content: 'audio/video', resolution: '720p', bitrate: '-', range: '-', vrOr3D: '3D' },
|
||||
{ itag: 96, container: 'hls', content: 'audio/video', resolution: '1080p', bitrate: '-', range: '-', vrOr3D: '-' },
|
||||
{ itag: 100, container: 'webm', content: 'audio/video', resolution: '360p', bitrate: '-', range: '-', vrOr3D: '3D' },
|
||||
{ itag: 101, container: 'webm', content: 'audio/video', resolution: '480p', bitrate: '-', range: '-', vrOr3D: '3D' },
|
||||
{ itag: 102, container: 'webm', content: 'audio/video', resolution: '720p', bitrate: '-', range: '-', vrOr3D: '3D' },
|
||||
{ itag: 132, container: 'hls', content: 'audio/video', resolution: '240p', bitrate: '-', range: '-', vrOr3D: '' },
|
||||
{ itag: 133, container: 'mp4', content: 'video', resolution: '240p', bitrate: '-', range: '-', vrOr3D: '' },
|
||||
{ itag: 134, container: 'mp4', content: 'video', resolution: '360p', bitrate: '-', range: '-', vrOr3D: '' },
|
||||
{ itag: 135, container: 'mp4', content: 'video', resolution: '480p', bitrate: '-', range: '-', vrOr3D: '' },
|
||||
{ itag: 136, container: 'mp4', content: 'video', resolution: '720p', bitrate: '-', range: '-', vrOr3D: '' },
|
||||
{ itag: 137, container: 'mp4', content: 'video', resolution: '1080p', bitrate: '-', range: '-', vrOr3D: '' },
|
||||
{ itag: 138, container: 'mp4', content: 'video', resolution: '2160p60', bitrate: '-', range: '-', vrOr3D: '' },
|
||||
{ itag: 139, container: 'm4a', content: 'audio', resolution: '-', bitrate: '48k', range: '-', vrOr3D: '' },
|
||||
{ itag: 140, container: 'm4a', content: 'audio', resolution: '-', bitrate: '128k', range: '-', vrOr3D: '' },
|
||||
{ itag: 141, container: 'm4a', content: 'audio', resolution: '-', bitrate: '256k', range: '-', vrOr3D: '' },
|
||||
{ itag: 151, container: 'hls', content: 'audio/video', resolution: '72p', bitrate: '-', range: '-', vrOr3D: '' },
|
||||
{ itag: 160, container: 'mp4', content: 'video', resolution: '144p', bitrate: '-', range: '-', vrOr3D: '' },
|
||||
{ itag: 167, container: 'webm', content: 'video', resolution: '360p', bitrate: '-', range: '-', vrOr3D: '' },
|
||||
{ itag: 168, container: 'webm', content: 'video', resolution: '480p', bitrate: '-', range: '-', vrOr3D: '' },
|
||||
{ itag: 169, container: 'webm', content: 'video', resolution: '1080p', bitrate: '-', range: '-', vrOr3D: '' },
|
||||
{ itag: 171, container: 'webm', content: 'audio', resolution: '-', bitrate: '128k', range: '-', vrOr3D: '' },
|
||||
{ itag: 218, container: 'webm', content: 'video', resolution: '480p', bitrate: '-', range: '-', vrOr3D: '' },
|
||||
{ itag: 219, container: 'webm', content: 'video', resolution: '144p', bitrate: '-', range: '-', vrOr3D: '' },
|
||||
{ itag: 242, container: 'webm', content: 'video', resolution: '240p', bitrate: '-', range: '-', vrOr3D: '' },
|
||||
{ itag: 243, container: 'webm', content: 'video', resolution: '360p', bitrate: '-', range: '-', vrOr3D: '' },
|
||||
{ itag: 244, container: 'webm', content: 'video', resolution: '480p', bitrate: '-', range: '-', vrOr3D: '' },
|
||||
{ itag: 245, container: 'webm', content: 'video', resolution: '480p', bitrate: '-', range: '-', vrOr3D: '' },
|
||||
{ itag: 246, container: 'webm', content: 'video', resolution: '480p', bitrate: '-', range: '-', vrOr3D: '' },
|
||||
{ itag: 247, container: 'webm', content: 'video', resolution: '720p', bitrate: '-', range: '-', vrOr3D: '' },
|
||||
{ itag: 248, container: 'webm', content: 'video', resolution: '1080p', bitrate: '-', range: '-', vrOr3D: '' },
|
||||
{ itag: 249, container: 'webm', content: 'audio', resolution: '-', bitrate: '50k', range: '-', vrOr3D: '' },
|
||||
{ itag: 250, container: 'webm', content: 'audio', resolution: '-', bitrate: '70k', range: '-', vrOr3D: '' },
|
||||
{ itag: 251, container: 'webm', content: 'audio', resolution: '-', bitrate: '160k', range: '-', vrOr3D: '' },
|
||||
{ itag: 264, container: 'mp4', content: 'video', resolution: '1440p', bitrate: '-', range: '-', vrOr3D: '' },
|
||||
{ itag: 266, container: 'mp4', content: 'video', resolution: '2160p60', bitrate: '-', range: '-', vrOr3D: '' },
|
||||
{ itag: 271, container: 'webm', content: 'video', resolution: '1440p', bitrate: '-', range: '-', vrOr3D: '' },
|
||||
{ itag: 272, container: 'webm', content: 'video', resolution: '4320p', bitrate: '-', range: '-', vrOr3D: '' },
|
||||
{ itag: 278, container: 'webm', content: 'video', resolution: '144p', bitrate: '-', range: '-', vrOr3D: '' },
|
||||
{ itag: 298, container: 'mp4', content: 'video', resolution: '720p60', bitrate: '-', range: '-', vrOr3D: '' },
|
||||
{ itag: 299, container: 'mp4', content: 'video', resolution: '1080p60', bitrate: '-', range: '-', vrOr3D: '' },
|
||||
{ itag: 302, container: 'webm', content: 'video', resolution: '720p60', bitrate: '-', range: '-', vrOr3D: '' },
|
||||
{ itag: 303, container: 'webm', content: 'video', resolution: '1080p60', bitrate: '-', range: '-', vrOr3D: '' },
|
||||
{ itag: 308, container: 'webm', content: 'video', resolution: '1440p60', bitrate: '-', range: '-', vrOr3D: '' },
|
||||
{ itag: 313, container: 'webm', content: 'video', resolution: '2160p', bitrate: '-', range: '-', vrOr3D: '' },
|
||||
{ itag: 315, container: 'webm', content: 'video', resolution: '2160p60', bitrate: '-', range: '-', vrOr3D: '' },
|
||||
{ itag: 330, container: 'webm', content: 'video', resolution: '144p60', bitrate: '-', range: 'hdr', vrOr3D: '' },
|
||||
{ itag: 331, container: 'webm', content: 'video', resolution: '240p60', bitrate: '-', range: 'hdr', vrOr3D: '' },
|
||||
{ itag: 332, container: 'webm', content: 'video', resolution: '360p60', bitrate: '-', range: 'hdr', vrOr3D: '' },
|
||||
{ itag: 333, container: 'webm', content: 'video', resolution: '480p60', bitrate: '-', range: 'hdr', vrOr3D: '' },
|
||||
{ itag: 334, container: 'webm', content: 'video', resolution: '720p60', bitrate: '-', range: 'hdr', vrOr3D: '' },
|
||||
{ itag: 335, container: 'webm', content: 'video', resolution: '1080p60', bitrate: '-', range: 'hdr', vrOr3D: '' },
|
||||
{ itag: 336, container: 'webm', content: 'video', resolution: '1440p60', bitrate: '-', range: 'hdr', vrOr3D: '' },
|
||||
{ itag: 337, container: 'webm', content: 'video', resolution: '2160p60', bitrate: '-', range: 'hdr', vrOr3D: '' },
|
||||
{ itag: 272, container: 'webm', content: 'video', resolution: '2880p/4320p', bitrate: '-', range: '-', vrOr3D: '' },
|
||||
{ itag: 399, container: 'mp4', content: 'video', resolution: '1080p', bitrate: '-', range: '-', vrOr3D: '' },
|
||||
{ itag: 400, container: 'mp4', content: 'video', resolution: '1440p', bitrate: '-', range: '-', vrOr3D: '' },
|
||||
{ itag: 401, container: 'mp4', content: 'video', resolution: '2160p', bitrate: '-', range: '-', vrOr3D: '' },
|
||||
{ itag: 402, container: 'mp4', content: 'video', resolution: '2880p', bitrate: '-', range: '-', vrOr3D: '' },
|
||||
{ itag: 571, container: 'mp4', content: 'video', resolution: '3840p', bitrate: '-', range: '-', vrOr3D: '' },
|
||||
{ itag: 702, container: 'mp4', content: 'video', resolution: '3840p', bitrate: '-', range: '-', vrOr3D: '' },
|
||||
];
|
||||
@ -1,45 +0,0 @@
|
||||
// "YouTube Music fix volume ratio 0.4" by Marco Pfeiffer
|
||||
// https://greasyfork.org/en/scripts/397686-youtube-music-fix-volume-ratio/
|
||||
|
||||
const exponentialVolume = () => {
|
||||
// Manipulation exponent, higher value = lower volume
|
||||
// 3 is the value used by pulseaudio, which Barteks2x figured out this gist here: https://gist.github.com/Barteks2x/a4e189a36a10c159bb1644ffca21c02a
|
||||
// 0.05 (or 5%) is the lowest you can select in the UI which with an exponent of 3 becomes 0.000125 or 0.0125%
|
||||
const EXPONENT = 3;
|
||||
|
||||
const storedOriginalVolumes = new WeakMap<HTMLMediaElement, number>();
|
||||
const propertyDescriptor = Object.getOwnPropertyDescriptor(
|
||||
HTMLMediaElement.prototype,
|
||||
'volume',
|
||||
);
|
||||
Object.defineProperty(HTMLMediaElement.prototype, 'volume', {
|
||||
get(this: HTMLMediaElement) {
|
||||
const lowVolume = propertyDescriptor?.get?.call(this) as number ?? 0;
|
||||
const calculatedOriginalVolume = lowVolume ** (1 / EXPONENT);
|
||||
|
||||
// The calculated value has some accuracy issues which can lead to problems for implementations that expect exact values.
|
||||
// To avoid this, I'll store the unmodified volume to return it when read here.
|
||||
// This mostly solves the issue, but the initial read has no stored value and the volume can also change though external influences.
|
||||
// To avoid ill effects, I check if the stored volume is somewhere in the same range as the calculated volume.
|
||||
const storedOriginalVolume = storedOriginalVolumes.get(this) ?? 0;
|
||||
const storedDeviation = Math.abs(
|
||||
storedOriginalVolume - calculatedOriginalVolume,
|
||||
);
|
||||
|
||||
return storedDeviation < 0.01
|
||||
? storedOriginalVolume
|
||||
: calculatedOriginalVolume;
|
||||
},
|
||||
set(this: HTMLMediaElement, originalVolume: number) {
|
||||
const lowVolume = originalVolume ** EXPONENT;
|
||||
storedOriginalVolumes.set(this, originalVolume);
|
||||
propertyDescriptor?.set?.call(this, lowVolume);
|
||||
},
|
||||
});
|
||||
};
|
||||
|
||||
export default () =>
|
||||
document.addEventListener('apiLoaded', exponentialVolume, {
|
||||
once: true,
|
||||
passive: true,
|
||||
});
|
||||
@ -1,3 +0,0 @@
|
||||
<svg fill="none" viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg">
|
||||
<path fill="#ffffff" d="m4.21 4.387.083-.094a1 1 0 0 1 1.32-.083l.094.083L12 10.585l6.293-6.292a1 1 0 1 1 1.414 1.414L13.415 12l6.292 6.293a1 1 0 0 1 .083 1.32l-.083.094a1 1 0 0 1-1.32.083l-.094-.083L12 13.415l-6.293 6.292a1 1 0 0 1-1.414-1.414L10.585 12 4.293 5.707a1 1 0 0 1-.083-1.32l.083-.094-.083.094Z"/>
|
||||
</svg>
|
||||
|
Before Width: | Height: | Size: 392 B |
@ -1,3 +0,0 @@
|
||||
<svg fill="none" viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg">
|
||||
<path fill="#ffffff" d="M6 3h12a3 3 0 0 1 3 3v12a3 3 0 0 1-3 3H6a3 3 0 0 1-3-3V6a3 3 0 0 1 3-3Zm0 2a1 1 0 0 0-1 1v12a1 1 0 0 0 1 1h12a1 1 0 0 0 1-1V6a1 1 0 0 0-1-1H6Z"/>
|
||||
</svg>
|
||||
|
Before Width: | Height: | Size: 252 B |
@ -1,3 +0,0 @@
|
||||
<svg width="24" height="24" fill="none" viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M3 17h12a1 1 0 0 1 .117 1.993L15 19H3a1 1 0 0 1-.117-1.993L3 17h12H3Zm0-6h18a1 1 0 0 1 .117 1.993L21 13H3a1 1 0 0 1-.117-1.993L3 11h18H3Zm0-6h15a1 1 0 0 1 .117 1.993L18 7H3a1 1 0 0 1-.117-1.993L3 5h15H3Z" fill="#ffffff"/>
|
||||
</svg>
|
||||
|
Before Width: | Height: | Size: 338 B |
@ -1,3 +0,0 @@
|
||||
<svg fill="none" viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg">
|
||||
<path fill="#ffffff" d="M3.755 12.5h16.492a.75.75 0 0 0 0-1.5H3.755a.75.75 0 0 0 0 1.5Z" />
|
||||
</svg>
|
||||
|
Before Width: | Height: | Size: 174 B |
@ -1,3 +0,0 @@
|
||||
<svg fill="none" viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg">
|
||||
<path fill="#ffffff" d="M7.518 5H6.009a3.25 3.25 0 0 1 3.24-3h8.001A4.75 4.75 0 0 1 22 6.75v8a3.25 3.25 0 0 1-3 3.24v-1.508a1.75 1.75 0 0 0 1.5-1.732v-8a3.25 3.25 0 0 0-3.25-3.25h-8A1.75 1.75 0 0 0 7.518 5ZM5.25 6A3.25 3.25 0 0 0 2 9.25v9.5A3.25 3.25 0 0 0 5.25 22h9.5A3.25 3.25 0 0 0 18 18.75v-9.5A3.25 3.25 0 0 0 14.75 6h-9.5ZM3.5 9.25c0-.966.784-1.75 1.75-1.75h9.5c.967 0 1.75.784 1.75 1.75v9.5a1.75 1.75 0 0 1-1.75 1.75h-9.5a1.75 1.75 0 0 1-1.75-1.75v-9.5Z"/>
|
||||
</svg>
|
||||
|
Before Width: | Height: | Size: 546 B |
@ -1,67 +0,0 @@
|
||||
import { register } from 'electron-localshortcut';
|
||||
|
||||
import { BrowserWindow, Menu, MenuItem, ipcMain } from 'electron';
|
||||
|
||||
import titlebarStyle from './titlebar.css';
|
||||
|
||||
import { injectCSS } from '../utils';
|
||||
|
||||
// Tracks menu visibility
|
||||
export default (win: BrowserWindow) => {
|
||||
injectCSS(win.webContents, titlebarStyle);
|
||||
|
||||
win.once('ready-to-show', () => {
|
||||
register(win, '`', () => {
|
||||
win.webContents.send('toggleMenu');
|
||||
});
|
||||
});
|
||||
|
||||
ipcMain.handle(
|
||||
'get-menu',
|
||||
() => JSON.parse(JSON.stringify(
|
||||
Menu.getApplicationMenu(),
|
||||
(key: string, value: unknown) => (key !== 'commandsMap' && key !== 'menu') ? value : undefined),
|
||||
),
|
||||
);
|
||||
|
||||
const getMenuItemById = (commandId: number): MenuItem | null => {
|
||||
const menu = Menu.getApplicationMenu();
|
||||
|
||||
let target: MenuItem | null = null;
|
||||
const stack = [...menu?.items ?? []];
|
||||
while (stack.length > 0) {
|
||||
const now = stack.shift();
|
||||
now?.submenu?.items.forEach((item) => stack.push(item));
|
||||
|
||||
if (now?.commandId === commandId) {
|
||||
target = now;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
return target;
|
||||
};
|
||||
|
||||
ipcMain.handle('menu-event', (event, commandId: number) => {
|
||||
const target = getMenuItemById(commandId);
|
||||
if (target) target.click(undefined, BrowserWindow.fromWebContents(event.sender), event.sender);
|
||||
});
|
||||
|
||||
ipcMain.handle('get-menu-by-id', (_, commandId: number) => {
|
||||
const result = getMenuItemById(commandId);
|
||||
|
||||
return JSON.parse(JSON.stringify(
|
||||
result,
|
||||
(key: string, value: unknown) => (key !== 'commandsMap' && key !== 'menu') ? value : undefined),
|
||||
);
|
||||
});
|
||||
|
||||
ipcMain.handle('window-is-maximized', () => win.isMaximized());
|
||||
|
||||
ipcMain.handle('window-close', () => win.close());
|
||||
ipcMain.handle('window-minimize', () => win.minimize());
|
||||
ipcMain.handle('window-maximize', () => win.maximize());
|
||||
win.on('maximize', () => win.webContents.send('window-maximize'));
|
||||
ipcMain.handle('window-unmaximize', () => win.unmaximize());
|
||||
win.on('unmaximize', () => win.webContents.send('window-unmaximize'));
|
||||
};
|
||||
@ -1,163 +0,0 @@
|
||||
import { ipcRenderer, Menu } from 'electron';
|
||||
|
||||
import { createPanel } from './menu/panel';
|
||||
|
||||
import logo from './assets/menu.svg';
|
||||
import close from './assets/close.svg';
|
||||
import minimize from './assets/minimize.svg';
|
||||
import maximize from './assets/maximize.svg';
|
||||
import unmaximize from './assets/unmaximize.svg';
|
||||
|
||||
import { isEnabled } from '../../config/plugins';
|
||||
import config from '../../config';
|
||||
|
||||
function $<E extends Element = Element>(selector: string) {
|
||||
return document.querySelector<E>(selector);
|
||||
}
|
||||
|
||||
const isMacOS = navigator.userAgent.includes('Macintosh');
|
||||
const isNotWindowsOrMacOS = !navigator.userAgent.includes('Windows') && !isMacOS;
|
||||
|
||||
export default async () => {
|
||||
let hideMenu = config.get('options.hideMenu');
|
||||
const titleBar = document.createElement('title-bar');
|
||||
const navBar = document.querySelector<HTMLDivElement>('#nav-bar-background');
|
||||
let maximizeButton: HTMLButtonElement;
|
||||
if (isMacOS) titleBar.style.setProperty('--offset-left', '70px');
|
||||
|
||||
logo.classList.add('title-bar-icon');
|
||||
const logoClick = () => {
|
||||
hideMenu = !hideMenu;
|
||||
let visibilityStyle: string;
|
||||
if (hideMenu) {
|
||||
visibilityStyle = 'hidden';
|
||||
} else {
|
||||
visibilityStyle = 'visible';
|
||||
}
|
||||
const menus = document.querySelectorAll<HTMLElement>('menu-button');
|
||||
menus.forEach((menu) => {
|
||||
menu.style.visibility = visibilityStyle;
|
||||
});
|
||||
};
|
||||
logo.onclick = logoClick;
|
||||
|
||||
ipcRenderer.on('toggleMenu', logoClick);
|
||||
|
||||
if (!isMacOS) titleBar.appendChild(logo);
|
||||
document.body.appendChild(titleBar);
|
||||
|
||||
titleBar.appendChild(logo);
|
||||
|
||||
const addWindowControls = async () => {
|
||||
|
||||
// Create window control buttons
|
||||
const minimizeButton = document.createElement('button');
|
||||
minimizeButton.classList.add('window-control');
|
||||
minimizeButton.appendChild(minimize);
|
||||
minimizeButton.onclick = () => ipcRenderer.invoke('window-minimize');
|
||||
|
||||
maximizeButton = document.createElement('button');
|
||||
if (await ipcRenderer.invoke('window-is-maximized')) {
|
||||
maximizeButton.classList.add('window-control');
|
||||
maximizeButton.appendChild(unmaximize);
|
||||
} else {
|
||||
maximizeButton.classList.add('window-control');
|
||||
maximizeButton.appendChild(maximize);
|
||||
}
|
||||
maximizeButton.onclick = async () => {
|
||||
if (await ipcRenderer.invoke('window-is-maximized')) {
|
||||
// change icon to maximize
|
||||
maximizeButton.removeChild(maximizeButton.firstChild!);
|
||||
maximizeButton.appendChild(maximize);
|
||||
|
||||
// call unmaximize
|
||||
await ipcRenderer.invoke('window-unmaximize');
|
||||
} else {
|
||||
// change icon to unmaximize
|
||||
maximizeButton.removeChild(maximizeButton.firstChild!);
|
||||
maximizeButton.appendChild(unmaximize);
|
||||
|
||||
// call maximize
|
||||
await ipcRenderer.invoke('window-maximize');
|
||||
}
|
||||
};
|
||||
|
||||
const closeButton = document.createElement('button');
|
||||
closeButton.classList.add('window-control');
|
||||
closeButton.appendChild(close);
|
||||
closeButton.onclick = () => ipcRenderer.invoke('window-close');
|
||||
|
||||
// Create a container div for the window control buttons
|
||||
const windowControlsContainer = document.createElement('div');
|
||||
windowControlsContainer.classList.add('window-controls-container');
|
||||
windowControlsContainer.appendChild(minimizeButton);
|
||||
windowControlsContainer.appendChild(maximizeButton);
|
||||
windowControlsContainer.appendChild(closeButton);
|
||||
|
||||
// Add window control buttons to the title bar
|
||||
titleBar.appendChild(windowControlsContainer);
|
||||
};
|
||||
|
||||
if (isNotWindowsOrMacOS) await addWindowControls();
|
||||
|
||||
if (navBar) {
|
||||
const observer = new MutationObserver((mutations) => {
|
||||
mutations.forEach(() => {
|
||||
titleBar.style.setProperty('--titlebar-background-color', navBar.style.backgroundColor);
|
||||
document.querySelector('html')!.style.setProperty('--titlebar-background-color', navBar.style.backgroundColor);
|
||||
});
|
||||
});
|
||||
|
||||
observer.observe(navBar, { attributes : true, attributeFilter : ['style'] });
|
||||
}
|
||||
|
||||
const updateMenu = async () => {
|
||||
const children = [...titleBar.children];
|
||||
children.forEach((child) => {
|
||||
if (child !== logo) child.remove();
|
||||
});
|
||||
|
||||
const menu = await ipcRenderer.invoke('get-menu') as Menu | null;
|
||||
if (!menu) return;
|
||||
|
||||
menu.items.forEach((menuItem) => {
|
||||
const menu = document.createElement('menu-button');
|
||||
createPanel(titleBar, menu, menuItem.submenu?.items ?? []);
|
||||
|
||||
menu.append(menuItem.label);
|
||||
titleBar.appendChild(menu);
|
||||
if (hideMenu) {
|
||||
menu.style.visibility = 'hidden';
|
||||
}
|
||||
});
|
||||
if (isNotWindowsOrMacOS) await addWindowControls();
|
||||
};
|
||||
await updateMenu();
|
||||
|
||||
document.title = 'Youtube Music';
|
||||
|
||||
ipcRenderer.on('refreshMenu', () => updateMenu());
|
||||
ipcRenderer.on('window-maximize', () => {
|
||||
maximizeButton.removeChild(maximizeButton.firstChild!);
|
||||
maximizeButton.appendChild(unmaximize);
|
||||
});
|
||||
ipcRenderer.on('window-unmaximize', () => {
|
||||
maximizeButton.removeChild(maximizeButton.firstChild!);
|
||||
maximizeButton.appendChild(maximize);
|
||||
});
|
||||
|
||||
if (isEnabled('picture-in-picture')) {
|
||||
ipcRenderer.on('pip-toggle', () => {
|
||||
updateMenu();
|
||||
});
|
||||
}
|
||||
|
||||
// Increases the right margin of Navbar background when the scrollbar is visible to avoid blocking it (z-index doesn't affect it)
|
||||
document.addEventListener('apiLoaded', () => {
|
||||
const htmlHeadStyle = $('head > div > style');
|
||||
if (htmlHeadStyle) {
|
||||
// HACK: This is a hack to remove the scrollbar width
|
||||
htmlHeadStyle.innerHTML = htmlHeadStyle.innerHTML.replace('html::-webkit-scrollbar {width: var(--ytmusic-scrollbar-width);', 'html::-webkit-scrollbar {');
|
||||
}
|
||||
}, { once: true, passive: true });
|
||||
};
|
||||
@ -1,10 +0,0 @@
|
||||
const Icons = {
|
||||
submenu: '<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor" fill="none" stroke-linecap="round" stroke-linejoin="round"><path stroke="none" d="M0 0h24v24H0z" fill="none" /><polyline points="9 6 15 12 9 18" /></svg>',
|
||||
checkbox: '<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor" fill="none" stroke-linecap="round" stroke-linejoin="round"><path stroke="none" d="M0 0h24v24H0z" fill="none"/><path d="M5 12l5 5l10 -10" /></svg>',
|
||||
radio: {
|
||||
checked: '<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 20 20" style="padding: 2px"><path fill="currentColor" d="M10,5 C7.2,5 5,7.2 5,10 C5,12.8 7.2,15 10,15 C12.8,15 15,12.8 15,10 C15,7.2 12.8,5 10,5 L10,5 Z M10,0 C4.5,0 0,4.5 0,10 C0,15.5 4.5,20 10,20 C15.5,20 20,15.5 20,10 C20,4.5 15.5,0 10,0 L10,0 Z M10,18 C5.6,18 2,14.4 2,10 C2,5.6 5.6,2 10,2 C14.4,2 18,5.6 18,10 C18,14.4 14.4,18 10,18 L10,18 Z" /></svg>',
|
||||
unchecked: '<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 20 20" style="padding: 2px"><path fill="currentColor" d="M10,0 C4.5,0 0,4.5 0,10 C0,15.5 4.5,20 10,20 C15.5,20 20,15.5 20,10 C20,4.5 15.5,0 10,0 L10,0 Z M10,18 C5.6,18 2,14.4 2,10 C2,5.6 5.6,2 10,2 C14.4,2 18,5.6 18,10 C18,14.4 14.4,18 10,18 L10,18 Z" /></svg>',
|
||||
},
|
||||
};
|
||||
|
||||
export default Icons;
|
||||
@ -1,134 +0,0 @@
|
||||
import { nativeImage, type MenuItem, ipcRenderer, Menu } from 'electron';
|
||||
|
||||
import Icons from './icons';
|
||||
|
||||
import { ElementFromHtml } from '../../utils';
|
||||
|
||||
interface PanelOptions {
|
||||
placement?: 'bottom' | 'right';
|
||||
order?: number;
|
||||
}
|
||||
|
||||
export const createPanel = (
|
||||
parent: HTMLElement,
|
||||
anchor: HTMLElement,
|
||||
items: MenuItem[],
|
||||
options: PanelOptions = { placement: 'bottom', order: 0 },
|
||||
) => {
|
||||
const childPanels: HTMLElement[] = [];
|
||||
const panel = document.createElement('menu-panel');
|
||||
panel.style.zIndex = `${options.order}`;
|
||||
|
||||
const updateIconState = (iconWrapper: HTMLElement, item: MenuItem) => {
|
||||
if (item.type === 'checkbox') {
|
||||
if (item.checked) iconWrapper.innerHTML = Icons.checkbox;
|
||||
else iconWrapper.innerHTML = '';
|
||||
} else if (item.type === 'radio') {
|
||||
if (item.checked) iconWrapper.innerHTML = Icons.radio.checked;
|
||||
else iconWrapper.innerHTML = Icons.radio.unchecked;
|
||||
} else {
|
||||
const nativeImageIcon = typeof item.icon === 'string' ? nativeImage.createFromPath(item.icon) : item.icon;
|
||||
const iconURL = nativeImageIcon?.toDataURL();
|
||||
|
||||
if (iconURL) iconWrapper.style.background = `url(${iconURL})`;
|
||||
}
|
||||
};
|
||||
|
||||
const radioGroups: [MenuItem, HTMLElement][] = [];
|
||||
items.map((item) => {
|
||||
if (item.type === 'separator') return panel.appendChild(document.createElement('menu-separator'));
|
||||
|
||||
const menu = document.createElement('menu-item');
|
||||
const iconWrapper = document.createElement('menu-icon');
|
||||
|
||||
updateIconState(iconWrapper, item);
|
||||
menu.appendChild(iconWrapper);
|
||||
menu.append(item.label);
|
||||
|
||||
menu.addEventListener('click', async () => {
|
||||
await ipcRenderer.invoke('menu-event', item.commandId);
|
||||
const menuItem = await ipcRenderer.invoke('get-menu-by-id', item.commandId) as MenuItem | null;
|
||||
|
||||
if (menuItem) {
|
||||
updateIconState(iconWrapper, menuItem);
|
||||
|
||||
if (menuItem.type === 'radio') {
|
||||
await Promise.all(
|
||||
radioGroups.map(async ([item, iconWrapper]) => {
|
||||
if (item.commandId === menuItem.commandId) return;
|
||||
const newItem = await ipcRenderer.invoke('get-menu-by-id', item.commandId) as MenuItem | null;
|
||||
|
||||
if (newItem) updateIconState(iconWrapper, newItem);
|
||||
})
|
||||
);
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
if (item.type === 'radio') {
|
||||
radioGroups.push([item, iconWrapper]);
|
||||
}
|
||||
|
||||
if (item.type === 'submenu') {
|
||||
const subMenuIcon = document.createElement('menu-icon');
|
||||
subMenuIcon.appendChild(ElementFromHtml(Icons.submenu));
|
||||
menu.appendChild(subMenuIcon);
|
||||
|
||||
const [child, , children] = createPanel(parent, menu, item.submenu?.items ?? [], {
|
||||
placement: 'right',
|
||||
order: (options?.order ?? 0) + 1,
|
||||
});
|
||||
|
||||
childPanels.push(child);
|
||||
children.push(...children);
|
||||
}
|
||||
|
||||
panel.appendChild(menu);
|
||||
});
|
||||
|
||||
/* methods */
|
||||
const isOpened = () => panel.getAttribute('open') === 'true';
|
||||
const close = () => panel.setAttribute('open', 'false');
|
||||
const open = () => {
|
||||
const rect = anchor.getBoundingClientRect();
|
||||
|
||||
if (options.placement === 'bottom') {
|
||||
panel.style.setProperty('--x', `${rect.x}px`);
|
||||
panel.style.setProperty('--y', `${rect.y + rect.height}px`);
|
||||
} else {
|
||||
panel.style.setProperty('--x', `${rect.x + rect.width}px`);
|
||||
panel.style.setProperty('--y', `${rect.y}px`);
|
||||
}
|
||||
|
||||
panel.setAttribute('open', 'true');
|
||||
|
||||
// Children are placed below their parent item, which can cause
|
||||
// long lists to squeeze their children at the bottom of the screen
|
||||
// (This needs to be done *after* setAttribute)
|
||||
panel.classList.remove('position-by-bottom');
|
||||
if (options.placement === 'right' && panel.scrollHeight > panel.clientHeight ) {
|
||||
panel.style.setProperty('--y', `${rect.y + rect.height}px`);
|
||||
panel.classList.add('position-by-bottom');
|
||||
}
|
||||
};
|
||||
|
||||
anchor.addEventListener('click', () => {
|
||||
if (isOpened()) close();
|
||||
else open();
|
||||
});
|
||||
|
||||
document.body.addEventListener('click', (event) => {
|
||||
const path = event.composedPath();
|
||||
const isInside = path.some((it) => it === panel || it === anchor || childPanels.includes(it as HTMLElement));
|
||||
|
||||
if (!isInside) close();
|
||||
});
|
||||
|
||||
parent.appendChild(panel);
|
||||
|
||||
return [
|
||||
panel,
|
||||
{ isOpened, close, open },
|
||||
childPanels,
|
||||
] as const;
|
||||
};
|
||||
@ -1,180 +0,0 @@
|
||||
:root {
|
||||
--titlebar-background-color: #030303;
|
||||
--menu-bar-height: 36px;
|
||||
}
|
||||
|
||||
title-bar {
|
||||
-webkit-app-region: drag;
|
||||
box-sizing: border-box;
|
||||
|
||||
position: fixed;
|
||||
top: 0;
|
||||
z-index: 10000000;
|
||||
|
||||
width: 100%;
|
||||
height: var(--menu-bar-height, 36px);
|
||||
|
||||
display: flex;
|
||||
flex-flow: row;
|
||||
justify-content: flex-start;
|
||||
align-items: center;
|
||||
gap: 4px;
|
||||
|
||||
color: #f1f1f1;
|
||||
font-size: 14px;
|
||||
padding: 4px 12px;
|
||||
padding-left: var(--offset-left, 12px);
|
||||
background-color: var(--titlebar-background-color, #030303);
|
||||
user-select: none;
|
||||
|
||||
transition: opacity 200ms ease 0s, background-color 300ms cubic-bezier(0.2, 0, 0.6, 1) 0s;
|
||||
}
|
||||
|
||||
menu-button {
|
||||
-webkit-app-region: none;
|
||||
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
align-self: stretch;
|
||||
|
||||
padding: 2px 8px;
|
||||
border-radius: 4px;
|
||||
|
||||
cursor: pointer;
|
||||
}
|
||||
menu-button:hover {
|
||||
background-color: rgba(255, 255, 255, 0.1);
|
||||
}
|
||||
|
||||
menu-panel {
|
||||
position: fixed;
|
||||
top: var(--y, 0);
|
||||
left: var(--x, 0);
|
||||
|
||||
max-height: calc(100vh - var(--menu-bar-height, 36px) - 16px - var(--y, 0));
|
||||
|
||||
display: flex;
|
||||
flex-flow: column;
|
||||
justify-content: flex-start;
|
||||
align-items: stretch;
|
||||
gap: 0;
|
||||
|
||||
overflow: auto;
|
||||
padding: 4px;
|
||||
border-radius: 8px;
|
||||
pointer-events: none;
|
||||
background-color: color-mix(in srgb, var(--titlebar-background-color, #030303) 50%, rgba(0, 0, 0, 0.1));
|
||||
backdrop-filter: blur(8px);
|
||||
box-shadow: 0 0 0 1px rgba(0, 0, 0, 0.05), 0 2px 8px rgba(0, 0, 0, 0.2);
|
||||
|
||||
z-index: 0;
|
||||
opacity: 0;
|
||||
transform: scale(0.8);
|
||||
transform-origin: top left;
|
||||
|
||||
transition: opacity 200ms ease 0s, transform 200ms ease 0s;
|
||||
}
|
||||
menu-panel[open="true"] {
|
||||
pointer-events: all;
|
||||
opacity: 1;
|
||||
transform: scale(1);
|
||||
}
|
||||
menu-panel.position-by-bottom {
|
||||
top: unset;
|
||||
bottom: calc(100vh - var(--y, 100%));
|
||||
max-height: calc(var(--y, 0) - var(--menu-bar-height, 36px) - 16px);
|
||||
}
|
||||
|
||||
menu-item {
|
||||
-webkit-app-region: none;
|
||||
min-height: 32px;
|
||||
height: 32px;
|
||||
|
||||
display: grid;
|
||||
grid-template-columns: 32px 1fr minmax(32px, auto);
|
||||
justify-content: flex-start;
|
||||
align-items: center;
|
||||
|
||||
border-radius: 4px;
|
||||
cursor: pointer;
|
||||
}
|
||||
menu-item:hover {
|
||||
background-color: rgba(255, 255, 255, 0.1);
|
||||
}
|
||||
|
||||
menu-item > menu-icon {
|
||||
height: 32px;
|
||||
padding: 4px;
|
||||
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
menu-separator {
|
||||
min-height: 1px;
|
||||
height: 1px;
|
||||
margin: 4px 0;
|
||||
|
||||
background-color: rgba(255, 255, 255, 0.2);
|
||||
}
|
||||
|
||||
/* classes */
|
||||
|
||||
.title-bar-icon {
|
||||
height: calc(100% - 8px);
|
||||
object-fit: cover;
|
||||
margin-left: -4px;
|
||||
}
|
||||
|
||||
/* Window control container */
|
||||
|
||||
.window-controls-container {
|
||||
-webkit-app-region: no-drag;
|
||||
display: flex;
|
||||
justify-content: flex-end; /* Align to the right end of the title-bar */
|
||||
align-items: center;
|
||||
gap: 4px; /* Add spacing between the window control buttons */
|
||||
position: absolute; /* Position it absolutely within title-bar */
|
||||
right: 4px; /* Adjust the right position as needed */
|
||||
}
|
||||
|
||||
/* Window control buttons */
|
||||
|
||||
.window-control {
|
||||
width: 24px;
|
||||
height: 24px;
|
||||
background: none;
|
||||
border: none;
|
||||
cursor: pointer;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
color: #f1f1f1;
|
||||
font-size: 14px;
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
/* youtube-music style */
|
||||
|
||||
ytmusic-app-layout {
|
||||
margin-top: var(--menu-bar-height, 36px) !important;
|
||||
}
|
||||
|
||||
ytmusic-app-layout>[slot=nav-bar], #nav-bar-background.ytmusic-app-layout {
|
||||
top: var(--menu-bar-height, 36px) !important;
|
||||
}
|
||||
#nav-bar-divider.ytmusic-app-layout {
|
||||
top: calc(var(--ytmusic-nav-bar-height) + var(--menu-bar-height, 36px)) !important;
|
||||
}
|
||||
ytmusic-app[is-bauhaus-sidenav-enabled] #guide-spacer.ytmusic-app,
|
||||
ytmusic-app[is-bauhaus-sidenav-enabled] #mini-guide-spacer.ytmusic-app {
|
||||
margin-top: calc(var(--ytmusic-nav-bar-height) + var(--menu-bar-height, 36px)) !important;
|
||||
}
|
||||
|
||||
ytmusic-app-layout>[slot=player-page] {
|
||||
margin-top: var(--menu-bar-height);
|
||||
height: calc(100vh - var(--menu-bar-height) - var(--ytmusic-nav-bar-height) - var(--ytmusic-player-bar-height)) !important;
|
||||
}
|
||||
|
||||
ytmusic-guide-renderer {
|
||||
height: calc(100vh - var(--menu-bar-height) - var(--ytmusic-nav-bar-height)) !important;
|
||||
}
|
||||
@ -1,203 +0,0 @@
|
||||
import crypto from 'node:crypto';
|
||||
|
||||
import { BrowserWindow, net, shell } from 'electron';
|
||||
|
||||
import { setOptions } from '../../config/plugins';
|
||||
import registerCallback, { SongInfo } from '../../providers/song-info';
|
||||
import defaultConfig from '../../config/defaults';
|
||||
|
||||
import type { ConfigType } from '../../config/dynamic';
|
||||
|
||||
type LastFMOptions = ConfigType<'last-fm'>;
|
||||
|
||||
interface LastFmData {
|
||||
method: string,
|
||||
timestamp?: number,
|
||||
}
|
||||
|
||||
interface LastFmSongData {
|
||||
track?: string,
|
||||
duration?: number,
|
||||
artist?: string,
|
||||
album?: string,
|
||||
api_key: string,
|
||||
sk?: string,
|
||||
format: string,
|
||||
method: string,
|
||||
timestamp?: number,
|
||||
api_sig?: string,
|
||||
}
|
||||
|
||||
const createFormData = (parameters: LastFmSongData) => {
|
||||
// Creates the body for in the post request
|
||||
const formData = new URLSearchParams();
|
||||
for (const key in parameters) {
|
||||
formData.append(key, String(parameters[key as keyof LastFmSongData]));
|
||||
}
|
||||
|
||||
return formData;
|
||||
};
|
||||
|
||||
const createQueryString = (parameters: Record<string, unknown>, apiSignature: string) => {
|
||||
// Creates a querystring
|
||||
const queryData = [];
|
||||
parameters.api_sig = apiSignature;
|
||||
for (const key in parameters) {
|
||||
queryData.push(`${encodeURIComponent(key)}=${encodeURIComponent(String(parameters[key]))}`);
|
||||
}
|
||||
|
||||
return '?' + queryData.join('&');
|
||||
};
|
||||
|
||||
const createApiSig = (parameters: LastFmSongData, secret: string) => {
|
||||
// This function creates the api signature, see: https://www.last.fm/api/authspec
|
||||
const keys = Object.keys(parameters);
|
||||
|
||||
keys.sort();
|
||||
let sig = '';
|
||||
for (const key of keys) {
|
||||
if (String(key) === 'format') {
|
||||
continue;
|
||||
}
|
||||
|
||||
sig += `${key}${parameters[key as keyof LastFmSongData]}`;
|
||||
}
|
||||
|
||||
sig += secret;
|
||||
sig = crypto.createHash('md5').update(sig, 'utf-8').digest('hex');
|
||||
return sig;
|
||||
};
|
||||
|
||||
const createToken = async ({ api_key: apiKey, api_root: apiRoot, secret }: LastFMOptions) => {
|
||||
// Creates and stores the auth token
|
||||
const data = {
|
||||
method: 'auth.gettoken',
|
||||
api_key: apiKey,
|
||||
format: 'json',
|
||||
};
|
||||
const apiSigature = createApiSig(data, secret);
|
||||
const response = await net.fetch(`${apiRoot}${createQueryString(data, apiSigature)}`);
|
||||
const json = await response.json() as Record<string, string>;
|
||||
return json?.token;
|
||||
};
|
||||
|
||||
const authenticate = async (config: LastFMOptions) => {
|
||||
// Asks the user for authentication
|
||||
await shell.openExternal(`https://www.last.fm/api/auth/?api_key=${config.api_key}&token=${config.token}`);
|
||||
};
|
||||
|
||||
const getAndSetSessionKey = async (config: LastFMOptions) => {
|
||||
// Get and store the session key
|
||||
const data = {
|
||||
api_key: config.api_key,
|
||||
format: 'json',
|
||||
method: 'auth.getsession',
|
||||
token: config.token,
|
||||
};
|
||||
const apiSignature = createApiSig(data, config.secret);
|
||||
const response = await net.fetch(`${config.api_root}${createQueryString(data, apiSignature)}`);
|
||||
const json = await response.json() as {
|
||||
error?: string,
|
||||
session?: {
|
||||
key: string,
|
||||
}
|
||||
};
|
||||
if (json.error) {
|
||||
config.token = await createToken(config);
|
||||
await authenticate(config);
|
||||
setOptions('last-fm', config);
|
||||
}
|
||||
if (json.session) {
|
||||
config.session_key = json.session.key;
|
||||
}
|
||||
setOptions('last-fm', config);
|
||||
return config;
|
||||
};
|
||||
|
||||
const postSongDataToAPI = async (songInfo: SongInfo, config: LastFMOptions, data: LastFmData) => {
|
||||
// This sends a post request to the api, and adds the common data
|
||||
if (!config.session_key) {
|
||||
await getAndSetSessionKey(config);
|
||||
}
|
||||
|
||||
const postData: LastFmSongData = {
|
||||
track: songInfo.title,
|
||||
duration: songInfo.songDuration,
|
||||
artist: songInfo.artist,
|
||||
...(songInfo.album ? { album: songInfo.album } : undefined), // Will be undefined if current song is a video
|
||||
api_key: config.api_key,
|
||||
sk: config.session_key,
|
||||
format: 'json',
|
||||
...data,
|
||||
};
|
||||
|
||||
postData.api_sig = createApiSig(postData, config.secret);
|
||||
const formData = createFormData(postData);
|
||||
net.fetch('https://ws.audioscrobbler.com/2.0/', { method: 'POST', body: formData })
|
||||
.catch(async (error: {
|
||||
response?: {
|
||||
data?: {
|
||||
error: number,
|
||||
}
|
||||
}
|
||||
}) => {
|
||||
if (error?.response?.data?.error === 9) {
|
||||
// Session key is invalid, so remove it from the config and reauthenticate
|
||||
config.session_key = undefined;
|
||||
config.token = await createToken(config);
|
||||
await authenticate(config);
|
||||
setOptions('last-fm', config);
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
const addScrobble = (songInfo: SongInfo, config: LastFMOptions) => {
|
||||
// This adds one scrobbled song to last.fm
|
||||
const data = {
|
||||
method: 'track.scrobble',
|
||||
timestamp: Math.trunc((Date.now() - (songInfo.elapsedSeconds ?? 0)) / 1000),
|
||||
};
|
||||
postSongDataToAPI(songInfo, config, data);
|
||||
};
|
||||
|
||||
const setNowPlaying = (songInfo: SongInfo, config: LastFMOptions) => {
|
||||
// This sets the now playing status in last.fm
|
||||
const data = {
|
||||
method: 'track.updateNowPlaying',
|
||||
};
|
||||
postSongDataToAPI(songInfo, config, data);
|
||||
};
|
||||
|
||||
// This will store the timeout that will trigger addScrobble
|
||||
let scrobbleTimer: NodeJS.Timeout | undefined;
|
||||
|
||||
const lastfm = async (_win: BrowserWindow, config: LastFMOptions) => {
|
||||
if (!config.api_root) {
|
||||
// Settings are not present, creating them with the default values
|
||||
config = defaultConfig.plugins['last-fm'];
|
||||
config.enabled = true;
|
||||
setOptions('last-fm', config);
|
||||
}
|
||||
|
||||
if (!config.session_key) {
|
||||
// Not authenticated
|
||||
config = await getAndSetSessionKey(config);
|
||||
}
|
||||
|
||||
registerCallback((songInfo) => {
|
||||
// Set remove the old scrobble timer
|
||||
clearTimeout(scrobbleTimer);
|
||||
if (!songInfo.isPaused) {
|
||||
setNowPlaying(songInfo, config);
|
||||
// Scrobble when the song is halfway through, or has passed the 4-minute mark
|
||||
const scrobbleTime = Math.min(Math.ceil(songInfo.songDuration / 2), 4 * 60);
|
||||
if (scrobbleTime > (songInfo.elapsedSeconds ?? 0)) {
|
||||
// Scrobble still needs to happen
|
||||
const timeToWait = (scrobbleTime - (songInfo.elapsedSeconds ?? 0)) * 1000;
|
||||
scrobbleTimer = setTimeout(addScrobble, timeToWait, songInfo, config);
|
||||
}
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
export default lastfm;
|
||||
@ -1,82 +0,0 @@
|
||||
import { BrowserWindow , net } from 'electron';
|
||||
|
||||
import registerCallback from '../../providers/song-info';
|
||||
|
||||
const secToMilisec = (t?: number) => t ? Math.round(Number(t) * 1e3) : undefined;
|
||||
const previousStatePaused = null;
|
||||
|
||||
type LumiaData = {
|
||||
origin: string;
|
||||
eventType: string;
|
||||
url?: string;
|
||||
videoId?: string;
|
||||
playlistId?: string;
|
||||
cover?: string|null;
|
||||
cover_url?: string|null;
|
||||
title?: string;
|
||||
artists?: string[];
|
||||
status?: string;
|
||||
progress?: number;
|
||||
duration?: number;
|
||||
album_url?: string|null;
|
||||
album?: string|null;
|
||||
views?: number;
|
||||
isPaused?: boolean;
|
||||
}
|
||||
const data: LumiaData = {
|
||||
origin: 'youtubemusic',
|
||||
eventType: 'switchSong',
|
||||
};
|
||||
|
||||
|
||||
const post = (data: LumiaData) => {
|
||||
const port = 39231;
|
||||
const headers = {
|
||||
'Content-Type': 'application/json',
|
||||
'Accept': 'application/json',
|
||||
'Access-Control-Allow-Headers': '*',
|
||||
'Access-Control-Allow-Origin': '*',
|
||||
};
|
||||
const url = `http://localhost:${port}/api/media`;
|
||||
|
||||
net.fetch(url, { method: 'POST', body: JSON.stringify({ token: 'lsmedia_ytmsI7812', data }), headers })
|
||||
.catch((error: { code: number, errno: number }) => {
|
||||
console.log(
|
||||
`Error: '${
|
||||
error.code || error.errno
|
||||
}' - when trying to access lumiastream webserver at port ${port}`
|
||||
);
|
||||
});
|
||||
};
|
||||
|
||||
export default (_: BrowserWindow) => {
|
||||
registerCallback((songInfo) => {
|
||||
if (!songInfo.title && !songInfo.artist) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (previousStatePaused === null) {
|
||||
data.eventType = 'switchSong';
|
||||
} else if (previousStatePaused !== songInfo.isPaused) {
|
||||
data.eventType = 'playPause';
|
||||
}
|
||||
|
||||
data.duration = secToMilisec(songInfo.songDuration);
|
||||
data.progress = secToMilisec(songInfo.elapsedSeconds);
|
||||
data.url = songInfo.url;
|
||||
data.videoId = songInfo.videoId;
|
||||
data.playlistId = songInfo.playlistId;
|
||||
data.cover = songInfo.imageSrc;
|
||||
data.cover_url = songInfo.imageSrc;
|
||||
data.album_url = songInfo.imageSrc;
|
||||
data.title = songInfo.title;
|
||||
data.artists = [songInfo.artist];
|
||||
data.status = songInfo.isPaused ? 'stopped' : 'playing';
|
||||
data.isPaused = songInfo.isPaused;
|
||||
data.album = songInfo.album;
|
||||
data.views = songInfo.views;
|
||||
post(data);
|
||||
});
|
||||
};
|
||||
|
||||
|
||||
@ -1,19 +0,0 @@
|
||||
import { BrowserWindow, MenuItem } from 'electron';
|
||||
|
||||
import { LyricGeniusType, toggleRomanized } from './back';
|
||||
|
||||
import { setOptions } from '../../config/plugins';
|
||||
import { MenuTemplate } from '../../menu';
|
||||
|
||||
export default (_: BrowserWindow, options: LyricGeniusType): MenuTemplate => [
|
||||
{
|
||||
label: 'Romanized Lyrics',
|
||||
type: 'checkbox',
|
||||
checked: options.romanizedLyrics,
|
||||
click(item: MenuItem) {
|
||||
options.romanizedLyrics = item.checked;
|
||||
setOptions('lyrics-genius', options);
|
||||
toggleRomanized();
|
||||
},
|
||||
},
|
||||
];
|
||||
@ -1,13 +0,0 @@
|
||||
import { BrowserWindow } from 'electron';
|
||||
|
||||
import style from './style.css';
|
||||
|
||||
import { injectCSS } from '../utils';
|
||||
|
||||
export function handle(win: BrowserWindow) {
|
||||
injectCSS(win.webContents, style, () => {
|
||||
win.webContents.send('navigation-css-ready');
|
||||
});
|
||||
}
|
||||
|
||||
export default handle;
|
||||
@ -1,20 +0,0 @@
|
||||
import { ipcRenderer } from 'electron';
|
||||
|
||||
import forwardHTML from './templates/forward.html';
|
||||
import backHTML from './templates/back.html';
|
||||
|
||||
import { ElementFromHtml } from '../utils';
|
||||
|
||||
export function run() {
|
||||
ipcRenderer.on('navigation-css-ready', () => {
|
||||
const forwardButton = ElementFromHtml(forwardHTML);
|
||||
const backButton = ElementFromHtml(backHTML);
|
||||
const menu = document.querySelector('#right-content');
|
||||
|
||||
if (menu) {
|
||||
menu.prepend(backButton, forwardButton);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
export default run;
|
||||
@ -1,33 +0,0 @@
|
||||
<div
|
||||
class="style-scope ytmusic-pivot-bar-renderer navigation-item"
|
||||
onclick="history.back()"
|
||||
role="tab"
|
||||
tab-id="FEmusic_back"
|
||||
>
|
||||
<div
|
||||
aria-disabled="false"
|
||||
class="search-icon style-scope ytmusic-search-box"
|
||||
role="button"
|
||||
tabindex="0"
|
||||
title="Go to previous page"
|
||||
>
|
||||
<div
|
||||
class="tab-icon style-scope paper-icon-button navigation-icon"
|
||||
id="icon"
|
||||
>
|
||||
<svg
|
||||
class="style-scope iron-icon"
|
||||
focusable="false"
|
||||
preserveAspectRatio="xMidYMid meet"
|
||||
style="pointer-events: none; display: block; width: 100%; height: 100%"
|
||||
viewBox="0 0 492 492"
|
||||
>
|
||||
<g class="style-scope iron-icon">
|
||||
<path
|
||||
d="M109.3 265.2l218.9 218.9c5.1 5.1 11.8 7.9 19 7.9s14-2.8 19-7.9l16.1-16.1c10.5-10.5 10.5-27.6 0-38.1L198.6 246.1 382.7 62c5.1-5.1 7.9-11.8 7.9-19 0-7.2-2.8-14-7.9-19L366.5 7.9c-5.1-5.1-11.8-7.9-19-7.9-7.2 0-14 2.8-19 7.9L109.3 227c-5.1 5.1-7.9 11.9-7.8 19.1 0 7.2 2.8 14 7.8 19.1z"
|
||||
></path>
|
||||
</g>
|
||||
</svg>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@ -1,9 +0,0 @@
|
||||
import { BrowserWindow } from 'electron';
|
||||
|
||||
import style from './style.css';
|
||||
|
||||
import { injectCSS } from '../utils';
|
||||
|
||||
export default (win: BrowserWindow) => {
|
||||
injectCSS(win.webContents, style);
|
||||
};
|
||||
@ -1,37 +0,0 @@
|
||||
function removeLoginElements() {
|
||||
const elementsToRemove = [
|
||||
'.sign-in-link.ytmusic-nav-bar',
|
||||
'.ytmusic-pivot-bar-renderer[tab-id="FEmusic_liked"]',
|
||||
];
|
||||
|
||||
for (const selector of elementsToRemove) {
|
||||
const node = document.querySelector(selector);
|
||||
if (node) {
|
||||
node.remove();
|
||||
}
|
||||
}
|
||||
|
||||
// Remove the library button
|
||||
const libraryIconPath
|
||||
= 'M16,6v2h-2v5c0,1.1-0.9,2-2,2s-2-0.9-2-2s0.9-2,2-2c0.37,0,0.7,0.11,1,0.28V6H16z M18,20H4V6H3v15h15V20z M21,3H6v15h15V3z M7,4h13v13H7V4z';
|
||||
const observer = new MutationObserver(() => {
|
||||
const menuEntries = document.querySelectorAll(
|
||||
'#items ytmusic-guide-entry-renderer',
|
||||
);
|
||||
menuEntries.forEach((item) => {
|
||||
const icon = item.querySelector('path');
|
||||
if (icon) {
|
||||
observer.disconnect();
|
||||
if (icon.getAttribute('d') === libraryIconPath) {
|
||||
item.remove();
|
||||
}
|
||||
}
|
||||
});
|
||||
});
|
||||
observer.observe(document.documentElement, {
|
||||
childList: true,
|
||||
subtree: true,
|
||||
});
|
||||
}
|
||||
|
||||
export default removeLoginElements;
|
||||
@ -1,5 +0,0 @@
|
||||
import { PluginConfig } from '../../config/dynamic';
|
||||
|
||||
const config = new PluginConfig('notifications');
|
||||
|
||||
export default config;
|
||||
@ -1,257 +0,0 @@
|
||||
import path from 'node:path';
|
||||
|
||||
import { app, BrowserWindow, ipcMain, Notification } from 'electron';
|
||||
|
||||
import { notificationImage, secondsToMinutes, ToastStyles } from './utils';
|
||||
import config from './config';
|
||||
|
||||
import getSongControls from '../../providers/song-controls';
|
||||
import registerCallback, { SongInfo } from '../../providers/song-info';
|
||||
import { changeProtocolHandler } from '../../providers/protocol-handler';
|
||||
import { setTrayOnClick, setTrayOnDoubleClick } from '../../tray';
|
||||
import { getMediaIconLocation, mediaIcons, saveMediaIcon } from '../utils';
|
||||
|
||||
let songControls: ReturnType<typeof getSongControls>;
|
||||
let savedNotification: Notification | undefined;
|
||||
|
||||
export default (win: BrowserWindow) => {
|
||||
songControls = getSongControls(win);
|
||||
|
||||
let currentSeconds = 0;
|
||||
ipcMain.on('apiLoaded', () => win.webContents.send('setupTimeChangedListener'));
|
||||
|
||||
ipcMain.on('timeChanged', (_, t: number) => currentSeconds = t);
|
||||
|
||||
if (app.isPackaged) {
|
||||
saveMediaIcon();
|
||||
}
|
||||
|
||||
let savedSongInfo: SongInfo;
|
||||
let lastUrl: string | undefined;
|
||||
|
||||
// Register songInfoCallback
|
||||
registerCallback((songInfo) => {
|
||||
if (!songInfo.artist && !songInfo.title) {
|
||||
return;
|
||||
}
|
||||
|
||||
savedSongInfo = { ...songInfo };
|
||||
if (!songInfo.isPaused
|
||||
&& (songInfo.url !== lastUrl || config.get('unpauseNotification'))
|
||||
) {
|
||||
lastUrl = songInfo.url;
|
||||
sendNotification(songInfo);
|
||||
}
|
||||
});
|
||||
|
||||
if (config.get('trayControls')) {
|
||||
setTrayOnClick(() => {
|
||||
if (savedNotification) {
|
||||
savedNotification.close();
|
||||
savedNotification = undefined;
|
||||
} else if (savedSongInfo) {
|
||||
sendNotification({
|
||||
...savedSongInfo,
|
||||
elapsedSeconds: currentSeconds,
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
setTrayOnDoubleClick(() => {
|
||||
if (win.isVisible()) {
|
||||
win.hide();
|
||||
} else {
|
||||
win.show();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
app.once('before-quit', () => {
|
||||
savedNotification?.close();
|
||||
});
|
||||
|
||||
changeProtocolHandler(
|
||||
(cmd) => {
|
||||
if (Object.keys(songControls).includes(cmd)) {
|
||||
songControls[cmd as keyof typeof songControls]();
|
||||
if (config.get('refreshOnPlayPause') && (
|
||||
cmd === 'pause'
|
||||
|| (cmd === 'play' && !config.get('unpauseNotification'))
|
||||
)
|
||||
) {
|
||||
setImmediate(() =>
|
||||
sendNotification({
|
||||
...savedSongInfo,
|
||||
isPaused: cmd === 'pause',
|
||||
elapsedSeconds: currentSeconds,
|
||||
}),
|
||||
);
|
||||
}
|
||||
}
|
||||
},
|
||||
);
|
||||
};
|
||||
|
||||
function sendNotification(songInfo: SongInfo) {
|
||||
const iconSrc = notificationImage(songInfo);
|
||||
|
||||
savedNotification?.close();
|
||||
|
||||
let icon: string;
|
||||
if (typeof iconSrc === 'object') {
|
||||
icon = iconSrc.toDataURL();
|
||||
} else {
|
||||
icon = iconSrc;
|
||||
}
|
||||
|
||||
savedNotification = new Notification({
|
||||
title: songInfo.title || 'Playing',
|
||||
body: songInfo.artist,
|
||||
icon: iconSrc,
|
||||
silent: true,
|
||||
// https://learn.microsoft.com/en-us/uwp/schemas/tiles/toastschema/schema-root
|
||||
// https://learn.microsoft.com/en-us/windows/apps/design/shell/tiles-and-notifications/toast-schema
|
||||
// https://learn.microsoft.com/en-us/windows/apps/design/shell/tiles-and-notifications/adaptive-interactive-toasts?tabs=xml
|
||||
// https://learn.microsoft.com/en-us/uwp/api/windows.ui.notifications.toasttemplatetype
|
||||
toastXml: getXml(songInfo, icon),
|
||||
});
|
||||
|
||||
savedNotification.on('close', () => {
|
||||
savedNotification = undefined;
|
||||
});
|
||||
|
||||
savedNotification.show();
|
||||
}
|
||||
|
||||
const getXml = (songInfo: SongInfo, iconSrc: string) => {
|
||||
switch (config.get('toastStyle')) {
|
||||
default:
|
||||
case ToastStyles.logo:
|
||||
case ToastStyles.legacy: {
|
||||
return xmlLogo(songInfo, iconSrc);
|
||||
}
|
||||
|
||||
case ToastStyles.banner_top_custom: {
|
||||
return xmlBannerTopCustom(songInfo, iconSrc);
|
||||
}
|
||||
|
||||
case ToastStyles.hero: {
|
||||
return xmlHero(songInfo, iconSrc);
|
||||
}
|
||||
|
||||
case ToastStyles.banner_bottom: {
|
||||
return xmlBannerBottom(songInfo, iconSrc);
|
||||
}
|
||||
|
||||
case ToastStyles.banner_centered_bottom: {
|
||||
return xmlBannerCenteredBottom(songInfo, iconSrc);
|
||||
}
|
||||
|
||||
case ToastStyles.banner_centered_top: {
|
||||
return xmlBannerCenteredTop(songInfo, iconSrc);
|
||||
}
|
||||
}
|
||||
};
|
||||
const display = (kind: keyof typeof mediaIcons) => {
|
||||
if (config.get('toastStyle') === ToastStyles.legacy) {
|
||||
return `content="${mediaIcons[kind]}"`;
|
||||
}
|
||||
|
||||
return `\
|
||||
content="${config.get('hideButtonText') ? '' : kind.charAt(0).toUpperCase() + kind.slice(1)}"\
|
||||
imageUri="file:///${path.resolve(getMediaIconLocation(), `${kind}.png`)}"
|
||||
`;
|
||||
};
|
||||
|
||||
const getButton = (kind: keyof typeof mediaIcons) =>
|
||||
`<action ${display(kind)} activationType="protocol" arguments="youtubemusic://${kind}"/>`;
|
||||
|
||||
const getButtons = (isPaused: boolean) => `\
|
||||
<actions>
|
||||
${getButton('previous')}
|
||||
${isPaused ? getButton('play') : getButton('pause')}
|
||||
${getButton('next')}
|
||||
</actions>\
|
||||
`;
|
||||
|
||||
const toast = (content: string, isPaused: boolean) => `\
|
||||
<toast>
|
||||
<audio silent="true" />
|
||||
<visual>
|
||||
<binding template="ToastGeneric">
|
||||
${content}
|
||||
</binding>
|
||||
</visual>
|
||||
|
||||
${getButtons(isPaused)}
|
||||
</toast>`;
|
||||
|
||||
const xmlImage = ({ title, artist, isPaused }: SongInfo, imgSrc: string, placement: string) => toast(`\
|
||||
<image id="1" src="${imgSrc}" name="Image" ${placement}/>
|
||||
<text id="1">${title}</text>
|
||||
<text id="2">${artist}</text>\
|
||||
`, isPaused ?? false);
|
||||
|
||||
const xmlLogo = (songInfo: SongInfo, imgSrc: string) => xmlImage(songInfo, imgSrc, 'placement="appLogoOverride"');
|
||||
|
||||
const xmlHero = (songInfo: SongInfo, imgSrc: string) => xmlImage(songInfo, imgSrc, 'placement="hero"');
|
||||
|
||||
const xmlBannerBottom = (songInfo: SongInfo, imgSrc: string) => xmlImage(songInfo, imgSrc, '');
|
||||
|
||||
const xmlBannerTopCustom = (songInfo: SongInfo, imgSrc: string) => toast(`\
|
||||
<image id="1" src="${imgSrc}" name="Image" />
|
||||
<text>ㅤ</text>
|
||||
<group>
|
||||
<subgroup>
|
||||
<text hint-style="body">${songInfo.title}</text>
|
||||
<text hint-style="captionSubtle">${songInfo.artist}</text>
|
||||
</subgroup>
|
||||
${xmlMoreData(songInfo)}
|
||||
</group>\
|
||||
`, songInfo.isPaused ?? false);
|
||||
|
||||
const xmlMoreData = ({ album, elapsedSeconds, songDuration }: SongInfo) => `\
|
||||
<subgroup hint-textStacking="bottom">
|
||||
${album
|
||||
? `<text hint-style="captionSubtle" hint-wrap="true" hint-align="right">${album}</text>` : ''}
|
||||
<text hint-style="captionSubtle" hint-wrap="true" hint-align="right">${secondsToMinutes(elapsedSeconds ?? 0)} / ${secondsToMinutes(songDuration)}</text>
|
||||
</subgroup>\
|
||||
`;
|
||||
|
||||
const xmlBannerCenteredBottom = ({ title, artist, isPaused }: SongInfo, imgSrc: string) => toast(`\
|
||||
<text>ㅤ</text>
|
||||
<group>
|
||||
<subgroup hint-weight="1" hint-textStacking="center">
|
||||
<text hint-align="center" hint-style="${titleFontPicker(title)}">${title}</text>
|
||||
<text hint-align="center" hint-style="SubtitleSubtle">${artist}</text>
|
||||
</subgroup>
|
||||
</group>
|
||||
<image id="1" src="${imgSrc}" name="Image" hint-removeMargin="true" />\
|
||||
`, isPaused ?? false);
|
||||
|
||||
const xmlBannerCenteredTop = ({ title, artist, isPaused }: SongInfo, imgSrc: string) => toast(`\
|
||||
<image id="1" src="${imgSrc}" name="Image" />
|
||||
<text>ㅤ</text>
|
||||
<group>
|
||||
<subgroup hint-weight="1" hint-textStacking="center">
|
||||
<text hint-align="center" hint-style="${titleFontPicker(title)}">${title}</text>
|
||||
<text hint-align="center" hint-style="SubtitleSubtle">${artist}</text>
|
||||
</subgroup>
|
||||
</group>\
|
||||
`, isPaused ?? false);
|
||||
|
||||
const titleFontPicker = (title: string) => {
|
||||
if (title.length <= 13) {
|
||||
return 'Header';
|
||||
}
|
||||
|
||||
if (title.length <= 22) {
|
||||
return 'Subheader';
|
||||
}
|
||||
|
||||
if (title.length <= 26) {
|
||||
return 'Title';
|
||||
}
|
||||
|
||||
return 'Subtitle';
|
||||
};
|
||||
@ -1,93 +0,0 @@
|
||||
import is from 'electron-is';
|
||||
|
||||
import { BrowserWindow, MenuItem } from 'electron';
|
||||
|
||||
import { snakeToCamel, ToastStyles, urgencyLevels } from './utils';
|
||||
|
||||
import config from './config';
|
||||
|
||||
import { MenuTemplate } from '../../menu';
|
||||
|
||||
import type { ConfigType } from '../../config/dynamic';
|
||||
|
||||
const getMenu = (options: ConfigType<'notifications'>): MenuTemplate => {
|
||||
if (is.linux()) {
|
||||
return [
|
||||
{
|
||||
label: 'Notification Priority',
|
||||
submenu: urgencyLevels.map((level) => ({
|
||||
label: level.name,
|
||||
type: 'radio',
|
||||
checked: options.urgency === level.value,
|
||||
click: () => config.set('urgency', level.value),
|
||||
})),
|
||||
}
|
||||
];
|
||||
} else if (is.windows()) {
|
||||
return [
|
||||
{
|
||||
label: 'Interactive Notifications',
|
||||
type: 'checkbox',
|
||||
checked: options.interactive,
|
||||
// Doesn't update until restart
|
||||
click: (item: MenuItem) => config.setAndMaybeRestart('interactive', item.checked),
|
||||
},
|
||||
{
|
||||
// Submenu with settings for interactive notifications (name shouldn't be too long)
|
||||
label: 'Interactive Settings',
|
||||
submenu: [
|
||||
{
|
||||
label: 'Open/Close on tray click',
|
||||
type: 'checkbox',
|
||||
checked: options.trayControls,
|
||||
click: (item: MenuItem) => config.set('trayControls', item.checked),
|
||||
},
|
||||
{
|
||||
label: 'Hide Button Text',
|
||||
type: 'checkbox',
|
||||
checked: options.hideButtonText,
|
||||
click: (item: MenuItem) => config.set('hideButtonText', item.checked),
|
||||
},
|
||||
{
|
||||
label: 'Refresh on Play/Pause',
|
||||
type: 'checkbox',
|
||||
checked: options.refreshOnPlayPause,
|
||||
click: (item: MenuItem) => config.set('refreshOnPlayPause', item.checked),
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
label: 'Style',
|
||||
submenu: getToastStyleMenuItems(options),
|
||||
},
|
||||
];
|
||||
} else {
|
||||
return [];
|
||||
}
|
||||
};
|
||||
|
||||
export default (_win: BrowserWindow, options: ConfigType<'notifications'>): MenuTemplate => [
|
||||
...getMenu(options),
|
||||
{
|
||||
label: 'Show notification on unpause',
|
||||
type: 'checkbox',
|
||||
checked: options.unpauseNotification,
|
||||
click: (item: MenuItem) => config.set('unpauseNotification', item.checked),
|
||||
},
|
||||
];
|
||||
|
||||
export function getToastStyleMenuItems(options: ConfigType<'notifications'>) {
|
||||
const array = Array.from({ length: Object.keys(ToastStyles).length });
|
||||
|
||||
// ToastStyles index starts from 1
|
||||
for (const [name, index] of Object.entries(ToastStyles)) {
|
||||
array[index - 1] = {
|
||||
label: snakeToCamel(name),
|
||||
type: 'radio',
|
||||
checked: options.toastStyle === index,
|
||||
click: () => config.set('toastStyle', index),
|
||||
} satisfies Electron.MenuItemConstructorOptions;
|
||||
}
|
||||
|
||||
return array as Electron.MenuItemConstructorOptions[];
|
||||
}
|
||||
@ -1,111 +0,0 @@
|
||||
import { app, BrowserWindow, ipcMain } from 'electron';
|
||||
|
||||
import style from './style.css';
|
||||
|
||||
import { injectCSS } from '../utils';
|
||||
import { setOptions as setPluginOptions } from '../../config/plugins';
|
||||
|
||||
import type { ConfigType } from '../../config/dynamic';
|
||||
|
||||
let isInPiP = false;
|
||||
let originalPosition: number[];
|
||||
let originalSize: number[];
|
||||
let originalFullScreen: boolean;
|
||||
let originalMaximized: boolean;
|
||||
|
||||
let win: BrowserWindow;
|
||||
|
||||
type PiPOptions = ConfigType<'picture-in-picture'>;
|
||||
|
||||
let options: Partial<PiPOptions>;
|
||||
|
||||
const pipPosition = () => (options.savePosition && options['pip-position']) || [10, 10];
|
||||
const pipSize = () => (options.saveSize && options['pip-size']) || [450, 275];
|
||||
|
||||
const setLocalOptions = (_options: Partial<PiPOptions>) => {
|
||||
options = { ...options, ..._options };
|
||||
setPluginOptions('picture-in-picture', _options);
|
||||
};
|
||||
|
||||
const togglePiP = () => {
|
||||
isInPiP = !isInPiP;
|
||||
setLocalOptions({ isInPiP });
|
||||
|
||||
if (isInPiP) {
|
||||
originalFullScreen = win.isFullScreen();
|
||||
if (originalFullScreen) {
|
||||
win.setFullScreen(false);
|
||||
}
|
||||
|
||||
originalMaximized = win.isMaximized();
|
||||
if (originalMaximized) {
|
||||
win.unmaximize();
|
||||
}
|
||||
|
||||
originalPosition = win.getPosition();
|
||||
originalSize = win.getSize();
|
||||
|
||||
win.webContents.on('before-input-event', blockShortcutsInPiP);
|
||||
|
||||
win.setMaximizable(false);
|
||||
win.setFullScreenable(false);
|
||||
|
||||
win.webContents.send('pip-toggle', true);
|
||||
|
||||
app.dock?.hide();
|
||||
win.setVisibleOnAllWorkspaces(true, {
|
||||
visibleOnFullScreen: true,
|
||||
});
|
||||
app.dock?.show();
|
||||
if (options.alwaysOnTop) {
|
||||
win.setAlwaysOnTop(true, 'screen-saver', 1);
|
||||
}
|
||||
} else {
|
||||
win.webContents.removeListener('before-input-event', blockShortcutsInPiP);
|
||||
win.setMaximizable(true);
|
||||
win.setFullScreenable(true);
|
||||
|
||||
win.webContents.send('pip-toggle', false);
|
||||
|
||||
win.setVisibleOnAllWorkspaces(false);
|
||||
win.setAlwaysOnTop(false);
|
||||
|
||||
if (originalFullScreen) {
|
||||
win.setFullScreen(true);
|
||||
}
|
||||
|
||||
if (originalMaximized) {
|
||||
win.maximize();
|
||||
}
|
||||
}
|
||||
|
||||
const [x, y] = isInPiP ? pipPosition() : originalPosition;
|
||||
const [w, h] = isInPiP ? pipSize() : originalSize;
|
||||
win.setPosition(x, y);
|
||||
win.setSize(w, h);
|
||||
|
||||
win.setWindowButtonVisibility?.(!isInPiP);
|
||||
};
|
||||
|
||||
const blockShortcutsInPiP = (event: Electron.Event, input: Electron.Input) => {
|
||||
const key = input.key.toLowerCase();
|
||||
|
||||
if (key === 'f') {
|
||||
event.preventDefault();
|
||||
} else if (key === 'escape') {
|
||||
togglePiP();
|
||||
event.preventDefault();
|
||||
}
|
||||
};
|
||||
|
||||
export default (_win: BrowserWindow, _options: PiPOptions) => {
|
||||
options ??= _options;
|
||||
win ??= _win;
|
||||
setLocalOptions({ isInPiP });
|
||||
injectCSS(win.webContents, style);
|
||||
ipcMain.on('picture-in-picture', () => {
|
||||
togglePiP();
|
||||
});
|
||||
};
|
||||
|
||||
export const setOptions = setLocalOptions;
|
||||
@ -1,75 +0,0 @@
|
||||
import prompt from 'custom-electron-prompt';
|
||||
|
||||
import { BrowserWindow } from 'electron';
|
||||
|
||||
import { setOptions } from './back';
|
||||
|
||||
import promptOptions from '../../providers/prompt-options';
|
||||
|
||||
import { MenuTemplate } from '../../menu';
|
||||
|
||||
import type { ConfigType } from '../../config/dynamic';
|
||||
|
||||
export default (win: BrowserWindow, options: ConfigType<'picture-in-picture'>): MenuTemplate => [
|
||||
{
|
||||
label: 'Always on top',
|
||||
type: 'checkbox',
|
||||
checked: options.alwaysOnTop,
|
||||
click(item) {
|
||||
setOptions({ alwaysOnTop: item.checked });
|
||||
win.setAlwaysOnTop(item.checked);
|
||||
},
|
||||
},
|
||||
{
|
||||
label: 'Save window position',
|
||||
type: 'checkbox',
|
||||
checked: options.savePosition,
|
||||
click(item) {
|
||||
setOptions({ savePosition: item.checked });
|
||||
},
|
||||
},
|
||||
{
|
||||
label: 'Save window size',
|
||||
type: 'checkbox',
|
||||
checked: options.saveSize,
|
||||
click(item) {
|
||||
setOptions({ saveSize: item.checked });
|
||||
},
|
||||
},
|
||||
{
|
||||
label: 'Hotkey',
|
||||
type: 'checkbox',
|
||||
checked: !!options.hotkey,
|
||||
async click(item) {
|
||||
const output = await prompt({
|
||||
title: 'Picture in Picture Hotkey',
|
||||
label: 'Choose a hotkey for toggling Picture in Picture',
|
||||
type: 'keybind',
|
||||
keybindOptions: [{
|
||||
value: 'hotkey',
|
||||
label: 'Hotkey',
|
||||
default: options.hotkey,
|
||||
}],
|
||||
...promptOptions(),
|
||||
}, win);
|
||||
|
||||
if (output) {
|
||||
const { value, accelerator } = output[0];
|
||||
setOptions({ [value]: accelerator });
|
||||
|
||||
item.checked = !!accelerator;
|
||||
} else {
|
||||
// Reset checkbox if prompt was canceled
|
||||
item.checked = !item.checked;
|
||||
}
|
||||
},
|
||||
},
|
||||
{
|
||||
label: 'Use native PiP',
|
||||
type: 'checkbox',
|
||||
checked: options.useNativePiP,
|
||||
click(item) {
|
||||
setOptions({ useNativePiP: item.checked });
|
||||
},
|
||||
},
|
||||
];
|
||||
@ -1,117 +0,0 @@
|
||||
import sliderHTML from './templates/slider.html';
|
||||
|
||||
import { getSongMenu } from '../../providers/dom-elements';
|
||||
import { ElementFromHtml } from '../utils';
|
||||
import { singleton } from '../../providers/decorators';
|
||||
|
||||
|
||||
function $<E extends Element = Element>(selector: string) {
|
||||
return document.querySelector<E>(selector);
|
||||
}
|
||||
|
||||
const slider = ElementFromHtml(sliderHTML);
|
||||
|
||||
const roundToTwo = (n: number) => Math.round(n * 1e2) / 1e2;
|
||||
|
||||
const MIN_PLAYBACK_SPEED = 0.07;
|
||||
const MAX_PLAYBACK_SPEED = 16;
|
||||
|
||||
let playbackSpeed = 1;
|
||||
|
||||
const updatePlayBackSpeed = () => {
|
||||
const videoElement = $<HTMLVideoElement>('video');
|
||||
if (videoElement) {
|
||||
videoElement.playbackRate = playbackSpeed;
|
||||
}
|
||||
|
||||
const playbackSpeedElement = $('#playback-speed-value');
|
||||
if (playbackSpeedElement) {
|
||||
playbackSpeedElement.innerHTML = String(playbackSpeed);
|
||||
}
|
||||
};
|
||||
|
||||
let menu: Element | null = null;
|
||||
|
||||
const setupSliderListener = singleton(() => {
|
||||
$('#playback-speed-slider')?.addEventListener('immediate-value-changed', (e) => {
|
||||
playbackSpeed = (e as CustomEvent<{ value: number; }>).detail.value || MIN_PLAYBACK_SPEED;
|
||||
if (isNaN(playbackSpeed)) {
|
||||
playbackSpeed = 1;
|
||||
}
|
||||
|
||||
updatePlayBackSpeed();
|
||||
});
|
||||
});
|
||||
|
||||
const observePopupContainer = () => {
|
||||
const observer = new MutationObserver(() => {
|
||||
if (!menu) {
|
||||
menu = getSongMenu();
|
||||
}
|
||||
|
||||
if (
|
||||
menu &&
|
||||
(menu.parentElement as HTMLElement & { eventSink_: Element | null })
|
||||
?.eventSink_
|
||||
?.matches('ytmusic-menu-renderer.ytmusic-player-bar')&& !menu.contains(slider)
|
||||
) {
|
||||
menu.prepend(slider);
|
||||
setupSliderListener();
|
||||
}
|
||||
});
|
||||
|
||||
const popupContainer = $('ytmusic-popup-container');
|
||||
if (popupContainer) {
|
||||
observer.observe(popupContainer, {
|
||||
childList: true,
|
||||
subtree: true,
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
const observeVideo = () => {
|
||||
const video = $<HTMLVideoElement>('video');
|
||||
if (video) {
|
||||
video.addEventListener('ratechange', forcePlaybackRate);
|
||||
video.addEventListener('srcChanged', forcePlaybackRate);
|
||||
}
|
||||
};
|
||||
|
||||
const setupWheelListener = () => {
|
||||
slider.addEventListener('wheel', (e) => {
|
||||
e.preventDefault();
|
||||
if (isNaN(playbackSpeed)) {
|
||||
playbackSpeed = 1;
|
||||
}
|
||||
|
||||
// E.deltaY < 0 means wheel-up
|
||||
playbackSpeed = roundToTwo(e.deltaY < 0
|
||||
? Math.min(playbackSpeed + 0.01, MAX_PLAYBACK_SPEED)
|
||||
: Math.max(playbackSpeed - 0.01, MIN_PLAYBACK_SPEED),
|
||||
);
|
||||
|
||||
updatePlayBackSpeed();
|
||||
// Update slider position
|
||||
const playbackSpeedSilder = $<HTMLElement & { value: number }>('#playback-speed-slider');
|
||||
if (playbackSpeedSilder) {
|
||||
playbackSpeedSilder.value = playbackSpeed;
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
function forcePlaybackRate(e: Event) {
|
||||
if (e.target instanceof HTMLVideoElement) {
|
||||
const videoElement = e.target;
|
||||
if (videoElement.playbackRate !== playbackSpeed) {
|
||||
videoElement.playbackRate = playbackSpeed;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export default () => {
|
||||
document.addEventListener('apiLoaded', () => {
|
||||
observePopupContainer();
|
||||
observeVideo();
|
||||
setupWheelListener();
|
||||
}, { once: true, passive: true });
|
||||
};
|
||||
@ -1,28 +0,0 @@
|
||||
import { globalShortcut, BrowserWindow } from 'electron';
|
||||
|
||||
import volumeHudStyle from './volume-hud.css';
|
||||
|
||||
import { injectCSS } from '../utils';
|
||||
|
||||
import type { ConfigType } from '../../config/dynamic';
|
||||
|
||||
/*
|
||||
This is used to determine if plugin is actually active
|
||||
(not if it's only enabled in options)
|
||||
*/
|
||||
let isEnabled = false;
|
||||
|
||||
export const enabled = () => isEnabled;
|
||||
|
||||
export default (win: BrowserWindow, options: ConfigType<'precise-volume'>) => {
|
||||
isEnabled = true;
|
||||
injectCSS(win.webContents, volumeHudStyle);
|
||||
|
||||
if (options.globalShortcuts?.volumeUp) {
|
||||
globalShortcut.register((options.globalShortcuts.volumeUp), () => win.webContents.send('changeVolume', true));
|
||||
}
|
||||
|
||||
if (options.globalShortcuts?.volumeDown) {
|
||||
globalShortcut.register((options.globalShortcuts.volumeDown), () => win.webContents.send('changeVolume', false));
|
||||
}
|
||||
};
|
||||
@ -1,270 +0,0 @@
|
||||
import { ipcRenderer } from 'electron';
|
||||
|
||||
import { setOptions, setMenuOptions, isEnabled } from '../../config/plugins';
|
||||
import { debounce } from '../../providers/decorators';
|
||||
|
||||
import { YoutubePlayer } from '../../types/youtube-player';
|
||||
|
||||
import type { ConfigType } from '../../config/dynamic';
|
||||
|
||||
function $<E extends Element = Element>(selector: string) {
|
||||
return document.querySelector<E>(selector);
|
||||
}
|
||||
|
||||
let api: YoutubePlayer;
|
||||
let options: ConfigType<'precise-volume'>;
|
||||
|
||||
export default (_options: ConfigType<'precise-volume'>) => {
|
||||
options = _options;
|
||||
document.addEventListener('apiLoaded', (e) => {
|
||||
api = e.detail;
|
||||
ipcRenderer.on('changeVolume', (_, toIncrease: boolean) => changeVolume(toIncrease));
|
||||
ipcRenderer.on('setVolume', (_, value: number) => setVolume(value));
|
||||
firstRun();
|
||||
}, { once: true, passive: true });
|
||||
};
|
||||
|
||||
// Without this function it would rewrite config 20 time when volume change by 20
|
||||
const writeOptions = debounce(() => {
|
||||
setOptions('precise-volume', options);
|
||||
}, 1000);
|
||||
|
||||
export const moveVolumeHud = debounce((showVideo: boolean) => {
|
||||
const volumeHud = $<HTMLElement>('#volumeHud');
|
||||
if (!volumeHud) {
|
||||
return;
|
||||
}
|
||||
|
||||
volumeHud.style.top = showVideo
|
||||
? `${($('ytmusic-player')!.clientHeight - $('video')!.clientHeight) / 2}px`
|
||||
: '0';
|
||||
}, 250);
|
||||
|
||||
const hideVolumeHud = debounce((volumeHud: HTMLElement) => {
|
||||
volumeHud.style.opacity = '0';
|
||||
}, 2000);
|
||||
|
||||
const hideVolumeSlider = debounce((slider: HTMLElement) => {
|
||||
slider.classList.remove('on-hover');
|
||||
}, 2500);
|
||||
|
||||
/** Restore saved volume and setup tooltip */
|
||||
function firstRun() {
|
||||
if (typeof options.savedVolume === 'number') {
|
||||
// Set saved volume as tooltip
|
||||
setTooltip(options.savedVolume);
|
||||
|
||||
if (api.getVolume() !== options.savedVolume) {
|
||||
setVolume(options.savedVolume);
|
||||
}
|
||||
}
|
||||
|
||||
setupPlaybar();
|
||||
|
||||
setupLocalArrowShortcuts();
|
||||
|
||||
// Workaround: computedStyleMap().get(string) returns CSSKeywordValue instead of CSSStyleValue
|
||||
const noVid = ($('#main-panel')?.computedStyleMap().get('display') as CSSKeywordValue)?.value === 'none';
|
||||
injectVolumeHud(noVid);
|
||||
if (!noVid) {
|
||||
setupVideoPlayerOnwheel();
|
||||
if (!isEnabled('video-toggle')) {
|
||||
// Video-toggle handles hud positioning on its own
|
||||
const videoMode = () => api.getPlayerResponse().videoDetails?.musicVideoType !== 'MUSIC_VIDEO_TYPE_ATV';
|
||||
$('video')?.addEventListener('srcChanged', () => moveVolumeHud(videoMode()));
|
||||
}
|
||||
}
|
||||
|
||||
// Change options from renderer to keep sync
|
||||
ipcRenderer.on('setOptions', (_event, newOptions = {}) => {
|
||||
Object.assign(options, newOptions);
|
||||
setMenuOptions('precise-volume', options);
|
||||
});
|
||||
}
|
||||
|
||||
function injectVolumeHud(noVid: boolean) {
|
||||
if (noVid) {
|
||||
const position = 'top: 18px; right: 60px;';
|
||||
const mainStyle = 'font-size: xx-large;';
|
||||
|
||||
$('.center-content.ytmusic-nav-bar')?.insertAdjacentHTML(
|
||||
'beforeend',
|
||||
`<span id="volumeHud" style="${position + mainStyle}"></span>`,
|
||||
);
|
||||
} else {
|
||||
const position = 'top: 10px; left: 10px;';
|
||||
const mainStyle = 'font-size: xxx-large; webkit-text-stroke: 1px black; font-weight: 600;';
|
||||
|
||||
$('#song-video')?.insertAdjacentHTML(
|
||||
'afterend',
|
||||
`<span id="volumeHud" style="${position + mainStyle}"></span>`,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
function showVolumeHud(volume: number) {
|
||||
const volumeHud = $<HTMLElement>('#volumeHud');
|
||||
if (!volumeHud) {
|
||||
return;
|
||||
}
|
||||
|
||||
volumeHud.textContent = `${volume}%`;
|
||||
volumeHud.style.opacity = '1';
|
||||
|
||||
hideVolumeHud(volumeHud);
|
||||
}
|
||||
|
||||
/** Add onwheel event to video player */
|
||||
function setupVideoPlayerOnwheel() {
|
||||
const panel = $<HTMLElement>('#main-panel');
|
||||
if (!panel) return;
|
||||
|
||||
panel.addEventListener('wheel', (event) => {
|
||||
event.preventDefault();
|
||||
// Event.deltaY < 0 means wheel-up
|
||||
changeVolume(event.deltaY < 0);
|
||||
});
|
||||
}
|
||||
|
||||
function saveVolume(volume: number) {
|
||||
options.savedVolume = volume;
|
||||
writeOptions();
|
||||
}
|
||||
|
||||
/** Add onwheel event to play bar and also track if play bar is hovered */
|
||||
function setupPlaybar() {
|
||||
const playerbar = $<HTMLElement>('ytmusic-player-bar');
|
||||
if (!playerbar) return;
|
||||
|
||||
playerbar.addEventListener('wheel', (event) => {
|
||||
event.preventDefault();
|
||||
// Event.deltaY < 0 means wheel-up
|
||||
changeVolume(event.deltaY < 0);
|
||||
});
|
||||
|
||||
// Keep track of mouse position for showVolumeSlider()
|
||||
playerbar.addEventListener('mouseenter', () => {
|
||||
playerbar.classList.add('on-hover');
|
||||
});
|
||||
|
||||
playerbar.addEventListener('mouseleave', () => {
|
||||
playerbar.classList.remove('on-hover');
|
||||
});
|
||||
|
||||
setupSliderObserver();
|
||||
}
|
||||
|
||||
/** Save volume + Update the volume tooltip when volume-slider is manually changed */
|
||||
function setupSliderObserver() {
|
||||
const sliderObserver = new MutationObserver((mutations) => {
|
||||
for (const mutation of mutations) {
|
||||
if (mutation.target instanceof HTMLInputElement) {
|
||||
// This checks that volume-slider was manually set
|
||||
const target = mutation.target;
|
||||
const targetValueNumeric = Number(target.value);
|
||||
if (mutation.oldValue !== target.value
|
||||
&& (typeof options.savedVolume !== 'number' || Math.abs(options.savedVolume - targetValueNumeric) > 4)) {
|
||||
// Diff>4 means it was manually set
|
||||
setTooltip(targetValueNumeric);
|
||||
saveVolume(targetValueNumeric);
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
const slider = $('#volume-slider');
|
||||
if (!slider) return;
|
||||
|
||||
// Observing only changes in 'value' of volume-slider
|
||||
sliderObserver.observe(slider, {
|
||||
attributeFilter: ['value'],
|
||||
attributeOldValue: true,
|
||||
});
|
||||
}
|
||||
|
||||
function setVolume(value: number) {
|
||||
api.setVolume(value);
|
||||
// Save the new volume
|
||||
saveVolume(value);
|
||||
|
||||
// Change slider position (important)
|
||||
updateVolumeSlider();
|
||||
|
||||
// Change tooltips to new value
|
||||
setTooltip(value);
|
||||
// Show volume slider
|
||||
showVolumeSlider();
|
||||
// Show volume HUD
|
||||
showVolumeHud(value);
|
||||
}
|
||||
|
||||
/** If (toIncrease = false) then volume decrease */
|
||||
function changeVolume(toIncrease: boolean) {
|
||||
// Apply volume change if valid
|
||||
const steps = Number(options.steps || 1);
|
||||
setVolume(toIncrease
|
||||
? Math.min(api.getVolume() + steps, 100)
|
||||
: Math.max(api.getVolume() - steps, 0));
|
||||
}
|
||||
|
||||
function updateVolumeSlider() {
|
||||
const savedVolume = options.savedVolume ?? 0;
|
||||
// Slider value automatically rounds to multiples of 5
|
||||
for (const slider of ['#volume-slider', '#expand-volume-slider']) {
|
||||
const silderElement = $<HTMLInputElement>(slider);
|
||||
if (silderElement) {
|
||||
silderElement.value = String(savedVolume > 0 && savedVolume < 5 ? 5 : savedVolume);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function showVolumeSlider() {
|
||||
const slider = $<HTMLElement>('#volume-slider');
|
||||
if (!slider) return;
|
||||
|
||||
// This class display the volume slider if not in minimized mode
|
||||
slider.classList.add('on-hover');
|
||||
|
||||
hideVolumeSlider(slider);
|
||||
}
|
||||
|
||||
// Set new volume as tooltip for volume slider and icon + expanding slider (appears when window size is small)
|
||||
const tooltipTargets = [
|
||||
'#volume-slider',
|
||||
'tp-yt-paper-icon-button.volume',
|
||||
'#expand-volume-slider',
|
||||
'#expand-volume',
|
||||
];
|
||||
|
||||
function setTooltip(volume: number) {
|
||||
for (const target of tooltipTargets) {
|
||||
const tooltipTargetElement = $<HTMLElement>(target);
|
||||
if (tooltipTargetElement) {
|
||||
tooltipTargetElement.title = `${volume}%`;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function setupLocalArrowShortcuts() {
|
||||
if (options.arrowsShortcut) {
|
||||
window.addEventListener('keydown', (event) => {
|
||||
if ($<HTMLElement & { opened: boolean }>('ytmusic-search-box')?.opened) {
|
||||
return;
|
||||
}
|
||||
|
||||
switch (event.code) {
|
||||
case 'ArrowUp': {
|
||||
event.preventDefault();
|
||||
changeVolume(true);
|
||||
break;
|
||||
}
|
||||
|
||||
case 'ArrowDown': {
|
||||
event.preventDefault();
|
||||
changeVolume(false);
|
||||
break;
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
@ -1,94 +0,0 @@
|
||||
import prompt, { KeybindOptions } from 'custom-electron-prompt';
|
||||
|
||||
import { BrowserWindow, MenuItem } from 'electron';
|
||||
|
||||
import { enabled } from './back';
|
||||
|
||||
import { setMenuOptions } from '../../config/plugins';
|
||||
import promptOptions from '../../providers/prompt-options';
|
||||
import { MenuTemplate } from '../../menu';
|
||||
|
||||
import type { ConfigType } from '../../config/dynamic';
|
||||
|
||||
function changeOptions(changedOptions: Partial<ConfigType<'precise-volume'>>, options: ConfigType<'precise-volume'>, win: BrowserWindow) {
|
||||
for (const option in changedOptions) {
|
||||
// HACK: Weird TypeScript error
|
||||
(options as Record<string, unknown>)[option] = (changedOptions as Record<string, unknown>)[option];
|
||||
}
|
||||
// Dynamically change setting if plugin is enabled
|
||||
if (enabled()) {
|
||||
win.webContents.send('setOptions', changedOptions);
|
||||
} else { // Fallback to usual method if disabled
|
||||
setMenuOptions('precise-volume', options);
|
||||
}
|
||||
}
|
||||
|
||||
export default (win: BrowserWindow, options: ConfigType<'precise-volume'>): MenuTemplate => [
|
||||
{
|
||||
label: 'Local Arrowkeys Controls',
|
||||
type: 'checkbox',
|
||||
checked: Boolean(options.arrowsShortcut),
|
||||
click(item) {
|
||||
changeOptions({ arrowsShortcut: item.checked }, options, win);
|
||||
},
|
||||
},
|
||||
{
|
||||
label: 'Global Hotkeys',
|
||||
type: 'checkbox',
|
||||
checked: Boolean(options.globalShortcuts?.volumeUp ?? options.globalShortcuts?.volumeDown),
|
||||
click: (item) => promptGlobalShortcuts(win, options, item),
|
||||
},
|
||||
{
|
||||
label: 'Set Custom Volume Steps',
|
||||
click: () => promptVolumeSteps(win, options),
|
||||
},
|
||||
];
|
||||
|
||||
// Helper function for globalShortcuts prompt
|
||||
const kb = (label_: string, value_: string, default_: string): KeybindOptions => ({ 'value': value_, 'label': label_, 'default': default_ || undefined });
|
||||
|
||||
async function promptVolumeSteps(win: BrowserWindow, options: ConfigType<'precise-volume'>) {
|
||||
const output = await prompt({
|
||||
title: 'Volume Steps',
|
||||
label: 'Choose Volume Increase/Decrease Steps',
|
||||
value: options.steps || 1,
|
||||
type: 'counter',
|
||||
counterOptions: { minimum: 0, maximum: 100, multiFire: true },
|
||||
width: 380,
|
||||
...promptOptions(),
|
||||
}, win);
|
||||
|
||||
if (output || output === 0) { // 0 is somewhat valid
|
||||
changeOptions({ steps: output }, options, win);
|
||||
}
|
||||
}
|
||||
|
||||
async function promptGlobalShortcuts(win: BrowserWindow, options: ConfigType<'precise-volume'>, item: MenuItem) {
|
||||
const output = await prompt({
|
||||
title: 'Global Volume Keybinds',
|
||||
label: 'Choose Global Volume Keybinds:',
|
||||
type: 'keybind',
|
||||
keybindOptions: [
|
||||
kb('Increase Volume', 'volumeUp', options.globalShortcuts?.volumeUp),
|
||||
kb('Decrease Volume', 'volumeDown', options.globalShortcuts?.volumeDown),
|
||||
],
|
||||
...promptOptions(),
|
||||
}, win);
|
||||
|
||||
if (output) {
|
||||
const newGlobalShortcuts: {
|
||||
volumeUp: string;
|
||||
volumeDown: string;
|
||||
} = { volumeUp: '', volumeDown: '' };
|
||||
for (const { value, accelerator } of output) {
|
||||
newGlobalShortcuts[value as keyof typeof newGlobalShortcuts] = accelerator;
|
||||
}
|
||||
|
||||
changeOptions({ globalShortcuts: newGlobalShortcuts }, options, win);
|
||||
|
||||
item.checked = Boolean(options.globalShortcuts.volumeUp) || Boolean(options.globalShortcuts.volumeDown);
|
||||
} else {
|
||||
// Reset checkbox if prompt was canceled
|
||||
item.checked = !item.checked;
|
||||
}
|
||||
}
|
||||
@ -1,41 +0,0 @@
|
||||
/* what */
|
||||
/* eslint-disable @typescript-eslint/ban-ts-comment */
|
||||
|
||||
import is from 'electron-is';
|
||||
|
||||
const ignored = {
|
||||
id: ['volume-slider', 'expand-volume-slider'],
|
||||
types: ['mousewheel', 'keydown', 'keyup'],
|
||||
};
|
||||
|
||||
function overrideAddEventListener() {
|
||||
// YO WHAT ARE YOU DOING NOW?!?!
|
||||
// Save native addEventListener
|
||||
// @ts-ignore
|
||||
// eslint-disable-next-line @typescript-eslint/unbound-method
|
||||
Element.prototype._addEventListener = Element.prototype.addEventListener;
|
||||
// Override addEventListener to Ignore specific events in volume-slider
|
||||
Element.prototype.addEventListener = function (type: string, listener: (event: Event) => void, useCapture = false) {
|
||||
if (!(
|
||||
ignored.id.includes(this.id)
|
||||
&& ignored.types.includes(type)
|
||||
)) {
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any,@typescript-eslint/no-unsafe-call,@typescript-eslint/no-unsafe-member-access
|
||||
(this as any)._addEventListener(type, listener, useCapture);
|
||||
} else if (is.dev()) {
|
||||
console.log(`Ignoring event: "${this.id}.${type}()"`);
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
export default () => {
|
||||
overrideAddEventListener();
|
||||
// Restore original function after finished loading to avoid keeping Element.prototype altered
|
||||
window.addEventListener('load', () => {
|
||||
// eslint-disable-next-line @typescript-eslint/no-unsafe-assignment,@typescript-eslint/no-explicit-any,@typescript-eslint/no-unsafe-member-access
|
||||
Element.prototype.addEventListener = (Element.prototype as any)._addEventListener;
|
||||
// eslint-disable-next-line @typescript-eslint/no-unsafe-assignment,@typescript-eslint/no-explicit-any,@typescript-eslint/no-unsafe-member-access
|
||||
(Element.prototype as any)._addEventListener = undefined;
|
||||
|
||||
}, { once: true });
|
||||
};
|
||||
@ -1,13 +0,0 @@
|
||||
import { ipcMain, dialog } from 'electron';
|
||||
|
||||
export default () => {
|
||||
ipcMain.handle('qualityChanger', async (_, qualityLabels: string[], currentIndex: number) => await dialog.showMessageBox({
|
||||
type: 'question',
|
||||
buttons: qualityLabels,
|
||||
defaultId: currentIndex,
|
||||
title: 'Choose Video Quality',
|
||||
message: 'Choose Video Quality:',
|
||||
detail: `Current Quality: ${qualityLabels[currentIndex]}`,
|
||||
cancelId: -1,
|
||||
}));
|
||||
};
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user