From a5e909cfc8cda71d6c3d4f98c95d38565f0fe035 Mon Sep 17 00:00:00 2001 From: Pun Butrach Date: Sat, 31 May 2025 14:57:21 +0700 Subject: [PATCH 01/19] fix: Obscure Flutter Impeller renderer bugs This is workaround to entirely disabling Flutter Impeller in favour of Skia. Signed-off-by: Pun Butrach --- pubspec.yaml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/pubspec.yaml b/pubspec.yaml index a5be6d2f..ce25f4d6 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -7,8 +7,8 @@ publish_to: 'none' version: 1.25.0-dev.1+101800061 environment: - sdk: '>=3.7.0' - flutter: '>=3.29.0' + sdk: '>=3.8.0' + flutter: '>=3.32.0' dependencies: animations: ^2.0.11 From d264a2a3636fbfe47250b145f0d7c4133b6635e5 Mon Sep 17 00:00:00 2001 From: Pun Butrach Date: Sat, 31 May 2025 15:11:49 +0700 Subject: [PATCH 02/19] build: Support Flutter v3.32 Signed-off-by: Pun Butrach --- pubspec.lock | 220 +++++++++++++++++++++++++-------------------------- pubspec.yaml | 8 +- 2 files changed, 110 insertions(+), 118 deletions(-) diff --git a/pubspec.lock b/pubspec.lock index 2787de8a..bafda137 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -34,18 +34,18 @@ packages: dependency: transitive description: name: args - sha256: bf9f5caeea8d8fe6721a9c358dd8a5c1947b27f1cfaa18b39c301273594919e6 + sha256: d0481093c50b1da8910eb0bb301626d4d8eb7284aa739614d2b394ee09e3ea04 url: "https://pub.dev" source: hosted - version: "2.6.0" + version: "2.7.0" async: dependency: transitive description: name: async - sha256: d2872f9c19731c2e5f10444b14686eb7cc85c76274bd6c16e1816bff9a3bab63 + sha256: "758e6d74e971c3e5aceb4110bfd6698efc7f501675bcfe0c775459a8140750eb" url: "https://pub.dev" source: hosted - version: "2.12.0" + version: "2.13.0" boolean_selector: dependency: transitive description: @@ -74,10 +74,10 @@ packages: dependency: transitive description: name: build_daemon - sha256: "294a2edaf4814a378725bfe6358210196f5ea37af89ecd81bfa32960113d4948" + sha256: "8e928697a82be082206edb0b9c99c5a4ad6bc31c9e9b8b2f291ae65cd4a25daa" url: "https://pub.dev" source: hosted - version: "4.0.3" + version: "4.0.4" build_resolvers: dependency: transitive description: @@ -114,10 +114,10 @@ packages: dependency: transitive description: name: built_value - sha256: "28a712df2576b63c6c005c465989a348604960c0958d28be5303ba9baa841ac2" + sha256: "082001b5c3dc495d4a42f1d5789990505df20d8547d42507c29050af6933ee27" url: "https://pub.dev" source: hosted - version: "8.9.3" + version: "8.10.1" characters: dependency: transitive description: @@ -162,10 +162,10 @@ packages: dependency: "direct main" description: name: connectivity_plus - sha256: "04bf81bb0b77de31557b58d052b24b3eee33f09a6e7a8c68a3e247c7df19ec27" + sha256: "051849e2bd7c7b3bc5844ea0d096609ddc3a859890ec3a9ac4a65a2620cc1f99" url: "https://pub.dev" source: hosted - version: "6.1.3" + version: "6.1.4" connectivity_plus_platform_interface: dependency: transitive description: @@ -210,18 +210,18 @@ packages: dependency: transitive description: name: dart_style - sha256: "7856d364b589d1f08986e140938578ed36ed948581fbc3bc9aef1805039ac5ab" + sha256: "7306ab8a2359a48d22310ad823521d723acfed60ee1f7e37388e8986853b6820" url: "https://pub.dev" source: hosted - version: "2.3.7" + version: "2.3.8" dbus: dependency: transitive description: name: dbus - sha256: "365c771ac3b0e58845f39ec6deebc76e3276aa9922b0cc60840712094d9047ac" + sha256: "79e0c23480ff85dc68de79e2cd6334add97e48f7f4865d17686dd6ea81a47e8c" url: "https://pub.dev" source: hosted - version: "0.7.10" + version: "0.7.11" device_apps: dependency: "direct main" description: @@ -235,10 +235,10 @@ packages: dependency: "direct main" description: name: device_info_plus - sha256: "306b78788d1bb569edb7c55d622953c2414ca12445b41c9117963e03afc5c513" + sha256: "0c6396126421b590089447154c5f98a5de423b70cfb15b1578fd018843ee6f53" url: "https://pub.dev" source: hosted - version: "11.3.3" + version: "11.4.0" device_info_plus_platform_interface: dependency: transitive description: @@ -251,10 +251,10 @@ packages: dependency: "direct main" description: name: dio - sha256: "5598aa796bbf4699afd5c67c0f5f6e2ed542afc956884b9cd58c306966efc260" + sha256: "253a18bbd4851fecba42f7343a1df3a9a4c1d31a2c1b37e221086b4fa8c8dbc9" url: "https://pub.dev" source: hosted - version: "5.7.0" + version: "5.8.0+1" dio_cache_interceptor: dependency: "direct main" description: @@ -267,10 +267,10 @@ packages: dependency: transitive description: name: dio_web_adapter - sha256: "33259a9276d6cea88774a0000cfae0d861003497755969c92faa223108620dc8" + sha256: "7586e476d70caecaf1686d21eee7247ea43ef5c345eab9e0cc3583ff13378d78" url: "https://pub.dev" source: hosted - version: "2.0.0" + version: "2.1.1" dynamic_color: dependency: "direct main" description: @@ -323,10 +323,10 @@ packages: dependency: "direct main" description: name: file_picker - sha256: "7423298f08f6fc8cce05792bae329f9a93653fc9c08712831b1a55540127995d" + sha256: "77f8e81d22d2a07d0dee2c62e1dda71dc1da73bf43bb2d45af09727406167964" url: "https://pub.dev" source: hosted - version: "9.0.2" + version: "10.1.9" fixnum: dependency: transitive description: @@ -368,10 +368,10 @@ packages: dependency: "direct dev" description: name: flutter_lints - sha256: "5398f14efa795ffb7a33e9b6a08798b26a180edac4ad7db3f231e40f82ce11e1" + sha256: "3105dc8492f6183fb076ccf1f351ac3d60564bff92e20bfc4af9cc1651f4e7e1" url: "https://pub.dev" source: hosted - version: "5.0.0" + version: "6.0.0" flutter_local_notifications: dependency: "direct main" description: @@ -405,18 +405,18 @@ packages: dependency: "direct main" description: name: flutter_markdown - sha256: e7bbc718adc9476aa14cfddc1ef048d2e21e4e8f18311aaac723266db9f9e7b5 + sha256: "08fb8315236099ff8e90cb87bb2b935e0a724a3af1623000a9cec930468e0f27" url: "https://pub.dev" source: hosted - version: "0.7.6+2" + version: "0.7.7+1" flutter_plugin_android_lifecycle: dependency: transitive description: name: flutter_plugin_android_lifecycle - sha256: "615a505aef59b151b46bbeef55b36ce2b6ed299d160c51d84281946f0aa0ce0e" + sha256: f948e346c12f8d5480d2825e03de228d0eb8c3a737e4cdaa122267b89c022b5e url: "https://pub.dev" source: hosted - version: "2.0.24" + version: "2.0.28" flutter_test: dependency: transitive description: flutter @@ -463,10 +463,10 @@ packages: dependency: transitive description: name: get - sha256: e4e7335ede17452b391ed3b2ede016545706c01a02292a6c97619705e7d2a85e + sha256: c79eeb4339f1f3deffd9ec912f8a923834bec55f7b49c9e882b8fef2c139d425 url: "https://pub.dev" source: hosted - version: "4.6.6" + version: "4.7.2" get_it: dependency: transitive description: @@ -479,10 +479,10 @@ packages: dependency: transitive description: name: glob - sha256: "0e7014b3b7d4dac1ca4d6114f82bf1782ee86745b9b42a92c9289c23d8a0ab63" + sha256: c3f1ee72c96f8f78935e18aa8cecced9ab132419e8625dc187e1c2408efc20de url: "https://pub.dev" source: hosted - version: "2.1.2" + version: "2.1.3" google_fonts: dependency: "direct main" description: @@ -503,10 +503,10 @@ packages: dependency: transitive description: name: http - sha256: b9c29a161230ee03d3ccf545097fccd9b87a5264228c5d348202e0f0c28f9010 + sha256: "2c11f3f94c687ee9bad77c171151672986360b2b001d109814ee7140b2cf261b" url: "https://pub.dev" source: hosted - version: "1.2.2" + version: "1.4.0" http_multi_server: dependency: transitive description: @@ -519,10 +519,10 @@ packages: dependency: transitive description: name: http_parser - sha256: "76d306a1c3afb33fe82e2bbacad62a61f409b5634c915fceb0d799de1a913360" + sha256: "178d74305e7866013777bab2c3d8726205dc5a4dd935297175b19a23a2e66571" url: "https://pub.dev" source: hosted - version: "4.1.1" + version: "4.1.2" injectable: dependency: "direct main" description: @@ -559,18 +559,10 @@ packages: dependency: transitive description: name: js - sha256: c1b2e9b5ea78c45e1a0788d29606ba27dc5f71f019f32ca5140f61ef071838cf + sha256: "53385261521cc4a0c4658fd0ad07a7d14591cf8fc33abbceae306ddb974888dc" url: "https://pub.dev" source: hosted - version: "0.7.1" - json2yaml: - dependency: transitive - description: - name: json2yaml - sha256: da94630fbc56079426fdd167ae58373286f603371075b69bf46d848d63ba3e51 - url: "https://pub.dev" - source: hosted - version: "3.0.1" + version: "0.7.2" json_annotation: dependency: "direct main" description: @@ -623,10 +615,10 @@ packages: dependency: transitive description: name: lints - sha256: "4a16b3f03741e1252fda5de3ce712666d010ba2122f8e912c94f9f7b90e1a4c3" + sha256: a5e2b223cb7c9c8efdc663ef484fdd95bb243bff242ef5b13e26883547fce9a0 url: "https://pub.dev" source: hosted - version: "5.1.0" + version: "6.0.0" logcat: dependency: "direct main" description: @@ -664,10 +656,10 @@ packages: dependency: transitive description: name: markdown - sha256: ef2a1298144e3f985cc736b22e0ccdaf188b5b3970648f2d9dc13efd1d9df051 + sha256: "935e23e1ff3bc02d390bad4d4be001208ee92cc217cb5b5a6c19bc14aaa318c1" url: "https://pub.dev" source: hosted - version: "7.2.2" + version: "7.3.0" matcher: dependency: transitive description: @@ -720,10 +712,10 @@ packages: dependency: transitive description: name: package_config - sha256: "92d4488434b520a62570293fbd33bb556c7d49230791c1b4bbd973baf6d2dc67" + sha256: f096c55ebb7deb7e384101542bfba8c52696c1b56fca2eb62827989ef2353bbc url: "https://pub.dev" source: hosted - version: "2.1.1" + version: "2.2.0" package_info_plus: dependency: "direct main" description: @@ -760,10 +752,10 @@ packages: dependency: transitive description: name: path_provider_android - sha256: "4adf4fd5423ec60a29506c76581bc05854c55e3a0b72d35bb28d661c9686edf2" + sha256: d0d310befe2c8ab9e7f393288ccbb11b60c019c6b5afc21973eeee4dda2b35e9 url: "https://pub.dev" source: hosted - version: "2.2.15" + version: "2.2.17" path_provider_foundation: dependency: transitive description: @@ -800,26 +792,26 @@ packages: dependency: "direct main" description: name: permission_handler - sha256: "59adad729136f01ea9e35a48f5d1395e25cba6cea552249ddbe9cf950f5d7849" + sha256: "2d070d8684b68efb580a5997eb62f675e8a885ef0be6e754fb9ef489c177470f" url: "https://pub.dev" source: hosted - version: "11.4.0" + version: "12.0.0+1" permission_handler_android: dependency: transitive description: name: permission_handler_android - sha256: d3971dcdd76182a0c198c096b5db2f0884b0d4196723d21a866fc4cdea057ebc + sha256: "1e3bc410ca1bf84662104b100eb126e066cb55791b7451307f9708d4007350e6" url: "https://pub.dev" source: hosted - version: "12.1.0" + version: "13.0.1" permission_handler_apple: dependency: transitive description: name: permission_handler_apple - sha256: f84a188e79a35c687c132a0a0556c254747a08561e99ab933f12f6ca71ef3c98 + sha256: f000131e755c54cf4d84a5d8bd6e4149e262cc31c5a8b1d698de1ac85fa41023 url: "https://pub.dev" source: hosted - version: "9.4.6" + version: "9.4.7" permission_handler_html: dependency: transitive description: @@ -848,10 +840,10 @@ packages: dependency: transitive description: name: petitparser - sha256: c15605cd28af66339f8eb6fbe0e541bfe2d1b72d5825efc6598f3e0a31b9ad27 + sha256: "07c8f0b1913bcde1ff0d26e57ace2f3012ccbf2b204e070290dad3bb22797646" url: "https://pub.dev" source: hosted - version: "6.0.2" + version: "6.1.0" platform: dependency: transitive description: @@ -880,26 +872,26 @@ packages: dependency: transitive description: name: provider - sha256: c8a055ee5ce3fd98d6fc872478b03823ffdb448699c6ebdbbc71d59b596fd48c + sha256: "4abbd070a04e9ddc287673bf5a030c7ca8b685ff70218720abab8b092f53dd84" url: "https://pub.dev" source: hosted - version: "6.1.2" + version: "6.1.5" pub_semver: dependency: transitive description: name: pub_semver - sha256: "7b3cfbf654f3edd0c6298ecd5be782ce997ddf0e00531b9464b55245185bbbbd" + sha256: "5bfcf68ca79ef689f8990d1160781b4bad40a3bd5e5218ad4076ddb7f4081585" url: "https://pub.dev" source: hosted - version: "2.1.5" + version: "2.2.0" pubspec_parse: dependency: transitive description: name: pubspec_parse - sha256: c799b721d79eb6ee6fa56f00c04b472dcd44a30d258fac2174a6ec57302678f8 + sha256: "0560ba233314abbed0a48a2956f7f022cce7c3e1e73df540277da7544cad4082" url: "https://pub.dev" source: hosted - version: "1.3.0" + version: "1.5.0" recase: dependency: transitive description: @@ -954,18 +946,18 @@ packages: dependency: "direct main" description: name: shared_preferences - sha256: "846849e3e9b68f3ef4b60c60cf4b3e02e9321bc7f4d8c4692cf87ffa82fc8a3a" + sha256: "6e8bf70b7fef813df4e9a36f658ac46d107db4b4cfe1048b477d4e453a8159f5" url: "https://pub.dev" source: hosted - version: "2.5.2" + version: "2.5.3" shared_preferences_android: dependency: transitive description: name: shared_preferences_android - sha256: "02a7d8a9ef346c9af715811b01fbd8e27845ad2c41148eefd31321471b41863d" + sha256: "20cbd561f743a342c76c151d6ddb93a9ce6005751e7aa458baad3858bfbfb6ac" url: "https://pub.dev" source: hosted - version: "2.4.0" + version: "2.4.10" shared_preferences_foundation: dependency: transitive description: @@ -994,10 +986,10 @@ packages: dependency: transitive description: name: shared_preferences_web - sha256: d2ca4132d3946fec2184261726b355836a82c33d7d5b67af32692aff18a4684e + sha256: c49bd060261c9a3f0ff445892695d6212ff603ef3115edbb448509d407600019 url: "https://pub.dev" source: hosted - version: "2.4.2" + version: "2.4.3" shared_preferences_windows: dependency: transitive description: @@ -1018,10 +1010,10 @@ packages: dependency: transitive description: name: shelf_web_socket - sha256: cc36c297b52866d203dbf9332263c94becc2fe0ceaa9681d07b6ef9807023b67 + sha256: "3632775c8e90d6c9712f883e633716432a27758216dfb61bd86a8321c0580925" url: "https://pub.dev" source: hosted - version: "2.0.1" + version: "3.0.0" skeletons: dependency: "direct main" description: @@ -1040,18 +1032,18 @@ packages: dependency: "direct main" description: name: slang - sha256: "354283dfe5d6b5bb72d17a52c2acba0ad08c98f8de317a00aa2c801814093982" + sha256: "6668a08355b370d5cb5446fc869c4492ed23c6433934fe88228876460fedac22" url: "https://pub.dev" source: hosted - version: "4.5.0" + version: "4.7.2" slang_flutter: dependency: "direct main" description: name: slang_flutter - sha256: "6891526b13641dd2667ce60a9a65ef2c1611f838105396e5e8ad39edc4ecb191" + sha256: fff13b6fc8b0378ee23856c4f9fd7f8e2777b430090681f4d19ab14c47de9bc6 url: "https://pub.dev" source: hosted - version: "4.5.0" + version: "4.7.0" source_gen: dependency: transitive description: @@ -1088,34 +1080,34 @@ packages: dependency: transitive description: name: sqflite - sha256: "2d7299468485dca85efeeadf5d38986909c5eb0cd71fd3db2c2f000e6c9454bb" + sha256: e2297b1da52f127bc7a3da11439985d9b536f75070f3325e62ada69a5c585d03 url: "https://pub.dev" source: hosted - version: "2.4.1" + version: "2.4.2" sqflite_android: dependency: transitive description: name: sqflite_android - sha256: "78f489aab276260cdd26676d2169446c7ecd3484bbd5fead4ca14f3ed4dd9ee3" + sha256: "2b3070c5fa881839f8b402ee4a39c1b4d561704d4ebbbcfb808a119bc2a1701b" url: "https://pub.dev" source: hosted - version: "2.4.0" + version: "2.4.1" sqflite_common: dependency: transitive description: name: sqflite_common - sha256: "761b9740ecbd4d3e66b8916d784e581861fd3c3553eda85e167bc49fdb68f709" + sha256: "84731e8bfd8303a3389903e01fb2141b6e59b5973cacbb0929021df08dddbe8b" url: "https://pub.dev" source: hosted - version: "2.5.4+6" + version: "2.5.5" sqflite_darwin: dependency: transitive description: name: sqflite_darwin - sha256: "96a698e2bc82bd770a4d6aab00b42396a7c63d9e33513a56945cbccb594c2474" + sha256: "279832e5cde3fe99e8571879498c9211f3ca6391b0d818df4e17d9fff5c6ccb3" url: "https://pub.dev" source: hosted - version: "2.4.1" + version: "2.4.2" sqflite_platform_interface: dependency: transitive description: @@ -1225,10 +1217,10 @@ packages: dependency: "direct main" description: name: timezone - sha256: ffc9d5f4d1193534ef051f9254063fa53d588609418c84299956c3db9383587d + sha256: dd14a3b83cfd7cb19e7888f1cbc20f258b8d71b54c06f79ac585f14093a287d1 url: "https://pub.dev" source: hosted - version: "0.10.0" + version: "0.10.1" timing: dependency: transitive description: @@ -1265,18 +1257,18 @@ packages: dependency: transitive description: name: url_launcher_android - sha256: "6fc2f56536ee873eeb867ad176ae15f304ccccc357848b351f6f0d8d4a40d193" + sha256: "8582d7f6fe14d2652b4c45c9b6c14c0b678c2af2d083a11b604caeba51930d79" url: "https://pub.dev" source: hosted - version: "6.3.14" + version: "6.3.16" url_launcher_ios: dependency: transitive description: name: url_launcher_ios - sha256: "16a513b6c12bb419304e72ea0ae2ab4fed569920d1c7cb850263fe3acc824626" + sha256: "7f2022359d4c099eea7df3fdf739f7d3d3b9faf3166fb1dd390775176e0b76cb" url: "https://pub.dev" source: hosted - version: "6.3.2" + version: "6.3.3" url_launcher_linux: dependency: transitive description: @@ -1305,18 +1297,18 @@ packages: dependency: transitive description: name: url_launcher_web - sha256: "772638d3b34c779ede05ba3d38af34657a05ac55b06279ea6edd409e323dca8e" + sha256: "4bd2b7b4dc4d4d0b94e5babfffbca8eac1a126c7f3d6ecbc1a11013faa3abba2" url: "https://pub.dev" source: hosted - version: "2.3.3" + version: "2.4.1" url_launcher_windows: dependency: transitive description: name: url_launcher_windows - sha256: "44cf3aabcedde30f2dba119a9dea3b0f2672fbe6fa96e85536251d678216b3c4" + sha256: "3284b6d2ac454cf34f114e1d3319866fdd1e19cdc329999057e44ffe936cfa77" url: "https://pub.dev" source: hosted - version: "3.1.3" + version: "3.1.4" uuid: dependency: transitive description: @@ -1345,18 +1337,18 @@ packages: dependency: "direct main" description: name: wakelock_plus - sha256: "36c88af0b930121941345306d259ec4cc4ecca3b151c02e3a9e71aede83c615e" + sha256: a474e314c3e8fb5adef1f9ae2d247e57467ad557fa7483a2b895bc1b421c5678 url: "https://pub.dev" source: hosted - version: "1.2.10" + version: "1.3.2" wakelock_plus_platform_interface: dependency: transitive description: name: wakelock_plus_platform_interface - sha256: "70e780bc99796e1db82fe764b1e7dcb89a86f1e5b3afb1db354de50f2e41eb7a" + sha256: e10444072e50dbc4999d7316fd303f7ea53d31c824aa5eb05d7ccbdd98985207 url: "https://pub.dev" source: hosted - version: "1.2.2" + version: "1.2.3" watcher: dependency: transitive description: @@ -1369,34 +1361,34 @@ packages: dependency: transitive description: name: web - sha256: cd3543bd5798f6ad290ea73d210f423502e71900302dde696f8bff84bf89a1cb + sha256: "868d88a33d8a87b18ffc05f9f030ba328ffefba92d6c127917a2ba740f9cfe4a" url: "https://pub.dev" source: hosted - version: "1.1.0" + version: "1.1.1" web_socket: dependency: transitive description: name: web_socket - sha256: "3c12d96c0c9a4eec095246debcea7b86c0324f22df69893d538fcc6f1b8cce83" + sha256: "34d64019aa8e36bf9842ac014bb5d2f5586ca73df5e4d9bf5c936975cae6982c" url: "https://pub.dev" source: hosted - version: "0.1.6" + version: "1.0.1" web_socket_channel: dependency: transitive description: name: web_socket_channel - sha256: "9f187088ed104edd8662ca07af4b124465893caf063ba29758f97af57e61da8f" + sha256: d645757fb0f4773d602444000a8131ff5d48c9e47adfe9772652dd1a4f2d45c8 url: "https://pub.dev" source: hosted - version: "3.0.1" + version: "3.0.3" win32: dependency: transitive description: name: win32 - sha256: b89e6e24d1454e149ab20fbb225af58660f0c0bf4475544650700d8e2da54aef + sha256: "329edf97fdd893e0f1e3b9e88d6a0e627128cc17cc316a8d67fda8f1451178ba" url: "https://pub.dev" source: hosted - version: "5.11.0" + version: "5.13.0" win32_registry: dependency: transitive description: @@ -1425,10 +1417,10 @@ packages: dependency: transitive description: name: yaml - sha256: "75769501ea3489fca56601ff33454fe45507ea3bfb014161abc3b43ae25989d5" + sha256: b9da305ac7c39faa3f030eccd175340f968459dae4af175130b3fc47e40d76ce url: "https://pub.dev" source: hosted - version: "3.1.2" + version: "3.1.3" sdks: - dart: ">=3.7.0 <4.0.0" - flutter: ">=3.29.0" + dart: ">=3.8.0 <4.0.0" + flutter: ">=3.32.0" diff --git a/pubspec.yaml b/pubspec.yaml index ce25f4d6..bf2c8a7d 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -24,7 +24,7 @@ dependencies: dynamic_color: ^1.7.0 dynamic_themes: ^1.1.0 expandable: ^5.0.1 - file_picker: ^9.0.2 + file_picker: ^10.1.9 flutter: sdk: flutter flutter_background: ^1.3.0+1 @@ -38,7 +38,7 @@ dependencies: font_awesome_flutter: ^10.8.0 google_fonts: ^6.2.1 injectable: ^2.4.0 - intl: ^0.19.0 + intl: ^0.20.2 json_annotation: ^4.9.0 language_code: ^0.5.5 logcat: @@ -47,7 +47,7 @@ dependencies: ref: 4a6d5e0e22292c8eb160cfb9365b9ea29735fd43 # Branch: master package_info_plus: ^8.3.0 path_provider: ^2.1.5 - permission_handler: ^11.4.0 + permission_handler: ^12.0.0 root: git: url: https://github.com/validcube/root @@ -80,7 +80,7 @@ dependencies: dev_dependencies: analyzer: ^6.3.0 build_runner: ^2.4.15 - flutter_lints: ^5.0.0 + flutter_lints: ^6.0.0 injectable_generator: ^2.6.1 json_serializable: ^6.9.0 From 1752fae9d911a0f1c2bc6d9858cb85db244d3699 Mon Sep 17 00:00:00 2001 From: Pun Butrach Date: Sat, 31 May 2025 15:12:26 +0700 Subject: [PATCH 03/19] chore: Better ignore rules Based on https://github.com/github/gitignore/blob/main/Flutter.gitignore Signed-off-by: Pun Butrach --- .gitignore | 45 +++++++++++++++++++++++++++------------------ 1 file changed, 27 insertions(+), 18 deletions(-) diff --git a/.gitignore b/.gitignore index 64ed78de..387727a0 100644 --- a/.gitignore +++ b/.gitignore @@ -1,14 +1,14 @@ # Miscellaneous *.class +*.lock *.log *.pyc *.swp -.DS_Store -.atom/ .buildlog/ .history -.svn/ -migrate_working_dir/ + +# packages file containing multi-root paths +.packages.generated # IntelliJ related *.iml @@ -16,32 +16,41 @@ migrate_working_dir/ *.iws .idea/ -# The .vscode folder contains launch configuration and tasks you configure in -# VS Code which you may wish to be included in version control, so this line -# is commented out by default. -#.vscode/ - # Flutter/Dart/Pub related **/doc/api/ -**/ios/Flutter/.last_build_id .dart_tool/ .flutter-plugins .flutter-plugins-dependencies -.pub-cache/ +**/generated_plugin_registrant.dart +.packages +.pub-preload-cache/ .pub/ -/build/ +build/ +flutter_*.png +linked_*.ds +unlinked.ds +unlinked_spec.ds -# Symbolication related +# Android related +**/android/**/gradle-wrapper.jar +.gradle/ +**/android/captures/ +**/android/gradlew +**/android/gradlew.bat +**/android/local.properties +**/android/**/GeneratedPluginRegistrant.java +**/android/key.properties +*.jks + +# Coverage +coverage/ + +# Symbols app.*.symbols # Obfuscation related app.*.map.json -# Android Studio will place build artifacts here -/android/app/debug -/android/app/profile -/android/app/release - # Generated files android/app/.cxx **/*.g.dart From 40c99ab4dc36378262ac7444e4a53edffa8d99c3 Mon Sep 17 00:00:00 2001 From: Pun Butrach Date: Sat, 31 May 2025 15:18:37 +0700 Subject: [PATCH 04/19] ci: Set missing translation actor/email for commit Signed-off-by: Pun Butrach --- .github/workflows/sync_crowdin.yml | 2 ++ 1 file changed, 2 insertions(+) diff --git a/.github/workflows/sync_crowdin.yml b/.github/workflows/sync_crowdin.yml index d74f03a1..cf27665e 100644 --- a/.github/workflows/sync_crowdin.yml +++ b/.github/workflows/sync_crowdin.yml @@ -73,6 +73,8 @@ jobs: - name: Commit translations run: | + git config --global user.name 'github-actions[bot]' + git config --global user.email 'github-actions[bot]@users.noreply.github.com' sudo chown -R $USER:$USER .git git commit -m "chore: Remove empty values from JSON" assets/i18n/*.i18n.json git push origin HEAD:feat/translations From 7ee2b1a0260e0e127fd4ad483e48cb3b486613cf Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" <41898282+github-actions[bot]@users.noreply.github.com> Date: Sat, 31 May 2025 15:36:42 +0700 Subject: [PATCH 05/19] chore: Sync translations (#2522) Signed-off-by: Pun Butrach Co-authored-by: Crowdin Bot Co-authored-by: github-actions[bot] Co-authored-by: Pun Butrach --- assets/i18n/ar_SA.i18n.json | 3 + assets/i18n/as_IN.i18n.json | 53 +++++++++++++++- assets/i18n/az_AZ.i18n.json | 3 + assets/i18n/bn_BD.i18n.json | 1 + assets/i18n/el_GR.i18n.json | 3 + assets/i18n/es_ES.i18n.json | 2 + assets/i18n/et_EE.i18n.json | 1 + assets/i18n/fi_FI.i18n.json | 3 + assets/i18n/fil_PH.i18n.json | 1 + assets/i18n/fr_FR.i18n.json | 5 +- assets/i18n/ga_IE.i18n.json | 3 + assets/i18n/id_ID.i18n.json | 3 +- assets/i18n/ja_JP.i18n.json | 91 ++++++++++++++-------------- assets/i18n/ko_KR.i18n.json | 113 ++++++++++++++++++----------------- assets/i18n/lv_LV.i18n.json | 1 + assets/i18n/ms_MY.i18n.json | 23 +++++++ assets/i18n/pl_PL.i18n.json | 14 ++--- assets/i18n/pt_BR.i18n.json | 1 + assets/i18n/ru_RU.i18n.json | 3 + assets/i18n/sl_SI.i18n.json | 2 +- assets/i18n/sr_CS.i18n.json | 3 + assets/i18n/sr_SP.i18n.json | 3 + assets/i18n/th_TH.i18n.json | 1 + assets/i18n/tr_TR.i18n.json | 3 + assets/i18n/uk_UA.i18n.json | 3 + assets/i18n/vi_VN.i18n.json | 42 ++++++------- assets/i18n/zh_TW.i18n.json | 4 +- 27 files changed, 253 insertions(+), 135 deletions(-) diff --git a/assets/i18n/ar_SA.i18n.json b/assets/i18n/ar_SA.i18n.json index 15a444a8..1c5f71bc 100755 --- a/assets/i18n/ar_SA.i18n.json +++ b/assets/i18n/ar_SA.i18n.json @@ -180,6 +180,9 @@ "disablePatchesSelectionWarningText": "أنت على وشك تعطيل تغيير تحديد التعديلات.\nستتم استعادة التحديد الافتراضي للتعديلات.\n\nهل تريد التعطيل على أي حال؟", "autoUpdatePatchesLabel": "تحديث التعديلات تلقائيًا", "autoUpdatePatchesHint": "تحديث التعديلات تلقائيًا إلى الإصدار الأحدث", + "usePrereleasesLabel": "استخدام الإصدارات الأولية", + "usePrereleasesHint": "استخدام إصدارات ما قبل الإصدار من ReVanced Manager و ReVanced Patches", + "usePrereleasesWarningText": "قد يتسبب استخدام الإصدارات الأولية في حدوث مشكلات غير متوقعة.\n\nهل تريد التمكين على أي حال؟", "showUpdateDialogLabel": "عرض مربع حوار التحديث", "showUpdateDialogHint": "عرض مربع حوار عندما يتوفر تحديث جديد", "universalPatchesLabel": "عرض التعديلات العامة", diff --git a/assets/i18n/as_IN.i18n.json b/assets/i18n/as_IN.i18n.json index 03aee6bb..fd0fe2f6 100755 --- a/assets/i18n/as_IN.i18n.json +++ b/assets/i18n/as_IN.i18n.json @@ -34,7 +34,25 @@ "updatePatchesSheetTitle": "ReVanced পেট্‌চবোৰ আপডে’ট কৰক", "updateChangelogTitle": "সলনি-পঞ্জী", "updateDialogText": "${file}-ৰ এটা নতুন আপডে’ট উপলব্ধ।\n\nবৰ্তমানে ইনষ্টল কৰা সংস্কৰণটো হৈছে ${version}।", - "downloadConsentDialogTitle": "প্ৰয়োজনীয় ফাইলবোৰ ডাউনল’ড কৰিবনে?" + "downloadConsentDialogTitle": "প্ৰয়োজনীয় ফাইলবোৰ ডাউনল’ড কৰিবনে?", + "downloadConsentDialogText": "সঠিকভাৱে কাম কৰিবলৈ ReVanced Manager-এ প্ৰয়োজনীয় ফাইলবোৰ ডাউনল'ড কৰিব লাগিব।", + "downloadConsentDialogText2": "ই আপোনাক ${url}-ৰ সৈতে সংযুক্ত কৰিব।", + "downloadingMessage": "আপডে’ট ডাউনল’ড হৈ আছে...", + "downloadedMessage": "আপডে’ট ডাউনল’ড কৰা হ’ল", + "installingMessage": "আপডে’ট ইনষ্টল হৈ আছে...", + "errorDownloadMessage": "আপডে’ট ডাউনল’ড কৰিব পৰা নগ’ল" + }, + "applicationItem": { + "infoButton": "তথ্য" + }, + "latestCommitCard": { + "timeagoLabel": "${time} আগত" + }, + "appSelectorCard": { + "widgetTitle": "এটা এপ বাছনি কৰক", + "widgetTitleSelected": "বাছনি কৰা এপ", + "widgetSubtitle": "কোনো এপ বাছনি কৰা নাই", + "noAppsLabel": "কোনো এপ্লিকেশ্যন পোৱা নগ’ল" }, "patchOptionsView": { "saveOptions": "ছে’ভ কৰক", @@ -50,9 +68,28 @@ "darkThemeLabel": "গাঢ়", "dynamicThemeLabel": "মেটেৰিয়েল ইউ", "languageLabel": "ভাষা", - "languageUpdated": "ভাষা আপডে’ট কৰা হ’ল" + "languageUpdated": "ভাষা আপডে’ট কৰা হ’ল", + "importKeystoreLabel": "কীষ্ট’ৰ ইম্প’ৰ্ট কৰক", + "importKeystoreHint": "এপবোৰত চহী কৰিবলৈ ব্যৱহাৰ কৰা কীষ্ট’ৰ এটা ইম্প’ৰ্ট কৰক", + "importedKeystore": "কীষ্ট’ৰ ইম্প’ৰ্ট কৰা হ’ল", + "selectKeystorePassword": "কীষ্ট’ৰৰ পাছৱৰ্ড", + "selectKeystorePasswordHint": "এপবোৰত চহী কৰিবলৈ ব্যৱহাৰ কৰা কীষ্ট’ৰৰ পাছৱৰ্ড বাছনি কৰক", + "jsonSelectorErrorMessage": "বাছনি কৰা JSON ফাইলটো ব্যৱহাৰ কৰিব পৰা নগ’ল", + "keystoreSelectorErrorMessage": "বাছনি কৰা কীষ্ট’ৰ ফাইলটো ব্যৱহাৰ কৰিব পৰা নগ’ল" }, "appInfoView": { + "widgetTitle": "এপৰ তথ্য", + "openButton": "খোলক", + "installButton": "ইনষ্টল কৰক", + "uninstallButton": "আনইনষ্টল কৰক", + "unmountButton": "আনমাউণ্ট কৰক", + "exportButton": "এক্সপ’ৰ্ট কৰক", + "deleteButton": "মচক", + "rootDialogTitle": "ত্ৰুটি", + "lastPatchedAppDescription": "এইটো শেষবাৰৰ বাবে পেট্‌চ কৰা এপটোৰ বেকআপ।", + "unmountDialogText": "আপুনি এই এপটো আনমাউণ্ট কৰিব বিচৰাটো নিশ্চিতনে?", + "uninstallDialogText": "আপুনি এই এপটো আনইনষ্টল কৰিব বিচৰাটো নিশ্চিতনে?", + "rootDialogText": "এপটো ছুপাৰইউজাৰৰ অনুমতিৰে ইনষ্টল কৰা হৈছে, কিন্তু বৰ্তমান ReVanced Manager-ৰ কোনো অনুমতি নাই।\nঅনুগ্ৰহ কৰি প্ৰথমে ছুপাৰইউজাৰৰ অনুমতি দিয়ক।", "removeAppDialogTitle": "এপ মচিবনে?", "removeAppDialogText": "আপুনি এই বেকআপ মচি পেলাব বিচৰাটো নিশ্চিতনে?", "packageNameLabel": "পেকেজৰ নাম", @@ -75,14 +112,24 @@ "mount_missing_installation": "ইনষ্টলেশ্যন পোৱা নগ’ল", "status_failure_blocked": "ইনষ্টলেশ্যন অৱৰুদ্ধ", "install_failed_verification_failure": "ভেৰিফিকেশ্যন বিফল", + "status_failure_invalid": "ইনষ্টলেশ্যন অবৈধ", "install_failed_version_downgrade": "ডাউনগ্ৰে’ড কৰিব নোৱাৰি", "status_failure_conflict": "ইনষ্টলেশ্যনৰ সংঘাত", "status_failure_storage": "ইনষ্টলেশ্যন ষ্ট’ৰেজৰ সমস্যা", "status_failure_incompatible": "ইনষ্টলেশ্যন নিমিলে", "status_failure_timeout": "ইনষ্টলেশ্যনৰ সময় উকলিল", "status_unknown": "ইনষ্টলেশ্যন বিফল", + "mount_version_mismatch_description": "ইনষ্টল কৰা এপটো পেট্‌চ কৰা এপতকৈ বেলেগ সংস্কৰণ হোৱাৰ বাবে ইনষ্টলেশ্যন বিফল হ’ল।\n\nআপুনি মাউণ্ট কৰা এপটোৰ সংস্কৰণ ইনষ্টল কৰি আকৌ চেষ্টা কৰক।", + "mount_no_root_description": "ৰুট এক্সেছ নিদিয়াৰ বাবে ইনষ্টলেশ্যন বিফল হ’ল।\n\nReVanced Manager-ক ৰুট এক্সেছ দি আকৌ চেষ্টা কৰক।", + "mount_missing_installation_description": "ওপৰত মাউণ্ট কৰিব পৰাকৈ পেট্‌চ নকৰা এপটো এই ডিভাইচত ইনষ্টল হৈ নথকাৰ বাবে ইনষ্টলেশ্যন বিফল হ’ল।\n\nমাউণ্ট কৰাৰ আগত পেট্‌চ নকৰা এপটো ইনষ্টল কৰি আকৌ চেষ্টা কৰক।", + "status_failure_timeout_description": "ইনষ্টলেশ্যন শেষ হ’বলৈ বহুত সময় লাগিল।\n\nআপুনি আকৌ চেষ্টা কৰিব বিচাৰেনে?", + "status_failure_storage_description": "অপৰ্যাপ্ত ষ্ট’ৰে’জৰ বাবে ইনষ্টলেশ্যন বিফল হ’ল।\n\nঅলপ ঠাই খালী কৰি আকৌ চেষ্টা কৰক।", + "status_failure_invalid_description": "পেট্‌চ কৰা এপটো অবৈধ হোৱাৰ বাবে ইনষ্টলেশ্যন বিফল হ’ল।\n\nএপটো আনইনষ্টল কৰি আকৌ চেষ্টা কৰিবনে?", + "status_failure_incompatible_description": "এই ডিভাইচৰ সৈতে এপটো সংগতিপূৰ্ণ নহয়।\n\nএই ডিভাইচটোৱে ছাপ’ৰ্ট কৰা এটা APK ব্যৱহাৰ কৰি আকৌ চেষ্টা কৰক।", "status_failure_conflict_description": "এপটোৰ এটা মজুত থকা ইনষ্টলেশ্যনৰ ফলত ইনষ্টলেশ্যনত বাধা আহিছিল।\n\n ইনষ্টল কৰা এপটো আনইনষ্টল কৰি পুনৰ চেষ্টা কৰিবনে?", "status_failure_blocked_description": "ইনষ্টলেশ্যনটো ${packageName}-ৰ দ্বাৰা অৱৰোধ কৰা হৈছে।\n\nআপোনাৰ সুৰক্ষা-সম্বন্ধীয় ছেটিংছ মিলাই লৈ পুনৰ চেষ্টা কৰক।", - "install_failed_verification_failure_description": "ভেৰিফিকেশ্যন-সম্বন্ধীয় সমস্যা এটাৰ কাৰণে ইনষ্টলেশ্যন বিফল হ’ল।\n\nআপোনাৰ সুৰক্ষা-সম্বন্ধীয় ছেটিংছ মিলাই লৈ পুনৰ চেষ্টা কৰক।" + "install_failed_verification_failure_description": "ভেৰিফিকেশ্যন-সম্বন্ধীয় সমস্যা এটাৰ কাৰণে ইনষ্টলেশ্যন বিফল হ’ল।\n\nআপোনাৰ সুৰক্ষা-সম্বন্ধীয় ছেটিংছ মিলাই লৈ পুনৰ চেষ্টা কৰক।", + "install_failed_version_downgrade_description": "পেট্‌চ কৰা এপটো ইনষ্টল কৰা এপতকৈ আগৰ সংস্কৰণ হোৱাৰ বাবে ইনষ্টলেশ্যন বিফল হ’ল।\n\nএপটো আনইনষ্টল কৰি আকৌ চেষ্টা কৰিবনে?", + "status_unknown_description": "অজ্ঞাত কাৰণত ইনষ্টলেশ্যন বিফল হ’ল। অনুগ্ৰহ কৰি আকৌ চেষ্টা কৰক।" } } \ No newline at end of file diff --git a/assets/i18n/az_AZ.i18n.json b/assets/i18n/az_AZ.i18n.json index 67b7a222..7a9709e2 100755 --- a/assets/i18n/az_AZ.i18n.json +++ b/assets/i18n/az_AZ.i18n.json @@ -180,6 +180,9 @@ "disablePatchesSelectionWarningText": "Yamaq seçimi dəyişdirilməsini qapatmaq üzrəsiniz.\nİlkin yamaq seçimi bərpa ediləcək.\n\nYenə də qapadılsın?", "autoUpdatePatchesLabel": "Yamaqları avtomatik yenilə", "autoUpdatePatchesHint": "Yamaqları son versiyaya avtomatik yenilə", + "usePrereleasesLabel": "İlkin buraxılışları istifadə et", + "usePrereleasesHint": "ReVanced Manager və ReVanced Patches-in buraxılışdan əvvəlki versiyaların istifadə et", + "usePrereleasesWarningText": "İlkin buraxılış versiyaların istifadə gözlənilməz problemlərə səbəb ola bilər.\n\nYenə də aktiv edilsin?", "showUpdateDialogLabel": "Yenilənmə dialoqunu göstər", "showUpdateDialogHint": "Təzə yenilənmə mövcud olduqda dialoq pəncərəsi göstər", "universalPatchesLabel": "Ümumi yamaqları göstər", diff --git a/assets/i18n/bn_BD.i18n.json b/assets/i18n/bn_BD.i18n.json index 6c3fd43e..302b15ee 100755 --- a/assets/i18n/bn_BD.i18n.json +++ b/assets/i18n/bn_BD.i18n.json @@ -180,6 +180,7 @@ "disablePatchesSelectionWarningText": "আপনি নির্বাচিত প্যাচ পরিবর্তনকে নিষ্ক্রিয় করতে যাচ্ছেন।\nপূর্বনির্ধারিত নির্বাচিত প্যাচসমূহ ফিরিয়ে আনা হবে।\n\nযেকোন ভাবে নিষ্ক্রিয় করতে চান?", "autoUpdatePatchesLabel": "প্যাচসমূহ স্বয়ংক্রিয়ভাবে আপডেট করুন", "autoUpdatePatchesHint": "প্যাচসমূহ স্বয়ংক্রিয়ভাবে সর্বশেষ সংস্করণে আপডেট হবে", + "usePrereleasesLabel": "প্রি-রিলিজ ব্যবহার করুন", "showUpdateDialogLabel": "হালনাগাদ ডায়ালগ দেখান", "showUpdateDialogHint": "যখন হালনাগাদ উপলব্ধ থাকবে তখন একটি ডায়ালগ দেখান", "universalPatchesLabel": "বৈশ্বিক প্যাচসমূহ দেখান", diff --git a/assets/i18n/el_GR.i18n.json b/assets/i18n/el_GR.i18n.json index a40e14f5..9e0a36f1 100755 --- a/assets/i18n/el_GR.i18n.json +++ b/assets/i18n/el_GR.i18n.json @@ -180,6 +180,9 @@ "disablePatchesSelectionWarningText": "Πρόκειται να απενεργοποιήσετε τη δυνατότητα αλλαγής των επιλογών τροποποιήσεων.\nΟι προεπιλεγμένες επιλογές τροποποιήσεων θα επαναφερθούν.\n\nΑπενεργοποίηση παρόλα αυτά;", "autoUpdatePatchesLabel": "Αυτόματες ενημερώσεις τροποποιήσεων", "autoUpdatePatchesHint": "Αυτόματη ενημέρωση τροποποιήσεων στην τελευταία έκδοση", + "usePrereleasesLabel": "Χρήση πρώιμων εκδόσεων", + "usePrereleasesHint": "Χρήση των πρώιμων εκδόσεων του ReVanced Manager και των τροποποιήσεων ReVanced", + "usePrereleasesWarningText": "Η χρήση των πρώιμων εκδόσεων ενδέχεται να προκαλέσει μη αναμενόμενα προβλήματα.\n\nΕνεργοποίηση ούτως ή άλλως;", "showUpdateDialogLabel": "Εμφάνιση προτροπής για ενημερώσεις", "showUpdateDialogHint": "Εμφάνιση ειδοποίησης όταν είναι διαθέσιμη κάποια νέα ενημέρωση", "universalPatchesLabel": "Εμφάνιση γενικευμένων τροποποιήσεων", diff --git a/assets/i18n/es_ES.i18n.json b/assets/i18n/es_ES.i18n.json index 31bd449d..ba617f51 100755 --- a/assets/i18n/es_ES.i18n.json +++ b/assets/i18n/es_ES.i18n.json @@ -180,6 +180,8 @@ "disablePatchesSelectionWarningText": "Estás a punto de desactivar cambiar la selección de parches.\nLa selección predeterminada de parches se restaurará.\n\n¿Deshabilitar de todos modos?", "autoUpdatePatchesLabel": "Actualizar automáticamente los parches", "autoUpdatePatchesHint": "Actualizar automáticamente los parches a la última versión", + "usePrereleasesLabel": "Usar versiones preliminares", + "usePrereleasesHint": "Usar versiones previas de ReVanced Manager y ReVanced Parches", "showUpdateDialogLabel": "Mostrar diálogo de actualización", "showUpdateDialogHint": "Mostrar un diálogo cuando una nueva actualización esté disponible", "universalPatchesLabel": "Mostrar parches universales", diff --git a/assets/i18n/et_EE.i18n.json b/assets/i18n/et_EE.i18n.json index a4ee1dd7..76a6e628 100755 --- a/assets/i18n/et_EE.i18n.json +++ b/assets/i18n/et_EE.i18n.json @@ -180,6 +180,7 @@ "disablePatchesSelectionWarningText": "Te olete peaaegu keelamas paranduste valiku muutmise.\nParanduste vaikimisi valik taastatakse.\n\nKas keelate ikkagi?", "autoUpdatePatchesLabel": "Automaatne paranduste uuendamine", "autoUpdatePatchesHint": "Uuenda parandused automaatselt uusimasse versiooni", + "usePrereleasesLabel": "Kasuta eelväljalaskeid", "showUpdateDialogLabel": "Näita uuenduse dialoogi", "showUpdateDialogHint": "Näita dialoogi, kui on saadaval uus uuendus", "universalPatchesLabel": "Näita universaalseid parandusi", diff --git a/assets/i18n/fi_FI.i18n.json b/assets/i18n/fi_FI.i18n.json index ca9b2d54..efed2ffd 100755 --- a/assets/i18n/fi_FI.i18n.json +++ b/assets/i18n/fi_FI.i18n.json @@ -180,6 +180,9 @@ "disablePatchesSelectionWarningText": "Olet poistamassa paikkausvalikoiman muokkauksen käytöstä.\nOletusarvoiset paikkasvalikoimat palautetaan.\n\nEstetäänkö se silti?", "autoUpdatePatchesLabel": "Päivitä paikkaukset automaattisesti", "autoUpdatePatchesHint": "Päivitä paikkaukset automaattisesti uusimpiin versioihin", + "usePrereleasesLabel": "Käytä ennakkojulkaisuja", + "usePrereleasesHint": "Käytä ReVanced Managerin ja ReVanced Patchesien ennakkojulkaisuversioita", + "usePrereleasesWarningText": "Ennakkojulkaisuversioiden käyttäminen voi aiheuttaa odottamattomia ongelmia.\n\nOtetaanko se silti käyttöön?", "showUpdateDialogLabel": "Näytä päivitysruutu", "showUpdateDialogHint": "Näytä ilmoitus, kun uusi päivitys on saatavilla", "universalPatchesLabel": "Näytä yleispaikkaukset", diff --git a/assets/i18n/fil_PH.i18n.json b/assets/i18n/fil_PH.i18n.json index bc3b6139..a69b35bd 100755 --- a/assets/i18n/fil_PH.i18n.json +++ b/assets/i18n/fil_PH.i18n.json @@ -180,6 +180,7 @@ "disablePatchesSelectionWarningText": "Ikaw ay malapit nang magdisable sa pagpalit ng mga pinili na patches.\nDahil dyan, ang karaniwang pagpili ng mga patch ay maibalik.\n\nI-disable pa rin?", "autoUpdatePatchesLabel": "Auto update patches", "autoUpdatePatchesHint": "Awtomatikong i-update ang mga patch sa pinakabagong bersyon", + "usePrereleasesLabel": "Gumamit ng mga pre-release", "showUpdateDialogLabel": "Ipakita ang dialog ng update", "showUpdateDialogHint": "Ipakita ang isang dialog kapag magagamit ang isang bagong update", "universalPatchesLabel": "Ipakita ang mga pangkalahatang patch", diff --git a/assets/i18n/fr_FR.i18n.json b/assets/i18n/fr_FR.i18n.json index 75a6a0c2..10d6c018 100755 --- a/assets/i18n/fr_FR.i18n.json +++ b/assets/i18n/fr_FR.i18n.json @@ -137,7 +137,7 @@ "notificationText": "Appuyez pour revenir à l'installation", "exportApkButtonTooltip": "Exporter l'APK patché", "exportLogButtonTooltip": "Exporter le journal", - "screenshotDetected": "Une capture d’écran a été détectée. Si vous essayez de partager le log, veuillez plutôt partager une copie du texte.\n\nCopier les logs dans le presse-papiers ?", + "screenshotDetected": "Une capture d’écran a été détectée. Si vous essayez de partager le journal, veuillez plutôt partager une copie textuelle.\n\nCopier le journal dans le presse-papiers ?", "copiedToClipboard": "Journal copié dans le presse-papiers", "noExit": "L'installateur s'exécute encore, impossible de quitter..." }, @@ -180,6 +180,9 @@ "disablePatchesSelectionWarningText": "Vous êtes sur le point de désactiver la modification de la sélection de patchs.\nLa sélection de patchs par défaut sera rétablie.\n\nDésactiver quand même ?", "autoUpdatePatchesLabel": "Mise à jour auto. des patchs", "autoUpdatePatchesHint": "Mettre à jour automatiquement les patchs vers la dernière version", + "usePrereleasesLabel": "Utiliser les préversions", + "usePrereleasesHint": "Utilisez les versions préliminaires de ReVanced Manager et ReVanced Patches", + "usePrereleasesWarningText": "L'utilisation de versions préliminaires peut entraîner des problèmes inattendus.\n\nActiver quand même ?", "showUpdateDialogLabel": "Afficher la boîte de dialogue de mise à jour", "showUpdateDialogHint": "Afficher une boîte de dialogue lorsqu'une nouvelle mise à jour est disponible", "universalPatchesLabel": "Afficher les patchs universels", diff --git a/assets/i18n/ga_IE.i18n.json b/assets/i18n/ga_IE.i18n.json index a4be13f2..2a3840b7 100755 --- a/assets/i18n/ga_IE.i18n.json +++ b/assets/i18n/ga_IE.i18n.json @@ -180,6 +180,9 @@ "disablePatchesSelectionWarningText": "Tá tú ar tí roghnú paistí a athrú a dhíchumasú.\nCuirfear an rogha réamhshocraithe paistí ar ais.\n\nDíchumasú ar aon chaoi?", "autoUpdatePatchesLabel": "Paistí nuashonruithe uathoibríoch", "autoUpdatePatchesHint": "Nuashonraigh go huathoibríoch paistí go dtí an leagan is déanaí", + "usePrereleasesLabel": "Úsáid réamheisiúintí", + "usePrereleasesHint": "Úsáid leaganacha réamheisiúna de ReVanced Manager agus ReVanced Patches", + "usePrereleasesWarningText": "D’fhéadfadh fadhbanna gan choinne a bheith mar thoradh ar úsáid a bhaint as leaganacha réamhscaoilte.\n\nAr mhaith leat é a chumasú ar aon nós?", "showUpdateDialogLabel": "Taispeáin dialóg nuashonrú", "showUpdateDialogHint": "Taispeáin dialóg nuair a bhíonn nuashonrú nua ar fáil", "universalPatchesLabel": "Taispeáin paistí uilíocha", diff --git a/assets/i18n/id_ID.i18n.json b/assets/i18n/id_ID.i18n.json index 06c14405..e3ab2081 100755 --- a/assets/i18n/id_ID.i18n.json +++ b/assets/i18n/id_ID.i18n.json @@ -26,7 +26,7 @@ "lastPatchedAppSubtitle": "Aplikasi terakhir ditambal", "patchedSubtitle": "Aplikasi terpasang", "changeLaterSubtitle": "Anda dapat mengubahnya di pengaturan nanti.", - "noSavedAppFound": "App tidak ditemukan", + "noSavedAppFound": "Aplikasi tidak ditemukan", "noInstallations": "Aplikasi tertambal tidak terpasang", "installUpdate": "Lanjut mengunduh pembaruan?", "updateSheetTitle": "Perbarui ReVanced Manager", @@ -180,6 +180,7 @@ "disablePatchesSelectionWarningText": "Anda akan menonaktifkan pengubahan pilihan tambalan.\nPilihan tambalan bawaan akan dikembalikan.\n\nTetap nonaktifkan?", "autoUpdatePatchesLabel": "Otomatis perbarui tambalan", "autoUpdatePatchesHint": "Otomatis perbarui tambalan ke versi terbaru", + "usePrereleasesLabel": "Gunakan rilis pra-rilis", "showUpdateDialogLabel": "Tampilkan dialog pembaruan", "showUpdateDialogHint": "Tampilkan dialog ketika pembaruan tersedia", "universalPatchesLabel": "Tampilkan tambalan universal", diff --git a/assets/i18n/ja_JP.i18n.json b/assets/i18n/ja_JP.i18n.json index 32cc6607..d25961a1 100755 --- a/assets/i18n/ja_JP.i18n.json +++ b/assets/i18n/ja_JP.i18n.json @@ -1,10 +1,10 @@ { "okButton": "OK", "cancelButton": "キャンセル", - "dismissButton": "閉じる", + "dismissButton": "取り消す", "quitButton": "終了", "updateButton": "更新", - "suggested": "推奨: ${version}", + "suggested": "推奨バージョン: ${version}", "yesButton": "はい", "noButton": "いいえ", "warning": "警告", @@ -30,12 +30,12 @@ "noInstallations": "パッチ済みのアプリはインストールされていません", "installUpdate": "更新を適用しますか?", "updateSheetTitle": "ReVanced Managerを更新", - "updateDialogTitle": "新しいアップデートが利用可能", + "updateDialogTitle": "新しいアップデートが利用可能です", "updatePatchesSheetTitle": "ReVanced Patchesを更新", - "updateChangelogTitle": "変更履歴", + "updateChangelogTitle": "更新履歴", "updateDialogText": "${file} の更新が利用可能です。\n現在のバージョン: ${version} ", "downloadConsentDialogTitle": "必要なファイルをダウンロードしますか?", - "downloadConsentDialogText": "ReVanced Managerを正常に動作させるためのファイルをダウンロードする必要があります。", + "downloadConsentDialogText": "ReVanced Managerを正常に動作させるために必要なファイルをダウンロードする必要があります。", "downloadConsentDialogText2": "${url} に接続します。", "downloadingMessage": "更新データをダウンロードしています...", "downloadedMessage": "アップデートのダウンロードが完了しました", @@ -74,7 +74,7 @@ }, "socialMediaCard": { "widgetTitle": "SNS", - "widgetSubtitle": "私たちはここにいます!" + "widgetSubtitle": "私たちはここにいます!" }, "appSelectorView": { "viewTitle": "アプリを選択", @@ -82,10 +82,10 @@ "storageButton": "APKファイルを選択", "selectFromStorageButton": "ストレージから選択", "errorMessage": "選択したアプリは使用できません", - "downloadToast": "ダウンロード機能は現在開発中です", + "downloadToast": "ダウンロード機能はまだ実装されていないため、利用できません", "requireSuggestedAppVersionDialogText": "選択したアプリのバージョンは推奨バージョンではありません。予期せぬ問題が発生する可能性があります。\n推奨バージョンを選択してください。\n\n選択されたバージョン ${selected}\n推奨バージョン ${suggested}\n\n選択されたバージョンを使用する場合、設定から「推奨バージョンの使用を強制」を無効にしてください。", "featureNotAvailable": "この機能は未実装です", - "featureNotAvailableText": "このアプリは分割APKであり、パッチの適用とインストールにはroot権限でマウントする必要があります。ただし、ストレージから単一APKを選択することでパッチの適用とインストールが可能です。" + "featureNotAvailableText": "このアプリは分割APKであり、パッチの適用とインストールにはルート権限でマウントする必要があります。ただし、ストレージから単一APKを選択することでパッチの適用とインストールが可能です。" }, "patchesSelectorView": { "viewTitle": "パッチを選択", @@ -111,8 +111,8 @@ "saveOptions": "保存", "unselectPatch": "パッチの選択を解除", "tooltip": "他の入力オプション", - "selectFilePath": "ファイルパスを選択", - "selectFolder": "フォルダーを選択", + "selectFilePath": "ファイルのパスを選択", + "selectFolder": "フォルダを選択", "requiredOption": "このオプションは必須です", "unsupportedOption": "このオプションはサポートされていません", "requiredOptionNull": "以下のオプションを設定する必要があります:\n\n${options}" @@ -130,12 +130,12 @@ "installButton": "インストール", "installRootType": "マウント", "installNonRootType": "標準", - "warning": "予期しない問題を避けるため、パッチを適用したアプリの自動更新を無効にします。", - "pressBackAgain": "キャンセルするには、もう一度戻るを押してください", + "warning": "予期しない問題を避けるため、パッチを適用したアプリの自動更新を無効にすることを推奨します。", + "pressBackAgain": "キャンセルするには、もう一度戻るボタンを押してください", "openButton": "開く", - "notificationTitle": "ReVanced Manager はパッチを適用しています", + "notificationTitle": "ReVanced Managerはパッチを適用しています", "notificationText": "インストーラーに戻るにはタップしてください", - "exportApkButtonTooltip": "パッチ済みの APK をエクスポート", + "exportApkButtonTooltip": "パッチ済みのAPKをエクスポート", "exportLogButtonTooltip": "ログをエクスポート", "screenshotDetected": "スクリーンショットが検出されました。ログを共有しようとしてる場合は、代わりにテキストをコピーしてください。\n\nクリップボードにログをコピーしますか?", "copiedToClipboard": "ログをクリップボードにコピーしました", @@ -147,14 +147,14 @@ "teamSectionTitle": "開発チーム", "debugSectionTitle": "デバッグ", "advancedSectionTitle": "高度な設定", - "exportSectionTitle": "インポート&エクスポート", + "exportSectionTitle": "インポート/エクスポート", "dataSectionTitle": "データソース", "themeModeLabel": "アプリのテーマ", - "systemThemeLabel": "システム", + "systemThemeLabel": "システムの設定に従う", "lightThemeLabel": "ライト", "darkThemeLabel": "ダーク", "dynamicThemeLabel": "Material You", - "dynamicThemeHint": "よりデバイスに近い体験が楽しめます", + "dynamicThemeHint": "お使いのデバイスに近い体験をお楽しみください", "languageLabel": "言語", "languageUpdated": "言語が更新されました", "sourcesLabel": "代替ソース", @@ -163,31 +163,32 @@ "useAlternativeSourcesHint": "APIの代わりにReVended Patchesの代替ソースを使用します", "sourcesResetDialogTitle": "リセット", "sourcesResetDialogText": "ソースをデフォルト値にリセットしてもよろしいですか?", - "apiURLResetDialogText": "API の URL をデフォルト値にリセットしてもよろしいですか?", + "apiURLResetDialogText": "APIのURLをデフォルト値にリセットしてもよろしいですか?", "sourcesUpdateNote": "注意: 自動的に代替ソースからReVanced Patchesをダウンロードします。\n\nこれにより、代替ソースに接続されます。", - "apiURLLabel": "API の URL", - "apiURLHint": "ReVinced ManagerのAPI URLを設定します", - "selectApiURL": "API の URL", + "apiURLLabel": "APIのURL", + "apiURLHint": "ReVanced ManagerのAPI URLを設定します", + "selectApiURL": "APIのURL", "orgPatchesLabel": "Patches の組織", "sourcesPatchesLabel": "Patches のソース", "contributorsLabel": "貢献者", "contributorsHint": "ReVancedの貢献者一覧", "logsLabel": "ログを共有", - "logsHint": "ReVanced Manager のログを共有します", + "logsHint": "ReVanced Managerのログを共有します", "enablePatchesSelectionLabel": "パッチ選択の変更を許可", - "enablePatchesSelectionHint": "パッチの選択・解除を禁止しません", + "enablePatchesSelectionHint": "パッチの選択/解除を禁止しません", "enablePatchesSelectionWarningText": "パッチの選択を変更すると、予期せぬ問題が起こる可能性があります。\n\n有効にしますか?", "disablePatchesSelectionWarningText": "パッチ選択の変更を無効にしようとしています。\nデフォルトのパッチの選択が復元されます。\n\n無効にしますか?", "autoUpdatePatchesLabel": "パッチの自動アップデート", "autoUpdatePatchesHint": "パッチを自動的に最新バージョンに更新する", + "usePrereleasesHint": "ReVanced ManagerとReVanced Patchesのプレリリース版を使用します", "showUpdateDialogLabel": "アップデートの通知を表示", "showUpdateDialogHint": "新しいアップデートが利用可能な場合にダイアログを表示します", "universalPatchesLabel": "共通パッチの表示", - "universalPatchesHint": "すべてのアプリと共通パッチを表示します(アプリ一覧の読み込みが遅くなる可能性があります)", + "universalPatchesHint": "すべてのアプリと共通パッチを表示します (アプリ一覧の読み込みが遅くなる可能性があります)", "lastPatchedAppLabel": "パッチを適用したアプリを保存", "lastPatchedAppHint": "最後にパッチされた内容を保存して、後でインストールまたはエクスポートできます", "versionCompatibilityCheckLabel": "バージョンの互換性の確認", - "versionCompatibilityCheckHint": "選択したアプリのバージョンと互換性のないパッチの選択を禁止する", + "versionCompatibilityCheckHint": "選択したアプリのバージョンと互換性のないパッチの選択を防止します", "requireSuggestedAppVersionLabel": "推奨バージョンの使用を強制", "requireSuggestedAppVersionHint": "推奨されていないバージョンのアプリを選択できないようにします", "requireSuggestedAppVersionDialogText": "推奨バージョンではないアプリを選択すると、予期しない問題が発生する可能性があります。\n\nこのまま続行しますか?", @@ -195,24 +196,24 @@ "snackbarMessage": "クリップボードにコピーしました", "restartAppForChanges": "変更を適用するにはアプリを再起動してください", "deleteTempDirLabel": "一時ファイルを削除", - "deleteTempDirHint": "未使用の一時ファイルを削除", + "deleteTempDirHint": "未使用の一時ファイルを削除します", "deletedTempDir": "一時ファイルを削除しました", "exportSettingsLabel": "設定をエクスポート", "exportSettingsHint": "設定をJSONファイルにエクスポートします", "exportedSettings": "設定をエクスポートしました", "importSettingsLabel": "設定をインポート", "importSettingsHint": "JSONファイルから設定をインポートします", - "importedSettings": "設定がインポートされました", + "importedSettings": "設定をインポートしました", "exportPatchesLabel": "パッチ選択をエクスポート", - "exportPatchesHint": "パッチ選択を JSON ファイルにエクスポートします", + "exportPatchesHint": "パッチ選択をJSONファイルにエクスポートします", "exportedPatches": "パッチ選択をエクスポートしました", "noExportFileFound": "エクスポートするパッチの選択がありません", "importPatchesLabel": "パッチ選択をインポート", - "importPatchesHint": "パッチ選択を JSON ファイルからインポートします", + "importPatchesHint": "パッチ選択をJSONファイルからインポートします", "importedPatches": "パッチ選択をインポートしました", "resetStoredPatchesLabel": "パッチの選択をリセット", "resetStoredPatchesHint": "保存されたパッチの選択をリセットする", - "resetStoredPatchesDialogTitle": "パッチの選択をリセット", + "resetStoredPatchesDialogTitle": "パッチの選択をリセットしますか?", "resetStoredPatchesDialogText": "デフォルトのパッチの選択が復元されます。", "resetStoredPatches": "パッチの選択をリセットしました", "resetStoredOptionsLabel": "パッチオプションをリセット", @@ -221,7 +222,7 @@ "resetStoredOptionsDialogText": "パッチオプションをリセットすると、保存されたすべてのオプションが削除されます。", "resetStoredOptions": "オプションをリセットしました", "deleteLogsLabel": "ログを削除", - "deleteLogsHint": "収集された ReVanced Manager のログを削除します", + "deleteLogsHint": "収集されたReVanced Managerのログを削除します", "deletedLogs": "ログを削除しました", "regenerateKeystoreLabel": "キーストアを再生成", "regenerateKeystoreHint": "アプリの署名に使われるキーストアを再生成します", @@ -237,11 +238,11 @@ "importedKeystore": "キーストアをインポートしました", "selectKeystorePassword": "キーストアのパスワード", "selectKeystorePasswordHint": "アプリの署名に使用するキーストアのパスワードを入力してください", - "jsonSelectorErrorMessage": "選択したJSON ファイルは使用できません", + "jsonSelectorErrorMessage": "選択したJSONファイルは使用できません", "keystoreSelectorErrorMessage": "選択したキーストアファイルは使用できません" }, "appInfoView": { - "widgetTitle": "アプリ情報", + "widgetTitle": "アプリの情報", "openButton": "開く", "installButton": "インストール", "uninstallButton": "アンインストール", @@ -250,11 +251,11 @@ "deleteButton": "削除", "rootDialogTitle": "エラー", "lastPatchedAppDescription": "これは最後にパッチを適用したアプリのバックアップです。", - "unmountDialogText": "このアプリをアンマウントしてもよろしいですか?", - "uninstallDialogText": "本当にこのアプリをアンインストールしますか?", - "rootDialogText": "アプリはスーパーユーザー権限でインストールされましたが、現在 ReVanced Manager にはその権限がありません。 スーパーユーザー権限を付与してください。", + "unmountDialogText": "このアプリをマウント解除してもよろしいですか?", + "uninstallDialogText": "このアプリをアンインストールしてもよろしいですか?", + "rootDialogText": "アプリはルート権限でインストールされましたが、現在ReVanced Managerにはその権限がありません。 ルート権限を付与してください。", "removeAppDialogTitle": "アプリを削除しますか?", - "removeAppDialogText": "本当にバックアップを削除してもよろしいですか?", + "removeAppDialogText": "バックアップを削除してもよろしいですか?", "packageNameLabel": "パッケージ名", "installTypeLabel": "インストールの種類", "mountTypeLabel": "マウント", @@ -263,7 +264,7 @@ "appliedPatchesLabel": "適用されたパッチ", "sizeLabel": "ファイルのサイズ", "patchedDateHint": "${date} ${time}", - "appliedPatchesHint": "${quantity} 個の適用されたパッチ", + "appliedPatchesHint": "${quantity}個の適用されたパッチ", "updateNotImplemented": "この機能はまだ実装されていません" }, "contributorsView": { @@ -271,7 +272,7 @@ }, "installErrorDialog": { "mount_version_mismatch": "バージョンが一致しません", - "mount_no_root": "ルート権限がありません", + "mount_no_root": "ルート権限が付与されていません", "mount_missing_installation": "インストールが見つかりませんでした", "status_failure_blocked": "インストールはブロックされました", "install_failed_verification_failure": "検証に失敗しました", @@ -283,16 +284,16 @@ "status_failure_timeout": "インストールがタイムアウトしました", "status_unknown": "インストールに失敗しました", "mount_version_mismatch_description": "インストールされたアプリがパッチを当てたアプリとは異なるバージョンであるため、インストールに失敗しました。\n\nマウントしているアプリのバージョンをインストールし、再度お試しください。", - "mount_no_root_description": "ルートアクセスが許可されていないためインストールに失敗しました。\n\nReVanced Managerへのルートアクセスを許可し、もう一度お試しください。", + "mount_no_root_description": "ルート権限が付与されていないためインストールに失敗しました。\n\nReVanced Managerへのルート権限を許可し、もう一度お試しください。", "mount_missing_installation_description": "パッチが適用されていないアプリがマウントされていないためインストールに失敗しました。\n\nマウントする前にパッチが適用されていないアプリをインストールし、もう一度お試しください。", - "status_failure_timeout_description": "インストールに時間がかかりすぎました。\n\nもう一度やり直しますか?", + "status_failure_timeout_description": "インストールに時間がかかりすぎたため、完了しませんでした。\n\nもう一度やり直しますか?", "status_failure_storage_description": "ストレージが不足しているためインストールに失敗しました。\n\n空き領域を解放して再度お試し下さい。", "status_failure_invalid_description": "パッチ適用されたアプリが無効なためインストールに失敗しました。\n\nアプリをアンインストールしてもう一度お試しください。", "status_failure_incompatible_description": "アプリはこのデバイスと互換性がありません。\n\nこのデバイスに対応しているAPKを使用して、もう一度お試しください。", - "status_failure_conflict_description": "インストールはアプリの既存のインストールによって中止されました。\n\nインストールされたアプリをアンインストールし、もう一度やり直してください。", - "status_failure_blocked_description": "インストールは ${packageName} によってブロックされました。\n\nセキュリティ設定を調整して、もう一度お試しください。", - "install_failed_verification_failure_description": "認証の問題によりインストールに失敗しました。\n\nセキュリティ設定を調整して、もう一度お試しください。", - "install_failed_version_downgrade_description": "インストールに失敗しました。パッチを当てたアプリがインストールされたアプリよりも低いバージョンであるためです。\n\nアプリをアンインストールしてもう一度お試しください。", + "status_failure_conflict_description": "既にインストールされているアプリがあるため、インストールが中止されました。\n\nインストールされているアプリをアンインストールして、再試行しますか?", + "status_failure_blocked_description": "インストールは${packageName}によってブロックされました。\n\nセキュリティ設定を確認して、もう一度お試しください。", + "install_failed_verification_failure_description": "認証の問題によりインストールに失敗しました。\n\nセキュリティ設定を確認して、もう一度お試しください。", + "install_failed_version_downgrade_description": "インストールに失敗しました。パッチが適用されたアプリがインストールされたアプリよりも低いバージョンであるためです。\n\nアプリをアンインストールしてもう一度お試しください。", "status_unknown_description": "不明な理由によりインストールに失敗しました。もう一度やり直してください。" } } \ No newline at end of file diff --git a/assets/i18n/ko_KR.i18n.json b/assets/i18n/ko_KR.i18n.json index 166349c5..4adb5b03 100755 --- a/assets/i18n/ko_KR.i18n.json +++ b/assets/i18n/ko_KR.i18n.json @@ -11,7 +11,7 @@ "notice": "알림", "noShowAgain": "다시 보지 않기", "add": "추가", - "remove": "제거", + "remove": "삭제", "showChangelogButton": "변경 사항 보기", "showUpdateButton": "업데이트 보기", "navigationView": { @@ -25,18 +25,18 @@ "updatesSubtitle": "업데이트", "lastPatchedAppSubtitle": "마지막으로 패치된 앱", "patchedSubtitle": "설치된 앱", - "changeLaterSubtitle": "나중에 설정에서 변경할 수 있습니다", + "changeLaterSubtitle": "나중에 설정에서 변경할 수 있습니다.", "noSavedAppFound": "앱을 찾을 수 없습니다", "noInstallations": "패치된 앱이 설치되어 있지 않습니다", "installUpdate": "업데이트를 계속 설치하시겠습니까?", "updateSheetTitle": "ReVanced Manager 업데이트", - "updateDialogTitle": "새 업데이트가 있습니다", - "updatePatchesSheetTitle": "ReVanced 패치 업데이트", + "updateDialogTitle": "새로운 업데이트가 있습니다", + "updatePatchesSheetTitle": "ReVanced Patches 업데이트", "updateChangelogTitle": "변경 사항", - "updateDialogText": "'${file}'에 대한 새 업데이트를 할 수 있습니다\n\n현재 설치된 버전은 '${version}' 입니다", + "updateDialogText": "'${file}'에 대한 새로운 업데이트를 할 수 있습니다.\n\n현재 설치된 버전은 '${version}' 입니다.", "downloadConsentDialogTitle": "필요한 파일을 다운로드하시겠습니까?", - "downloadConsentDialogText": "ReVanced Manager가 제대로 작동하려면 필요한 파일을 다운로드해야 합니다", - "downloadConsentDialogText2": "진행하면 '${url}' 에 연결됩니다", + "downloadConsentDialogText": "ReVanced Manager가 제대로 작동하려면 필요한 파일을 다운로드해야 합니다.", + "downloadConsentDialogText2": "진행하면 '${url}' 에 연결됩니다.", "downloadingMessage": "업데이트 다운로드 중 ...", "downloadedMessage": "업데이트를 다운로드 완료하였습니다", "installingMessage": "업데이트 설치 중 ...", @@ -55,9 +55,9 @@ "widgetTitle": "Patcher", "patchButton": "패치하기", "incompatibleArchWarningDialogText": "이 아키텍처에 대한 패치는 아직 지원되지 않으므로 실패할 수 있습니다. 그래도 계속하시겠습니까?", - "removedPatchesWarningDialogText": "이 앱을 마지막으로 패치한 이후 제거된 패치입니다:\n\n${patches}\n\n${newPatches}\n\n그래도 계속하시겠습니까?", + "removedPatchesWarningDialogText": "이 앱을 마지막으로 패치한 이후 삭제된 패치입니다:\n\n${patches}\n\n${newPatches}\n\n그래도 계속하시겠습니까?", "addedPatchesDialogText": "이 앱을 마지막으로 패치한 이후 추가된 패치입니다:\n\n${addedPatches}", - "requiredOptionDialogText": "일부 패치 옵션을 설정해야 합니다" + "requiredOptionDialogText": "일부 패치 옵션을 설정해야 합니다." }, "appSelectorCard": { "widgetTitle": "앱 선택하기", @@ -83,15 +83,15 @@ "selectFromStorageButton": "기기 저장소에서 선택", "errorMessage": "선택한 앱을 사용할 수 없습니다", "downloadToast": "다운로드 기능은 아직 사용할 수 없습니다", - "requireSuggestedAppVersionDialogText": "선택한 앱 버전이 권장 앱 버전과 일치하지 않아서 예상되지 않은 문제점이 발생할 수 있습니다. 권장 앱 버전을 사용하세요\n\n선택한 앱 버전: ${selected}\n권장 앱 버전: ${suggested}\n\n계속하려면 설정에서 '권장 앱 버전 요구'를 비활성화하세요", + "requireSuggestedAppVersionDialogText": "선택한 앱 버전이 권장 앱 버전과 일치하지 않아서 예상되지 않은 문제점이 발생할 수 있습니다. 권장 앱 버전을 사용하세요.\n\n선택한 앱 버전: ${selected}\n권장 앱 버전: ${suggested}\n\n계속하려면 설정에서 '권장 앱 버전 요구'를 비활성화하세요.", "featureNotAvailable": "기능이 구현되지 않았습니다", - "featureNotAvailableText": "이 기기에서 추출할 수 있는 앱이 분할된 APK 파일이므로 Root 권한으로 마운트해야만 안정적으로 패치 및 설치할 수 있습니다. 그러나 Non-Root 사용자는 기기 저장소에서 '외부에서 다운로드한 완전한 APK 파일'을 선택하여 패치 및 설치할 수 있습니다" + "featureNotAvailableText": "이 기기에서 추출할 수 있는 앱이 분할 APK 파일이므로 Root 권한으로 마운트해야만 안정적으로 패치 및 설치할 수 있습니다. 그러나 Non-Root 사용자는 기기 저장소에서 '외부에서 다운로드한 완전한 APK 파일'을 선택하여 패치 및 설치할 수 있습니다." }, "patchesSelectorView": { "viewTitle": "패치 선택하기", "searchBarHint": "패치 검색하기", "universalPatches": "공용 패치", - "newPatches": "새 패치", + "newPatches": "새로운 패치", "patches": "패치", "doneButton": "선택완료", "defaultChip": "기본값", @@ -99,9 +99,9 @@ "noneChip": "선택안함", "noneTooltip": "모든 패치 선택 해제", "loadPatchesSelection": "패치 선택목록 가져오기", - "noSavedPatches": "선택한 앱에 적용할 패치가 저장되지 않았습니다\n'선택완료' 버튼을 눌러서 현재 선택목록을 저장하세요", + "noSavedPatches": "선택한 앱에 적용할 패치가 저장되지 않았습니다.\n'선택완료' 버튼을 눌러서 현재 선택목록을 저장하세요.", "noPatchesFound": "선택한 앱에 대한 패치를 찾을 수 없습니다", - "setRequiredOption": "옵션을 설정해야 하는 패치가 있습니다:\n\n${patches}\n\n진행하기 전 설정을 마쳐주세요" + "setRequiredOption": "옵션을 설정해야 하는 패치가 있습니다:\n\n${patches}\n\n진행하기 전 설정을 마쳐주세요." }, "patchOptionsView": { "customValue": "사용자 정의 값", @@ -118,26 +118,26 @@ "requiredOptionNull": "다음 옵션들이 설정되어 있어야 합니다:\n\n${options}" }, "patchItem": { - "unsupportedDialogText": "이 패치는 문제점을 발생시킬 수 있습니다\n\n앱 버전: ${packageVersion}\n지원되는 버전:\n${supportedVersions}", - "unsupportedPatchVersion": "패치가 이 앱 버전을 지원하지 않습니다", + "unsupportedDialogText": "이 패치는 문제점을 발생시킬 수 있습니다.\n\n앱 버전: ${packageVersion}\n지원되는 버전:\n${supportedVersions}", + "unsupportedPatchVersion": "패치가 이 앱 버전을 지원하지 않습니다.", "unsupportedRequiredOption": "패치에 이 앱을 지원하지 않는 필수 옵션이 포함되어 있습니다", - "patchesChangeWarningDialogText": "기본 패치 선택을 사용하는 것을 권장합니다. 설정을 변경할 경우에는 예상되지 않은 문제점의 원인이 될 수 있습니다\n\n패치 선택을 변경하기 위해서는 설정에서 \"패치 선택 변경 허용\"을 활성화해야 합니다", + "patchesChangeWarningDialogText": "기본 패치 선택을 사용하는 것을 권장합니다. 설정을 변경할 경우에는 예상되지 않은 문제점의 원인이 될 수 있습니다.\n\n패치 선택을 변경하기 위해서는 설정에서 \"패치 선택 변경 허용\"을 활성화해야 합니다.", "patchesChangeWarningDialogButton": "기본 선택목록 사용" }, "installerView": { "installType": "설치 유형 선택", - "installTypeDescription": "설치를 진행할 유형을 선택해주세요", + "installTypeDescription": "설치를 진행할 유형을 선택해주세요.", "installButton": "설치하기", "installRootType": "마운트", "installNonRootType": "일반", - "warning": "패치된 앱의 자동 업데이트를 비활성화하여 예상되지 않은 문제점을 방지하세요", + "warning": "패치된 앱의 자동 업데이트를 비활성화하여 예상되지 않은 문제점을 방지하세요.", "pressBackAgain": "취소하려면 뒤로가기 버튼을 다시 누르세요", "openButton": "열기", "notificationTitle": "ReVanced Manager가 패치 중입니다", "notificationText": "설치 관리자로 돌아가려면 여기를 누르세요", "exportApkButtonTooltip": "패치된 APK 내보내기", "exportLogButtonTooltip": "로그 내보내기", - "screenshotDetected": "스크린샷이 감지되었습니다. 로그를 공유할 목적이라면, 대신 텍스트 사본으로 공유해주세요\n\n로그를 클립보드에 복사하시겠습니까?", + "screenshotDetected": "스크린샷이 감지되었습니다. 로그를 공유할 목적이라면, 대신 텍스트 사본으로 공유해주세요.\n\n로그를 클립보드에 복사하시겠습니까?", "copiedToClipboard": "로그를 클립보드에 복사하였습니다", "noExit": "설치 관리자가 실행 중이므로 중단할 수 없습니다 ..." }, @@ -164,7 +164,7 @@ "sourcesResetDialogTitle": "초기화", "sourcesResetDialogText": "정말 사용자 정의 소스를 기본값으로 초기화하시겠습니까?", "apiURLResetDialogText": "정말 API URL을 기본값으로 초기화하시겠습니까?", - "sourcesUpdateNote": "알림: 변경하면 대체 소스에서 ReVanced Patches가 자동으로 다운로드됩니다\n\n그 이후에는 대체 소스로 연결됩니다", + "sourcesUpdateNote": "알림: 변경하면 대체 소스에서 ReVanced Patches가 자동으로 다운로드됩니다.\n\n그 이후에는 대체 소스로 연결됩니다.", "apiURLLabel": "API URL", "apiURLHint": "ReVanced Manager의 API URL를 설정할 수 있습니다", "selectApiURL": "API URL", @@ -176,12 +176,15 @@ "logsHint": "수집된 ReVanced Manager 로그를 공유합니다", "enablePatchesSelectionLabel": "패치 선택 변경 허용", "enablePatchesSelectionHint": "패치를 선택하거나 선택 해제할 수 있습니다", - "enablePatchesSelectionWarningText": "패치 선택을 변경하는 경우에는 예상되지 않은 문제점이 발생할 수 있습니다\n\n그래도 활성화하시겠습니까?", - "disablePatchesSelectionWarningText": "패치 선택 변경을 비활성화하려 합니다\n기본 패치 선택목록으로 복원될 것입니다\n\n그래도 비활성화하시겠습니까?", + "enablePatchesSelectionWarningText": "패치 선택을 변경하는 경우에는 예상되지 않은 문제점이 발생할 수 있습니다.\n\n그래도 활성화하시겠습니까?", + "disablePatchesSelectionWarningText": "패치 선택 변경을 비활성화하려 합니다.\n기본 패치 선택목록으로 복원될 것입니다.\n\n그래도 비활성화하시겠습니까?", "autoUpdatePatchesLabel": "패치 자동 업데이트", - "autoUpdatePatchesHint": "ReVanced Manager 앱 내에서 사용되는 패치를 최신 버전으로 자동 업데이트합니다", + "autoUpdatePatchesHint": "ReVanced Patches를 최신 버전으로 자동 업데이트합니다", + "usePrereleasesLabel": "Pre-Releases 버전 사용", + "usePrereleasesHint": "ReVanced Manager 및 ReVanced Patches의 Pre-Releases 버전을 사용합니다", + "usePrereleasesWarningText": "Pre-Releases 버전을 사용하면 예상되지 않은 문제점이 발생할 수 있습니다.\n\n그래도 활성화하시겠습니까?", "showUpdateDialogLabel": "업데이트 팝업창 보기", - "showUpdateDialogHint": "새 업데이트가 있으면 팝업창을 표시합니다", + "showUpdateDialogHint": "새로운 업데이트가 있으면 팝업창을 표시합니다", "universalPatchesLabel": "공용 패치 보기", "universalPatchesHint": "기기에 설치된 모든 앱과 공용 패치를 표시합니다 (앱 목록이 느려질 수 있음)", "lastPatchedAppLabel": "패치된 앱 저장", @@ -190,18 +193,18 @@ "versionCompatibilityCheckHint": "선택한 앱 버전과 호환되지 않는 패치를 선택할 수 없습니다", "requireSuggestedAppVersionLabel": "권장 앱 버전 요구", "requireSuggestedAppVersionHint": "권장되지 않은 앱 버전은 선택할 수 없습니다", - "requireSuggestedAppVersionDialogText": "권장 앱 버전이 아닌 앱을 선택하는 경우에는 예상되지 않은 문제점이 발생할 수 있습니다\n\n그래도 계속하시겠습니까?", + "requireSuggestedAppVersionDialogText": "권장 앱 버전이 아닌 앱을 선택하는 경우에는 예상되지 않은 문제점이 발생할 수 있습니다.\n\n그래도 계속하시겠습니까?", "aboutLabel": "정보", "snackbarMessage": "클립보드에 복사하였습니다", "restartAppForChanges": "변경 사항을 적용하려면 앱을 다시 시작하세요", - "deleteTempDirLabel": "임시 파일 제거", - "deleteTempDirHint": "사용하지 않는 임시 파일을 제거합니다", - "deletedTempDir": "임시 파일을 제거하였습니다", + "deleteTempDirLabel": "임시 파일 삭제", + "deleteTempDirHint": "사용하지 않는 임시 파일을 삭제합니다", + "deletedTempDir": "임시 파일을 삭제하였습니다", "exportSettingsLabel": "설정 내보내기", - "exportSettingsHint": "설정을 JSON 파일로 내보낼 수 있습니다", + "exportSettingsHint": "설정을 JSON 파일로 내보냅니다", "exportedSettings": "설정을 내보냈습니다", "importSettingsLabel": "설정 가져오기", - "importSettingsHint": "설정을 JSON 파일에서 가져올 수 있습니다", + "importSettingsHint": "설정을 JSON 파일에서 가져옵니다", "importedSettings": "설정을 가져왔습니다", "exportPatchesLabel": "패치 선택목록 내보내기", "exportPatchesHint": "패치 선택목록을 JSON 파일로 내보냅니다", @@ -218,15 +221,15 @@ "resetStoredOptionsLabel": "패치 옵션 초기화", "resetStoredOptionsHint": "모든 패치 옵션을 초기화합니다", "resetStoredOptionsDialogTitle": "패치 옵션을 초기화하시겠습니까?", - "resetStoredOptionsDialogText": "패치 옵션을 초기화하면 저장한 모든 옵션이 제거됩니다", + "resetStoredOptionsDialogText": "패치 옵션을 초기화하면 저장한 모든 옵션이 삭제됩니다.", "resetStoredOptions": "옵션을 초기화하였습니다", - "deleteLogsLabel": "로그 제거하기", - "deleteLogsHint": "수집된 ReVanced Manager 로그를 제거합니다", - "deletedLogs": "로그를 제거하였습니다", + "deleteLogsLabel": "로그 삭제하기", + "deleteLogsHint": "수집된 ReVanced Manager 로그를 삭제합니다", + "deletedLogs": "로그를 삭제하였습니다", "regenerateKeystoreLabel": "키스토어 재생성", "regenerateKeystoreHint": "앱을 서명할 때 사용한 키스토어를 재생성합니다", "regenerateKeystoreDialogTitle": "키스토어를 재생성하시겠습니까?", - "regenerateKeystoreDialogText": "기존 키스토어로 서명한 패치된 앱을 더 이상 업데이트할 수 없게 됩니다", + "regenerateKeystoreDialogText": "기존 키스토어로 서명한 패치된 앱을 더 이상 업데이트할 수 없게 됩니다.", "regeneratedKeystore": "키스토어를 재생성하였습니다", "exportKeystoreLabel": "키스토어 내보내기", "exportKeystoreHint": "앱을 서명할 때 사용한 키스토어를 내보냅니다", @@ -244,17 +247,17 @@ "widgetTitle": "앱 정보", "openButton": "열기", "installButton": "설치하기", - "uninstallButton": "제거하기", + "uninstallButton": "삭제하기", "unmountButton": "마운트 해제", "exportButton": "내보내기", - "deleteButton": "제거하기", + "deleteButton": "삭제하기", "rootDialogTitle": "오류", - "lastPatchedAppDescription": "마지막으로 패치된 앱의 백업입니다", + "lastPatchedAppDescription": "마지막으로 패치된 앱의 백업입니다.", "unmountDialogText": "이 앱의 마운트를 해제하시겠습니까?", - "uninstallDialogText": "이 앱을 제거하시겠습니까?", - "rootDialogText": "앱이 슈퍼유저 권한으로 설치되었으나 현재 ReVanced Manager에는 권한이 없습니다\n먼저 슈퍼유저 권한을 부여하세요", - "removeAppDialogTitle": "앱을 제거하시겠습니까?", - "removeAppDialogText": "이 백업을 제거하시겠습니까?", + "uninstallDialogText": "이 앱을 삭제하시겠습니까?", + "rootDialogText": "앱이 슈퍼유저 권한으로 설치되었으나 현재 ReVanced Manager에는 권한이 없습니다.\n먼저 슈퍼유저 권한을 부여하세요.", + "removeAppDialogTitle": "앱을 삭제하시겠습니까?", + "removeAppDialogText": "이 백업을 삭제하시겠습니까?", "packageNameLabel": "패키지 이름", "installTypeLabel": "설치 유형", "mountTypeLabel": "마운트", @@ -282,17 +285,17 @@ "status_failure_incompatible": "설치 미호환", "status_failure_timeout": "설치 시간 초과", "status_unknown": "설치 실패", - "mount_version_mismatch_description": "패치된 앱과 설치된 앱의 버전이 달라서 설치에 실패하였습니다\n\n마운트하고 있는 앱의 버전으로 설치한 후에 다시 시도하세요", - "mount_no_root_description": "Root 권한이 주어지지 않아서 설치에 실패하였습니다\n\nReVanced Manager에 Root 권한을 부여한 후에 다시 시도하세요", - "mount_missing_installation_description": "패치되지 않은 앱이 이 기기에 설치되지 않아서 마운트를 진행할 수 없어서 설치에 실패하였습니다\n\n마운트하기 전 패치되지 않은 앱을 설치한 후에 다시 시도하세요", - "status_failure_timeout_description": "설치하는 데 시간이 너무 오래 걸립니다\n\n다시 시도하시겠습니까?", - "status_failure_storage_description": "저장소 공간이 충분하지 않아서 설치에 실패하였습니다\n\n저장소 공간을 확보한 후에 다시 시도하세요", - "status_failure_invalid_description": "패치된 앱이 유효하지 않아서 설치에 실패하였습니다\n\n앱을 제거한 후에 다시 시도하시겠습니까?", - "status_failure_incompatible_description": "이 앱은 이 기기와 호환되지 않습니다\n\n이 기기에서 지원되는 APK를 사용하여 다시 시도하세요", - "status_failure_conflict_description": "기존에 설치된 앱이 설치를 방해하였습니다\n\n설치된 앱을 제거한 후에 다시 시도하시겠습니까?", - "status_failure_blocked_description": "설치가 '${packageName}'에 의해 차단되었습니다\n\n보안 설정을 조정한 후에 다시 시도하세요", - "install_failed_verification_failure_description": "인증 문제로 인해 설치에 실패하였습니다\n\n보안 설정을 조정한 후에 다시 시도하세요", - "install_failed_version_downgrade_description": "패치된 앱의 버전이 설치된 앱의 버전보다 낮아서 설치에 실패하였습니다\n\n앱을 제거한 후에 다시 시도하시겠습니까?", - "status_unknown_description": "알 수 없는 이유로 설치에 실패하였습니다. 다시 시도하세요" + "mount_version_mismatch_description": "패치된 앱과 설치된 앱의 버전이 달라서 설치에 실패하였습니다.\n\n마운트하고 있는 앱의 버전으로 설치한 후에 다시 시도하세요.", + "mount_no_root_description": "Root 권한이 주어지지 않아서 설치에 실패하였습니다.\n\nReVanced Manager에 Root 권한을 부여한 후에 다시 시도하세요.", + "mount_missing_installation_description": "패치되지 않은 앱이 이 기기에 설치되지 않아서 마운트를 진행할 수 없어서 설치에 실패하였습니다.\n\n마운트하기 전 패치되지 않은 앱을 설치한 후에 다시 시도하세요.", + "status_failure_timeout_description": "설치하는 데 시간이 너무 오래 걸립니다.\n\n다시 시도하시겠습니까?", + "status_failure_storage_description": "저장소 공간이 충분하지 않아서 설치에 실패하였습니다.\n\n저장소 공간을 확보한 후에 다시 시도하세요.", + "status_failure_invalid_description": "패치된 앱이 유효하지 않아서 설치에 실패하였습니다.\n\n앱을 삭제한 후에 다시 시도하시겠습니까?", + "status_failure_incompatible_description": "이 앱은 이 기기와 호환되지 않습니다.\n\n이 기기에서 지원되는 APK를 사용하여 다시 시도하세요.", + "status_failure_conflict_description": "기존에 설치된 앱이 설치를 방해하였습니다.\n\n설치된 앱을 삭제한 후에 다시 시도하시겠습니까?", + "status_failure_blocked_description": "설치가 '${packageName}'에 의해 차단되었습니다.\n\n보안 설정을 조정한 후에 다시 시도하세요.", + "install_failed_verification_failure_description": "인증 문제로 인해 설치에 실패하였습니다.\n\n보안 설정을 조정한 후에 다시 시도하세요.", + "install_failed_version_downgrade_description": "패치된 앱의 버전이 설치된 앱의 버전보다 낮아서 설치에 실패하였습니다.\n\n앱을 삭제한 후에 다시 시도하시겠습니까?", + "status_unknown_description": "알 수 없는 이유로 설치에 실패하였습니다. 다시 시도하세요." } } \ No newline at end of file diff --git a/assets/i18n/lv_LV.i18n.json b/assets/i18n/lv_LV.i18n.json index 884e4a7f..0dc4ccdd 100755 --- a/assets/i18n/lv_LV.i18n.json +++ b/assets/i18n/lv_LV.i18n.json @@ -180,6 +180,7 @@ "disablePatchesSelectionWarningText": "Jūs gatavojaties atspējot labojumu izvēles maiņu.\nTiksies atjaunota labojumu noklusējuma izvēle.\n\nTomēr atspējot?", "autoUpdatePatchesLabel": "Automātiski atjaunināt ielāpus", "autoUpdatePatchesHint": "Automātiski atjaunināt ielāpus uz jaunāko versiju", + "usePrereleasesLabel": "Lietot pirmizlaidumus", "showUpdateDialogLabel": "Rādīt atjauninājuma dialoglodziņu", "showUpdateDialogHint": "Rādīt dialoglodziņu, kad ir pieejams jauns atjauninājums", "universalPatchesLabel": "Rādīt universālos ielāpus", diff --git a/assets/i18n/ms_MY.i18n.json b/assets/i18n/ms_MY.i18n.json index 3da81459..68e1ce0f 100755 --- a/assets/i18n/ms_MY.i18n.json +++ b/assets/i18n/ms_MY.i18n.json @@ -1,18 +1,41 @@ { + "okButton": "OK", "cancelButton": "Batal", + "dismissButton": "Melepaskan", + "quitButton": "Henti", "updateButton": "Kemas kini", + "suggested": "Versi dicadangkan: ${version}", "yesButton": "Ya", "noButton": "Tidak", "warning": "Amaran", + "notice": "Notis", + "noShowAgain": "Jangan tunjukkan lagi", + "add": "Tambah", + "remove": "Buang", + "showChangelogButton": "Tunjuk log perubahan", + "showUpdateButton": "Tunjuk kemas kini", "navigationView": { "dashboardTab": "Papan Pemuka", "patcherTab": "Pemodifikasi", "settingsTab": "Tetapan" }, "homeView": { + "refreshSuccess": "Muat semula berjaya", "widgetTitle": "Papan Pemuka", "updatesSubtitle": "Kemas Kini", + "lastPatchedAppSubtitle": "Aplikasi tampalan terakhir", + "patchedSubtitle": "Apl yang telah di pasang", + "changeLaterSubtitle": "Anda boleh menukar tetapan pada waktu lain", + "noSavedAppFound": "Tiada aplikasi di jumpai", + "noInstallations": "Tiada aplikasi tampalan telah di pasang", + "installUpdate": "Teruskan untuk pasang kemaskini? ", + "updateSheetTitle": "Kemaskini ReVanced Manager", + "updateDialogTitle": "Kemas kini baharu tersedia", + "updatePatchesSheetTitle": "Kemaskini ReVanced Patches", "updateChangelogTitle": "Log perubahan", + "updateDialogText": "Kemas kini baharu tersedia untuk ${file}\n\nVersi yang terpasang adalah ${version}", + "downloadConsentDialogTitle": "Muat turun fail yang diperlukan?", + "downloadConsentDialogText": "ReVanced Manager perlu memuat turun fail penting untuk berfungsi dengan baik.", "downloadingMessage": "Memuat turun pengemaskinian...", "downloadedMessage": "Kemaskini dimuat turun", "installingMessage": "Memasang pengemaskinian...", diff --git a/assets/i18n/pl_PL.i18n.json b/assets/i18n/pl_PL.i18n.json index e090d59d..9bb8f02e 100755 --- a/assets/i18n/pl_PL.i18n.json +++ b/assets/i18n/pl_PL.i18n.json @@ -7,7 +7,7 @@ "suggested": "Sugerowana wersja: ${version}", "yesButton": "Tak", "noButton": "Nie", - "warning": "Ostrzeżenie", + "warning": "Uwaga", "notice": "Komunikat", "noShowAgain": "Nie pokazuj ponownie", "add": "Dodaj", @@ -23,7 +23,7 @@ "refreshSuccess": "Odświeżono pomyślnie", "widgetTitle": "Panel główny", "updatesSubtitle": "Aktualizacje", - "lastPatchedAppSubtitle": "Ostatnio poprawiona aplikacja", + "lastPatchedAppSubtitle": "Ostatnio załatana aplikacja", "patchedSubtitle": "Zainstalowane aplikacje", "changeLaterSubtitle": "Możesz to zmienić w ustawieniach w późniejszym czasie.", "noSavedAppFound": "Nie znaleziono aplikacji", @@ -52,7 +52,7 @@ "timeagoLabel": "${time} temu" }, "patcherView": { - "widgetTitle": "Łataj", + "widgetTitle": "Łatanie", "patchButton": "Załataj", "incompatibleArchWarningDialogText": "Łatanie na tej architekturze nie jest jeszcze obsługiwane i może się nie powieść. Czy chcesz kontynuować mimo to?", "removedPatchesWarningDialogText": "Łatki usunięte od czasu Twojego ostatniego łatania aplikacji:\n\n${patches}\n\n${newPatches}Kontynuować?", @@ -79,7 +79,7 @@ "appSelectorView": { "viewTitle": "Wybierz aplikację", "searchBarHint": "Wyszukaj aplikację", - "storageButton": "Pamięć", + "storageButton": "Wybierz z pamięci", "selectFromStorageButton": "Wybierz z pamięci", "errorMessage": "Nie można użyć wybranej aplikacji", "downloadToast": "Funkcja pobierania jest jeszcze niedostępna", @@ -107,7 +107,7 @@ "customValue": "Niestandardowa wartość", "setToNull": "Ustaw na zerowy", "nullValue": "Ta opcja jest obecnie pusta", - "viewTitle": "Opcje łatek", + "viewTitle": "Opcje łatki", "saveOptions": "Zapisz", "unselectPatch": "Odznacz łatkę", "tooltip": "Więcej opcji wejściowych", @@ -154,9 +154,9 @@ "lightThemeLabel": "Jasny", "darkThemeLabel": "Ciemny", "dynamicThemeLabel": "Material You", - "dynamicThemeHint": "Ciesz się wrażeniami bliższymi twojemu urządzeniu", + "dynamicThemeHint": "Ciesz się wyglądem dopasowanym do twojego urządzenia", "languageLabel": "Język", - "languageUpdated": "Zaktualizowano język", + "languageUpdated": "Zmieniono język", "sourcesLabel": "Alternatywne źródło", "sourcesLabelHint": "Skonfiguruj alternatywne źródła dla łatek ReVanced", "useAlternativeSources": "Używaj alternatywnych źródeł", diff --git a/assets/i18n/pt_BR.i18n.json b/assets/i18n/pt_BR.i18n.json index 3d332fe9..97c0ffbf 100755 --- a/assets/i18n/pt_BR.i18n.json +++ b/assets/i18n/pt_BR.i18n.json @@ -180,6 +180,7 @@ "disablePatchesSelectionWarningText": "Você irá desabilitar a mudança da pré-seleção dos patches.\nA seleção padrão dos patches será restaurada.\n\nDesabilitar mesmo assim?", "autoUpdatePatchesLabel": "Atualizar patches automaticamente", "autoUpdatePatchesHint": "Atualize automaticamente os patches para a versão mais recente", + "usePrereleasesLabel": "Usar versões preliminares", "showUpdateDialogLabel": "Mostrar alerta de atualização", "showUpdateDialogHint": "Mostra um aviso quando uma nova atualização estiver disponível", "universalPatchesLabel": "Mostrar patches universais", diff --git a/assets/i18n/ru_RU.i18n.json b/assets/i18n/ru_RU.i18n.json index 98b59b2c..7d746c1a 100755 --- a/assets/i18n/ru_RU.i18n.json +++ b/assets/i18n/ru_RU.i18n.json @@ -180,6 +180,9 @@ "disablePatchesSelectionWarningText": "Вы собираетесь выключить измененную выборку патчей.\nБудет восстановлен стандартный выбор патчей.\n\nВсе равно выключить?", "autoUpdatePatchesLabel": "Автообновление патчей", "autoUpdatePatchesHint": "Автоматически обновлять патчи до последней версии", + "usePrereleasesLabel": "Использовать предварительные версии", + "usePrereleasesHint": "Использовать предварительные версии ReVanced Manager и ReVanced Patches", + "usePrereleasesWarningText": "Использование предварительных версий может привести к непредвиденным проблемам.\n\nВключить в любом случае?", "showUpdateDialogLabel": "Показать диалог обновления", "showUpdateDialogHint": "Показывать диалоговое окно, когда доступно новое обновление", "universalPatchesLabel": "Показать универсальные патчи", diff --git a/assets/i18n/sl_SI.i18n.json b/assets/i18n/sl_SI.i18n.json index 5750d4e8..023f01a3 100755 --- a/assets/i18n/sl_SI.i18n.json +++ b/assets/i18n/sl_SI.i18n.json @@ -8,7 +8,7 @@ "yesButton": "Da", "noButton": "Ne", "warning": "Opozorilo", - "notice": "Notice", + "notice": "Obvestilo", "noShowAgain": "Ne prikazuj več", "add": "Dodaj", "remove": "Odstrani", diff --git a/assets/i18n/sr_CS.i18n.json b/assets/i18n/sr_CS.i18n.json index 1b6acf10..69c39616 100755 --- a/assets/i18n/sr_CS.i18n.json +++ b/assets/i18n/sr_CS.i18n.json @@ -180,6 +180,9 @@ "disablePatchesSelectionWarningText": "Upravo ćete da onemogućite promenu izbora pečeva.\nPodrazumevani izbor pečeva će biti vraćen.\n\nIpak onemogućiti?", "autoUpdatePatchesLabel": "Automatski ažuriraj pečeve", "autoUpdatePatchesHint": "Instalira najnoviju verziju pečeva automatski", + "usePrereleasesLabel": "Koristi predizdanja", + "usePrereleasesHint": "Korišćenje verzije predizdanja ReVanced Managera i ReVanced pečeva", + "usePrereleasesWarningText": "Korišćenje verzija predizdanja može izazvati neočekivane probleme.\n\nIpak omogućiti?", "showUpdateDialogLabel": "Prikaži dijalog o ažuriranju", "showUpdateDialogHint": "Prikazivanje dijaloga kada je novo ažuriranje dostupno", "universalPatchesLabel": "Prikaži univerzalne pečeve", diff --git a/assets/i18n/sr_SP.i18n.json b/assets/i18n/sr_SP.i18n.json index f6cbbe15..241239cd 100755 --- a/assets/i18n/sr_SP.i18n.json +++ b/assets/i18n/sr_SP.i18n.json @@ -180,6 +180,9 @@ "disablePatchesSelectionWarningText": "Управо ћете да онемогућите промену избора печева.\nПодразумевани избор печева ће бити враћен.\n\nИпак онемогућити?", "autoUpdatePatchesLabel": "Аутоматски ажурирај печеве", "autoUpdatePatchesHint": "Инсталира најновију верзију печева аутоматски", + "usePrereleasesLabel": "Користи предиздања", + "usePrereleasesHint": "Коришћење верзија предиздања ReVanced Manager-а и ReVanced печева", + "usePrereleasesWarningText": "Коришћење верзија предиздања може изазвати неочекиване проблеме.\n\nИпак омогућити?", "showUpdateDialogLabel": "Прикажи дијалог о ажурирању", "showUpdateDialogHint": "Приказивање дијалога када је ново ажурирање доступно", "universalPatchesLabel": "Прикажи универзалне печеве", diff --git a/assets/i18n/th_TH.i18n.json b/assets/i18n/th_TH.i18n.json index ac965676..3bb9c477 100755 --- a/assets/i18n/th_TH.i18n.json +++ b/assets/i18n/th_TH.i18n.json @@ -180,6 +180,7 @@ "disablePatchesSelectionWarningText": "คุณกำลังจะปิดใช้งานการเปลี่ยนการเลือกการแก้ไข\nการเลือกการแก้ไขเริ่มต้นจะถูกคืนค่า\n\nปิดใช้งานอยู่ดี?", "autoUpdatePatchesLabel": "อัปเดตการแก้ไขโดยอัตโนมัติ", "autoUpdatePatchesHint": "อัปเดตการแก้ไขเป็นเวอร์ชันล่าสุดโดยอัตโนมัติ", + "usePrereleasesLabel": "ใช้รุ่นก่อนเผยแพร่", "showUpdateDialogLabel": "แสดงกล่องโต้ตอบการอัปเดต", "showUpdateDialogHint": "แสดงกล่องโต้ตอบเมื่อมีการอัปเดตใหม่", "universalPatchesLabel": "แสดงการแก้ไขแบบสากล", diff --git a/assets/i18n/tr_TR.i18n.json b/assets/i18n/tr_TR.i18n.json index dae99b1b..26c7089a 100755 --- a/assets/i18n/tr_TR.i18n.json +++ b/assets/i18n/tr_TR.i18n.json @@ -180,6 +180,9 @@ "disablePatchesSelectionWarningText": "Yama seçimini değiştirmeyi devre dışı bırakmak üzeresiniz.\nVarsayılan yama seçimi geri yüklenecektir.\n\nYine de devre dışı bırakılsın mı?", "autoUpdatePatchesLabel": "Yamaları otomatik güncelle", "autoUpdatePatchesHint": "Yamaları otomatik olarak en son sürüme güncelle", + "usePrereleasesLabel": "Ön sürümleri kullan", + "usePrereleasesHint": "ReVanced Manager ve ReVanced Yamaları'nın ön sürüm versiyonlarını kullan", + "usePrereleasesWarningText": "Ön sürüm versiyonlarını kullanmak beklenmedik sorunlara neden olabilir.\n\nYine de etkinleştirmek istiyor musunuz?", "showUpdateDialogLabel": "Güncelleme penceresini göster", "showUpdateDialogHint": "Yeni bir güncelleme mevcut olduğunda bir pencere göster", "universalPatchesLabel": "Ortak yamaları göster", diff --git a/assets/i18n/uk_UA.i18n.json b/assets/i18n/uk_UA.i18n.json index ffd9589a..785c58d4 100755 --- a/assets/i18n/uk_UA.i18n.json +++ b/assets/i18n/uk_UA.i18n.json @@ -180,6 +180,9 @@ "disablePatchesSelectionWarningText": "Ви збираєтеся вимкнути зміну вибору патчів.\nБуде відновлено стандартний вибір патчів.\n\nВсе одно вимкнути?", "autoUpdatePatchesLabel": "Автоматичне оновлення патчів", "autoUpdatePatchesHint": "Автоматично оновлювати патчі до останньої версії", + "usePrereleasesLabel": "Використовувати передрелізи", + "usePrereleasesHint": "Використовувати передрелізні версії ReVanced Manager та ReVanced Patches", + "usePrereleasesWarningText": "Використання передрелізних версій може спричинити несподівані проблеми.\n\nВсе одно увімкнути?", "showUpdateDialogLabel": "Показувати вікно оновлення", "showUpdateDialogHint": "Показувати діалогове вікно, коли доступне нове оновлення", "universalPatchesLabel": "Показувати універсальні патчі", diff --git a/assets/i18n/vi_VN.i18n.json b/assets/i18n/vi_VN.i18n.json index 84f31823..cff4e3d4 100755 --- a/assets/i18n/vi_VN.i18n.json +++ b/assets/i18n/vi_VN.i18n.json @@ -1,7 +1,7 @@ { "okButton": "Đồng ý", "cancelButton": "Hủy", - "dismissButton": "Từ bỏ", + "dismissButton": "Đóng", "quitButton": "Thoát", "updateButton": "Cập nhật", "suggested": "Được đề xuất: ${version}", @@ -9,7 +9,7 @@ "noButton": "Không", "warning": "Cảnh báo", "notice": "Lưu ý", - "noShowAgain": "Không hiển thị lại điều này", + "noShowAgain": "Không hiển thị lại nội dung này", "add": "Thêm", "remove": "Loại bỏ", "showChangelogButton": "Hiển thị nhật ký thay đổi", @@ -25,7 +25,7 @@ "updatesSubtitle": "Các bản cập nhật", "lastPatchedAppSubtitle": "Ứng dụng đã vá lần cuối", "patchedSubtitle": "Các ứng dụng đã cài đặt", - "changeLaterSubtitle": "Bạn có thể thay đổi cài đặt này sau.", + "changeLaterSubtitle": "Bạn có thể thay đổi cài đặt này sau đó.", "noSavedAppFound": "Không tìm thấy ứng dụng", "noInstallations": "Không có ứng dụng đã vá nào được cài đặt", "installUpdate": "Tiếp tục cài đặt bản cập nhật?", @@ -33,7 +33,7 @@ "updateDialogTitle": "Có bản cập nhật mới", "updatePatchesSheetTitle": "Cập nhật Các bản vá ReVanced", "updateChangelogTitle": "Nhật ký thay đổi", - "updateDialogText": "Có một bản cập nhật cho ${file}.\n\nPhiên bản đã cài hiện tại là ${version}.", + "updateDialogText": "${file} đã có bản cập nhật mới.\n\nBạn đang dùng phiên bản ${version}.", "downloadConsentDialogTitle": "Tải các tập tin cần thiết?", "downloadConsentDialogText": "ReVanced Manager cần tải các tập tin cần thiết để hoạt động đúng cách.", "downloadConsentDialogText2": "Điều này sẽ kết nối bạn đến ${url}.", @@ -54,7 +54,7 @@ "patcherView": { "widgetTitle": "Trình vá", "patchButton": "Vá", - "incompatibleArchWarningDialogText": "Việc vá ứng dụng trên kiến ​​trúc này hiện chưa được hỗ trợ và có thể thất bại. Bạn vẫn muốn tiếp tục?", + "incompatibleArchWarningDialogText": "Việc vá trên kiến ​​trúc này hiện chưa được hỗ trợ và có thể không thành công. Vẫn tiếp tục dù sao?", "removedPatchesWarningDialogText": "Các bản vá đã loại bỏ từ lần cuối cùng bạn vá ứng dụng này:\n\n${patches}\n\n${newPatches}Vẫn tiếp tục?", "addedPatchesDialogText": "Các bản vá đã thêm vào từ lần cuối cùng bạn vá ứng dụng này:\n\n${addedPatches}", "requiredOptionDialogText": "Một số tùy chọn bản vá cần được thiết đặt." @@ -83,9 +83,9 @@ "selectFromStorageButton": "Chọn từ bộ nhớ", "errorMessage": "Không thể dùng ứng dụng đã chọn", "downloadToast": "Tính năng tải về chưa khả dụng", - "requireSuggestedAppVersionDialogText": "Phiên bản của ứng dụng bạn đã chọn không trùng khớp vói phiên bản được đề xuất có thể dẫn đến các phát sinh không mong muốn. Xin chọn ứng dụng khớp với phiên bản được đề xuất.\n\nPhiên bản đã chọn: ${selected}\nPhiên bản được đề xuất: ${suggested}\n\nĐể tiếp tục, tắt \"Yêu cầu phiên bản ứng dụng được đề xuất\" trong cài đặt.", + "requireSuggestedAppVersionDialogText": "Phiên bản của ứng dụng bạn đã chọn không khớp với phiên bản được đề xuất có thể dẫn đến các sự cố không mong muốn. Vui lòng sử dụng phiên bản đề xuất.\n\nPhiên bản đã chọn: ${selected}\nPhiên bản được đề xuất: ${suggested}\n\nĐể tiếp tục, hãy tắt \"Yêu cầu phiên bản ứng dụng được đề xuất\" trong cài đặt.", "featureNotAvailable": "Tính năng chưa triển khai", - "featureNotAvailableText": "Ứng dụng này là một APK tách rời và chỉ có thể được vá và cài một cách tin cậy bằng cách gắn kết với quyền root. Tuy nhiên, bạn có thể vá và cài APK đầy đủ bằng cách chọn chúng từ lưu trữ." + "featureNotAvailableText": "Ứng dụng này là một APK tách rời và chỉ có thể được vá và cài đặt một cách tin cậy bằng cách gắn kết với quyền root. Tuy nhiên, bạn có thể vá và cài đặt APK đầy đủ bằng cách chọn chúng từ kho lưu trữ." }, "patchesSelectorView": { "viewTitle": "Chọn bản vá", @@ -99,7 +99,7 @@ "noneChip": "Không có", "noneTooltip": "Bỏ chọn tất cả bản vá", "loadPatchesSelection": "Nạp các bản vá được chọn", - "noSavedPatches": "Không có bản vá cho ứng dụng được chọn\nNhấn Hoàn tất để lưu lựa chọn hiện tại.", + "noSavedPatches": "Không có bản vá nào được lưu cho ứng dụng đã chọn.\nNhấn Hoàn tất để lưu lựa chọn hiện tại.", "noPatchesFound": "Không tìm thấy bản vá cho ứng dụng đã chọn", "setRequiredOption": "Một số bản vá yêu cầu thiết đặt tùy chọn:\n\n${patches}\n\nXin thiết đặt chúng trước khi tiếp tục." }, @@ -121,12 +121,12 @@ "unsupportedDialogText": "Chọn bản vá này có thể gây lỗi khi vá.\n\nPhiên bản ứng dụng: ${packageVersion}\nPhiên bản được hỗ trợ: ${supportedVersions}", "unsupportedPatchVersion": "Bản vá không được hỗ trợ cho phiên bản ứng dụng này.", "unsupportedRequiredOption": "Bản vá này chứa một tùy chọn bắt buộc không được hỗ trợ bởi ứng dụng này", - "patchesChangeWarningDialogText": "Bạn nên sử dụng lựa chọn bản vá mặc định và các tùy chọn. Thay đổi chúng có thể dẫn đến các vấn đề không mong muốn.\n\nBạn cần bật \"Cho phép thay đổi lựa chọn đường dẫn\" trong cài đặt trước khi thay đổi bất kỳ lựa chọn đường dẫn nào.", + "patchesChangeWarningDialogText": "Nên sử dụng các tùy chọn và lựa chọn bản vá mặc định. Thay đổi chúng có thể dẫn đến các sự cố không mong muốn.\n\nBạn cần bật \"Cho phép thay đổi lựa chọn bản vá\" trong cài đặt trước khi thay đổi bất kỳ lựa chọn bản vá nào.", "patchesChangeWarningDialogButton": "Dùng lựa chọn mặc định" }, "installerView": { "installType": "Chọn kiểu cài đặt", - "installTypeDescription": "Chọn kiểu cài đặt để thực hiện với nó.", + "installTypeDescription": "Chọn kiểu cài đặt để tiếp tục.", "installButton": "Cài đặt", "installRootType": "Gắn kết", "installNonRootType": "Thông thường", @@ -137,8 +137,8 @@ "notificationText": "Nhấn để trở lại trình cài đặt", "exportApkButtonTooltip": "Xuất APK đã vá", "exportLogButtonTooltip": "Xuất nhật ký", - "screenshotDetected": "Một ảnh chụp màn hình đã được phát hiện. Nếu bạn đang cố chia sẻ nhật ký, xin thay bằng chia sẻ văn bản sao chép.\n\nSao chép nhật ký vào bảng tạm?", - "copiedToClipboard": "Đã sao chép nhật ký vào bảng tạm", + "screenshotDetected": "Đã phát hiện ảnh chụp màn hình. Nếu bạn đang cố chia sẻ nhật ký, vui lòng thay thế bằng chia sẻ văn bản sao chép.\n\nSao chép nhật ký vào bảng nhớ tạm?", + "copiedToClipboard": "Đã sao chép nhật ký vào bảng nhớ tạm", "noExit": "Trình cài đặt vẫn đang chạy, không thể thoát..." }, "settingsView": { @@ -152,15 +152,15 @@ "themeModeLabel": "Chủ đề ứng dụng", "systemThemeLabel": "Hệ thống", "lightThemeLabel": "Sáng", - "darkThemeLabel": "Chế độ tối", + "darkThemeLabel": "Tối", "dynamicThemeLabel": "Cá nhân", "dynamicThemeHint": "Tận hưởng trải nghiệm gần hơn với thiết bị của bạn", "languageLabel": "Ngôn ngữ", - "languageUpdated": "Ngôn ngữ đã cập nhập", + "languageUpdated": "Ngôn ngữ đã được cập nhập", "sourcesLabel": "Nguồn thay thế", - "sourcesLabelHint": "Cấu hình nguồn thay thế cho Bản vá ReVanced", + "sourcesLabelHint": "Cấu hình nguồn thay thế cho ReVanced Patches", "useAlternativeSources": "Dùng nguồn thay thế", - "useAlternativeSourcesHint": "Dùng nguồn thay thế cho Bản vá ReVanced thay cho API", + "useAlternativeSourcesHint": "Dùng nguồn thay thế cho ReVanced Patches thay vì dùng API", "sourcesResetDialogTitle": "Đặt lại", "sourcesResetDialogText": "Bạn có chắc chắn muốn đặt lại nguồn của mình về giá trị mặc định không?", "apiURLResetDialogText": "Bạn có chắc bạn muốn đặt lại API URL của bạn về giá trị mặc định của nó không?", @@ -175,11 +175,13 @@ "logsLabel": "Chia sẻ nhật ký", "logsHint": "Chia sẻ nhật ký ReVanced Manager", "enablePatchesSelectionLabel": "Cho phép thay đổi lựa chọn bản vá", - "enablePatchesSelectionHint": "Không ngăn chặn việc chọn hoặc bỏ chọn các bản vá", - "enablePatchesSelectionWarningText": "Thay đổi lựa chọn mặc định của các bản vá có thể gây vấn đề không ngờ tới.\n\nVẫn bật?", + "enablePatchesSelectionHint": "Không ngăn chặn chọn hoặc bỏ chọn các bản vá", + "enablePatchesSelectionWarningText": "Thay đổi lựa chọn mặc định của các bản vá có thể gây ra các sự cố không mong muốn.\n\nVẫn tiếp tục bật?", "disablePatchesSelectionWarningText": "Bạn chuẩn bị tắt thay đổi lựa chọn các bản vá.\nLựa chọn mặc định các bản vá sẽ được khôi phục.\n\nVẫn tắt?", "autoUpdatePatchesLabel": "Tự động cập nhật các bản vá", "autoUpdatePatchesHint": "Tự động cập nhật các bản vá lên phiên bản mới nhất", + "usePrereleasesLabel": "Sử dụng bản phát hành trước", + "usePrereleasesWarningText": "Sử dụng các phiên bản phát hành trước có thể gây ra các sự cố không mong muốn.\n\nVẫn tiếp tục bật?", "showUpdateDialogLabel": "Hiện hộp thoại cập nhật", "showUpdateDialogHint": "Hiện một hộp thoại khi có một bản cập nhật", "universalPatchesLabel": "Các bản vá phổ quát", @@ -290,8 +292,8 @@ "status_failure_invalid_description": "Việc cài đặt đã thất bại do ứng dụng đã vá không hợp lệ.\n\nGỡ cài đặt ứng dụng và thử lại?", "status_failure_incompatible_description": "Ứng dụng không tương thích với thiết bị này.\n\nDùng một APK được hỗ trợ bởi thiết bị và thử lại.", "status_failure_conflict_description": "Việc cài đặt đã bị ngăn chặn bởi bản cài đã có của ứng dụng.\n\nGỡ cài đặt ứng dụng đã cài và thử lại?", - "status_failure_blocked_description": "Việc cài đặt đã đã bị chặn bởi ${packageName}.\n\nĐiều chỉnh thiết lập bảo mật của bạn và thử lại.", - "install_failed_verification_failure_description": "Việc cài đặt đã thất bại do phát sinh xác minh.\n\nĐiều chỉnh thiết lập bảo mật của bạn và thử lại.", + "status_failure_blocked_description": "Quá trình cài đặt đã bị chặn bởi ${packageName}.\n\nĐiều chỉnh cài đặt bảo mật của bạn và thử lại.", + "install_failed_verification_failure_description": "Quá trình cài đặt không thành công do gặp lỗi xác minh.\n\nĐiều chỉnh cài đặt bảo mật của bạn và thử lại.", "install_failed_version_downgrade_description": "Việc cài đặt đã thất bại do ứng dụng đã vá có phiên bản nhỏ hơn ứng dụng đã cài đặt.\n\nGỡ cài đặt ứng dụng và thử lại?", "status_unknown_description": "Việc cài đặt đã thất bại do một lý do chưa rõ. Xin thử lại." } diff --git a/assets/i18n/zh_TW.i18n.json b/assets/i18n/zh_TW.i18n.json index 9e874f22..9d72a720 100755 --- a/assets/i18n/zh_TW.i18n.json +++ b/assets/i18n/zh_TW.i18n.json @@ -27,7 +27,7 @@ "patchedSubtitle": "已安裝應用程式", "changeLaterSubtitle": "你稍後可以在設定中變更此選項。", "noSavedAppFound": "找不到應用程式", - "noInstallations": "未安裝已修補的應用程式", + "noInstallations": "未安裝已修補應用程式", "installUpdate": "是否要繼續更新?", "updateSheetTitle": "更新 ReVanced Manager", "updateDialogTitle": "有可用的更新", @@ -99,7 +99,7 @@ "noneChip": "無", "noneTooltip": "取消選取修補檔", "loadPatchesSelection": "載入修補檔選項", - "noSavedPatches": "所選的應用程式沒有已儲存的修補選項。\n按下「完成」以儲存目前的選擇。", + "noSavedPatches": "所選的應用程式沒有已儲存的修補選項。\n按下「完成」以儲存目前的修補選項。", "noPatchesFound": "找不到適合所選應用程式的修補檔", "setRequiredOption": "某些修補檔選項需要進行設定:\n\n${patches}\n\n請在繼續之前進行設定。" }, From 8e4a9088ead600d2394e949db8dad8e661cd0580 Mon Sep 17 00:00:00 2001 From: oSumAtrIX Date: Mon, 2 Jun 2025 16:05:59 +0200 Subject: [PATCH 06/19] ci: Use install instead of clean install for NPM dependencies Co-authored-by: Pun Butrach --- .github/workflows/release.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 0050159a..1c3a2b45 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -45,7 +45,7 @@ jobs: build-root-directory: ${{ github.workspace }}/android - name: Install dependencies - run: npm ci + run: npm i - name: Get dependencies run: flutter pub get From 83fc7f131a8d25acac0102d4f8e8d0a0e1cf9b3a Mon Sep 17 00:00:00 2001 From: Ushie Date: Mon, 2 Jun 2025 17:12:50 +0300 Subject: [PATCH 07/19] chore: Remove obsolete deleteLastPatchedApp call --- lib/services/manager_api.dart | 1 - 1 file changed, 1 deletion(-) diff --git a/lib/services/manager_api.dart b/lib/services/manager_api.dart index e8119917..26d950a0 100644 --- a/lib/services/manager_api.dart +++ b/lib/services/manager_api.dart @@ -378,7 +378,6 @@ class ManagerAPI { PatchedApplication app, File outFile ) async { - deleteLastPatchedApp(); final Directory appCache = await getApplicationSupportDirectory(); app.patchedFilePath = outFile.copySync('${appCache.path}/lastPatchedApp.apk').path; From b70fc03bc7381ee5c584f82e1323976741e108c7 Mon Sep 17 00:00:00 2001 From: brosssh <44944126+brosssh@users.noreply.github.com> Date: Sat, 5 Jul 2025 18:44:11 +0200 Subject: [PATCH 08/19] fix: Allow different app version when downloading via plugin if setting is off (#2579) Co-authored-by: Ax333l --- .../manager/domain/repository/DownloadedAppRepository.kt | 9 ++++++++- .../app/revanced/manager/patcher/worker/PatcherWorker.kt | 2 ++ 2 files changed, 10 insertions(+), 1 deletion(-) diff --git a/app/src/main/java/app/revanced/manager/domain/repository/DownloadedAppRepository.kt b/app/src/main/java/app/revanced/manager/domain/repository/DownloadedAppRepository.kt index b4598fb9..e8536de1 100644 --- a/app/src/main/java/app/revanced/manager/domain/repository/DownloadedAppRepository.kt +++ b/app/src/main/java/app/revanced/manager/domain/repository/DownloadedAppRepository.kt @@ -40,6 +40,8 @@ class DownloadedAppRepository( data: Parcelable, expectedPackageName: String, expectedVersion: String?, + appCompatibilityCheck: Boolean, + patchesCompatibilityCheck: Boolean, onDownload: suspend (downloadProgress: Pair) -> Unit, ): File { // Converted integers cannot contain / or .. unlike the package name or version, so they are safer to use here. @@ -96,7 +98,12 @@ class DownloadedAppRepository( val pkgInfo = pm.getPackageInfo(targetFile.toFile()) ?: error("Downloaded APK file is invalid") if (pkgInfo.packageName != expectedPackageName) error("Downloaded APK has the wrong package name. Expected: $expectedPackageName, Actual: ${pkgInfo.packageName}") - if (expectedVersion != null && pkgInfo.versionName != expectedVersion) error("Downloaded APK has the wrong version. Expected: $expectedVersion, Actual: ${pkgInfo.versionName}") + expectedVersion?.let { + if ( + pkgInfo.versionName != expectedVersion && + (appCompatibilityCheck || patchesCompatibilityCheck) + ) error("The selected app version ($pkgInfo.versionName) doesn't match the suggested version. Please use the suggested version ($expectedVersion), or adjust your settings by disabling \"Require suggested app version\" and enabling \"Disable version compatibility check\".") + } // Delete the previous copy (if present). dao.get(pkgInfo.packageName, pkgInfo.versionName!!)?.directory?.let { diff --git a/app/src/main/java/app/revanced/manager/patcher/worker/PatcherWorker.kt b/app/src/main/java/app/revanced/manager/patcher/worker/PatcherWorker.kt index 5096170c..90687420 100644 --- a/app/src/main/java/app/revanced/manager/patcher/worker/PatcherWorker.kt +++ b/app/src/main/java/app/revanced/manager/patcher/worker/PatcherWorker.kt @@ -158,6 +158,8 @@ class PatcherWorker( data, args.packageName, args.input.version, + prefs.suggestedVersionSafeguard.get(), + !prefs.disablePatchVersionCompatCheck.get(), onDownload = args.onDownloadProgress ).also { args.setInputFile(it) From baa9122a882b0e8a5e362bc68e7420df8c77c32f Mon Sep 17 00:00:00 2001 From: kitadai31 <90122968+kitadai31@users.noreply.github.com> Date: Tue, 8 Jul 2025 05:38:41 +0900 Subject: [PATCH 09/19] fix: Improve background running notification (#2614) --- .../manager/patcher/worker/PatcherWorker.kt | 23 +++++++++++-------- app/src/main/res/values/strings.xml | 3 ++- 2 files changed, 15 insertions(+), 11 deletions(-) diff --git a/app/src/main/java/app/revanced/manager/patcher/worker/PatcherWorker.kt b/app/src/main/java/app/revanced/manager/patcher/worker/PatcherWorker.kt index 90687420..0708817d 100644 --- a/app/src/main/java/app/revanced/manager/patcher/worker/PatcherWorker.kt +++ b/app/src/main/java/app/revanced/manager/patcher/worker/PatcherWorker.kt @@ -14,9 +14,9 @@ import android.os.Parcelable import android.os.PowerManager import android.util.Log import androidx.activity.result.ActivityResult -import androidx.core.content.ContextCompat import androidx.work.ForegroundInfo import androidx.work.WorkerParameters +import app.revanced.manager.MainActivity import app.revanced.manager.R import app.revanced.manager.data.platform.Filesystem import app.revanced.manager.data.room.apps.installed.InstallType @@ -88,22 +88,25 @@ class PatcherWorker( ) private fun createNotification(): Notification { - val notificationIntent = Intent(applicationContext, PatcherWorker::class.java) - val pendingIntent: PendingIntent = PendingIntent.getActivity( + val notificationIntent = Intent(applicationContext, MainActivity::class.java).apply { + flags = Intent.FLAG_ACTIVITY_SINGLE_TOP or Intent.FLAG_ACTIVITY_CLEAR_TOP + } + val pendingIntent = PendingIntent.getActivity( applicationContext, 0, notificationIntent, PendingIntent.FLAG_IMMUTABLE ) val channel = NotificationChannel( - "revanced-patcher-patching", "Patching", NotificationManager.IMPORTANCE_HIGH + "revanced-patcher-patching", "Patching", NotificationManager.IMPORTANCE_LOW ) val notificationManager = - ContextCompat.getSystemService(applicationContext, NotificationManager::class.java) - notificationManager!!.createNotificationChannel(channel) + applicationContext.getSystemService(NotificationManager::class.java) + notificationManager.createNotificationChannel(channel) return Notification.Builder(applicationContext, channel.id) - .setContentTitle(applicationContext.getText(R.string.app_name)) - .setContentText(applicationContext.getText(R.string.patcher_notification_message)) - .setLargeIcon(Icon.createWithResource(applicationContext, R.drawable.ic_notification)) + .setContentTitle(applicationContext.getText(R.string.patcher_notification_title)) + .setContentText(applicationContext.getText(R.string.patcher_notification_text)) .setSmallIcon(Icon.createWithResource(applicationContext, R.drawable.ic_notification)) - .setContentIntent(pendingIntent).build() + .setContentIntent(pendingIntent) + .setCategory(Notification.CATEGORY_SERVICE) + .build() } override suspend fun doWork(): Result { diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index 2148b082..253de2df 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -317,7 +317,8 @@ Saving Write patched APK file Sign patched APK file - Patching in progress… + Patching in progress… + Tap to return to the patcher Stop patcher Are you sure you want to stop the patching process? Execute patches From 979a2dc410db1c7b30bdb0f5a440edc5cd6c17f1 Mon Sep 17 00:00:00 2001 From: Pun Butrach Date: Tue, 8 Jul 2025 18:11:45 +0700 Subject: [PATCH 10/19] fix: Playback Switch's Haptic Feedback (#2639) Signed-off-by: Pun Butrach --- .../revanced/manager/ui/component/haptics/HapticSwitch.kt | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/app/src/main/java/app/revanced/manager/ui/component/haptics/HapticSwitch.kt b/app/src/main/java/app/revanced/manager/ui/component/haptics/HapticSwitch.kt index c2491397..f1e26e2c 100644 --- a/app/src/main/java/app/revanced/manager/ui/component/haptics/HapticSwitch.kt +++ b/app/src/main/java/app/revanced/manager/ui/component/haptics/HapticSwitch.kt @@ -9,6 +9,7 @@ import androidx.compose.material3.SwitchDefaults import androidx.compose.runtime.Composable import androidx.compose.runtime.remember import androidx.compose.ui.Modifier +import androidx.compose.ui.platform.LocalView @Composable fun HapticSwitch( @@ -20,16 +21,19 @@ fun HapticSwitch( colors: SwitchColors = SwitchDefaults.colors(), interactionSource: MutableInteractionSource = remember { MutableInteractionSource() }, ) { + val view = LocalView.current Switch( checked = checked, onCheckedChange = { newChecked -> val useNewConstants = Build.VERSION.SDK_INT >= Build.VERSION_CODES.UPSIDE_DOWN_CAKE - when { + val hapticFeedbackType = when { newChecked && useNewConstants -> HapticFeedbackConstants.TOGGLE_ON newChecked -> HapticFeedbackConstants.VIRTUAL_KEY !newChecked && useNewConstants -> HapticFeedbackConstants.TOGGLE_OFF !newChecked -> HapticFeedbackConstants.CLOCK_TICK + else -> {HapticFeedbackConstants.VIRTUAL_KEY} } + view.performHapticFeedback(hapticFeedbackType) onCheckedChange(newChecked) }, modifier = modifier, From 8c6c0f3c76175ae4386ee75305f95fe4416c2c26 Mon Sep 17 00:00:00 2001 From: brosssh <44944126+brosssh@users.noreply.github.com> Date: Tue, 8 Jul 2025 17:20:44 +0200 Subject: [PATCH 11/19] fix: Patch selection screen padding (#2533) --- .../revanced/manager/ui/component/bundle/ImportBundleDialog.kt | 2 +- .../app/revanced/manager/ui/screen/PatchesSelectorScreen.kt | 1 + 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/app/src/main/java/app/revanced/manager/ui/component/bundle/ImportBundleDialog.kt b/app/src/main/java/app/revanced/manager/ui/component/bundle/ImportBundleDialog.kt index 37d9ed1a..821a2669 100644 --- a/app/src/main/java/app/revanced/manager/ui/component/bundle/ImportBundleDialog.kt +++ b/app/src/main/java/app/revanced/manager/ui/component/bundle/ImportBundleDialog.kt @@ -210,7 +210,7 @@ fun ImportBundleStep( ) } Column( - modifier = Modifier.padding(horizontal = 8.dp) + modifier = Modifier.padding(horizontal = 8.dp, vertical = 5.dp) ) { ListItem( modifier = Modifier.clickable( diff --git a/app/src/main/java/app/revanced/manager/ui/screen/PatchesSelectorScreen.kt b/app/src/main/java/app/revanced/manager/ui/screen/PatchesSelectorScreen.kt index 6c732d7b..2bb29e4a 100644 --- a/app/src/main/java/app/revanced/manager/ui/screen/PatchesSelectorScreen.kt +++ b/app/src/main/java/app/revanced/manager/ui/screen/PatchesSelectorScreen.kt @@ -389,6 +389,7 @@ fun PatchesSelectorScreen( modifier = Modifier .fillMaxSize() .padding(paddingValues) + .padding(top = 16.dp) ) { if (bundles.size > 1) { ScrollableTabRow( From cb2dbbee242392df3529e8c0daac0bcde1edcff9 Mon Sep 17 00:00:00 2001 From: brosssh <44944126+brosssh@users.noreply.github.com> Date: Tue, 8 Jul 2025 20:23:03 +0200 Subject: [PATCH 12/19] feat: Improve bundle info screen design (#2548) --- .../domain/bundles/LocalPatchBundle.kt | 2 +- .../domain/bundles/PatchBundleSource.kt | 4 +- .../manager/patcher/patch/PatchBundle.kt | 28 +++++- .../ui/component/bundle/BaseBundleDialog.kt | 99 +++++++++++-------- .../bundle/BundleInformationDialog.kt | 7 +- .../revanced/manager/ui/model/BundleInfo.kt | 4 +- app/src/main/res/values/strings.xml | 1 + 7 files changed, 93 insertions(+), 52 deletions(-) diff --git a/app/src/main/java/app/revanced/manager/domain/bundles/LocalPatchBundle.kt b/app/src/main/java/app/revanced/manager/domain/bundles/LocalPatchBundle.kt index 2fcac8d3..a940e05d 100644 --- a/app/src/main/java/app/revanced/manager/domain/bundles/LocalPatchBundle.kt +++ b/app/src/main/java/app/revanced/manager/domain/bundles/LocalPatchBundle.kt @@ -15,7 +15,7 @@ class LocalPatchBundle(name: String, id: Int, directory: File) : } reload()?.also { - saveVersionHash(it.readManifestAttribute("Version")) + saveVersionHash(it.patchBundleManifestAttributes?.version) } } } diff --git a/app/src/main/java/app/revanced/manager/domain/bundles/PatchBundleSource.kt b/app/src/main/java/app/revanced/manager/domain/bundles/PatchBundleSource.kt index ee46cc71..1bc8a7f8 100644 --- a/app/src/main/java/app/revanced/manager/domain/bundles/PatchBundleSource.kt +++ b/app/src/main/java/app/revanced/manager/domain/bundles/PatchBundleSource.kt @@ -38,7 +38,7 @@ sealed class PatchBundleSource(initialName: String, val uid: Int, directory: Fil suspend fun getName() = nameFlow.first() - val versionFlow = state.map { it.patchBundleOrNull()?.readManifestAttribute("Version") } + val versionFlow = state.map { it.patchBundleOrNull()?.patchBundleManifestAttributes?.version } val patchCountFlow = state.map { it.patchBundleOrNull()?.patches?.size ?: 0 } /** @@ -74,7 +74,7 @@ sealed class PatchBundleSource(initialName: String, val uid: Int, directory: Fil val bundle = newState.patchBundleOrNull() // Try to read the name from the patch bundle manifest if the bundle does not have a name. if (bundle != null && _nameFlow.value.isEmpty()) { - bundle.readManifestAttribute("Name")?.let { setName(it) } + bundle.patchBundleManifestAttributes?.name?.let { setName(it) } } return bundle diff --git a/app/src/main/java/app/revanced/manager/patcher/patch/PatchBundle.kt b/app/src/main/java/app/revanced/manager/patcher/patch/PatchBundle.kt index 2b93a829..e894f748 100644 --- a/app/src/main/java/app/revanced/manager/patcher/patch/PatchBundle.kt +++ b/app/src/main/java/app/revanced/manager/patcher/patch/PatchBundle.kt @@ -8,6 +8,17 @@ import java.io.File import java.io.IOException import java.util.jar.JarFile +class PatchBundleManifestAttributes( + val name: String?, + val version: String?, + val description: String?, + val source: String?, + val author: String?, + val contact: String?, + val website: String?, + val license: String? +) + class PatchBundle(val patchesJar: File) { private val loader = object : Iterable> { private fun load(): Iterable> { @@ -36,7 +47,20 @@ class PatchBundle(val patchesJar: File) { null } - fun readManifestAttribute(name: String) = manifest?.mainAttributes?.getValue(name) + val patchBundleManifestAttributes = if(manifest != null) + PatchBundleManifestAttributes( + name = readManifestAttribute("name"), + version = readManifestAttribute("version"), + description = readManifestAttribute("description"), + source = readManifestAttribute("source"), + author = readManifestAttribute("author"), + contact = readManifestAttribute("contact"), + website = readManifestAttribute("website"), + license = readManifestAttribute("license") + ) else + null + + private fun readManifestAttribute(name: String) = manifest?.mainAttributes?.getValue(name)?.takeIf { it.isNotBlank() } // If empty, set it to null instead. /** * Load all patches compatible with the specified package. @@ -53,4 +77,4 @@ class PatchBundle(val patchesJar: File) { true } -} \ No newline at end of file +} diff --git a/app/src/main/java/app/revanced/manager/ui/component/bundle/BaseBundleDialog.kt b/app/src/main/java/app/revanced/manager/ui/component/bundle/BaseBundleDialog.kt index dfc63735..0ec27d8d 100644 --- a/app/src/main/java/app/revanced/manager/ui/component/bundle/BaseBundleDialog.kt +++ b/app/src/main/java/app/revanced/manager/ui/component/bundle/BaseBundleDialog.kt @@ -2,13 +2,26 @@ package app.revanced.manager.ui.component.bundle import android.webkit.URLUtil import androidx.compose.foundation.clickable -import androidx.compose.foundation.layout.* +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.ColumnScope +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.size import androidx.compose.material.icons.Icons import androidx.compose.material.icons.automirrored.outlined.ArrowRight -import androidx.compose.material.icons.outlined.Extension -import androidx.compose.material.icons.outlined.Inventory2 +import androidx.compose.material.icons.automirrored.outlined.Send +import androidx.compose.material.icons.outlined.Commit +import androidx.compose.material.icons.outlined.Description +import androidx.compose.material.icons.outlined.Gavel +import androidx.compose.material.icons.outlined.Language +import androidx.compose.material.icons.outlined.Person import androidx.compose.material.icons.outlined.Sell -import androidx.compose.material3.* +import androidx.compose.material3.HorizontalDivider +import androidx.compose.material3.Icon +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.Text import androidx.compose.runtime.Composable import androidx.compose.runtime.getValue import androidx.compose.runtime.mutableStateOf @@ -17,10 +30,11 @@ import androidx.compose.runtime.setValue import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.graphics.vector.ImageVector +import androidx.compose.ui.platform.LocalUriHandler import androidx.compose.ui.res.stringResource -import androidx.compose.ui.text.font.FontWeight import androidx.compose.ui.unit.dp import app.revanced.manager.R +import app.revanced.manager.patcher.patch.PatchBundleManifestAttributes import app.revanced.manager.ui.component.ColumnWithScrollbar import app.revanced.manager.ui.component.TextInputDialog import app.revanced.manager.ui.component.haptics.HapticSwitch @@ -29,12 +43,12 @@ import app.revanced.manager.ui.component.haptics.HapticSwitch fun BaseBundleDialog( modifier: Modifier = Modifier, isDefault: Boolean, - name: String?, remoteUrl: String?, onRemoteUrlChange: ((String) -> Unit)? = null, patchCount: Int, version: String?, autoUpdate: Boolean, + bundleManifestAttributes: PatchBundleManifestAttributes?, onAutoUpdateChange: (Boolean) -> Unit, onPatchesClick: () -> Unit, extraFields: @Composable ColumnScope.() -> Unit = {} @@ -48,35 +62,26 @@ fun BaseBundleDialog( modifier = Modifier.padding(16.dp), verticalArrangement = Arrangement.spacedBy(4.dp) ) { - Row( - modifier = Modifier.fillMaxWidth(), - horizontalArrangement = Arrangement.spacedBy(8.dp, Alignment.Start), - verticalAlignment = Alignment.CenterVertically - ) { - Icon( - imageVector = Icons.Outlined.Inventory2, - contentDescription = null, - tint = MaterialTheme.colorScheme.primary, - modifier = Modifier.size(32.dp) - ) - name?.let { - Text( - text = it, - style = MaterialTheme.typography.titleLarge.copy(fontWeight = FontWeight(800)), - color = MaterialTheme.colorScheme.primary, - ) - } + version?.let { + Tag(Icons.Outlined.Sell, it) } - Row( - horizontalArrangement = Arrangement.spacedBy(16.dp), - modifier = Modifier - .fillMaxWidth() - .padding(start = 2.dp) - ) { - version?.let { - Tag(Icons.Outlined.Sell, it) - } - Tag(Icons.Outlined.Extension, patchCount.toString()) + bundleManifestAttributes?.description?.let { + Tag(Icons.Outlined.Description, it) + } + bundleManifestAttributes?.source?.let { + Tag(Icons.Outlined.Commit, it) + } + bundleManifestAttributes?.author?.let { + Tag(Icons.Outlined.Person, it) + } + bundleManifestAttributes?.contact?.let { + Tag(Icons.AutoMirrored.Outlined.Send, it) + } + bundleManifestAttributes?.website?.let { + Tag(Icons.Outlined.Language, it, isUrl = true) + } + bundleManifestAttributes?.license?.let { + Tag(Icons.Outlined.Gavel, it) } } @@ -138,8 +143,8 @@ fun BaseBundleDialog( val patchesClickable = patchCount > 0 BundleListItem( - headlineText = stringResource(R.string.patches), - supportingText = stringResource(R.string.bundle_view_patches), + headlineText = stringResource(R.string.bundle_view_patches), + supportingText = stringResource(R.string.bundle_view_all_patches, patchCount), modifier = Modifier.clickable( enabled = patchesClickable, onClick = onPatchesClick @@ -160,22 +165,34 @@ fun BaseBundleDialog( @Composable private fun Tag( icon: ImageVector, - text: String + text: String, + isUrl: Boolean = false ) { + val uriHandler = LocalUriHandler.current + Row( horizontalArrangement = Arrangement.spacedBy(6.dp), - verticalAlignment = Alignment.CenterVertically + verticalAlignment = Alignment.CenterVertically, + modifier = if (isUrl) { + Modifier + .clickable { + try { + uriHandler.openUri(text) + } catch (_: Exception) {} + } + } + else + Modifier, ) { Icon( imageVector = icon, contentDescription = null, - modifier = Modifier.size(16.dp), - tint = MaterialTheme.colorScheme.outline, + modifier = Modifier.size(16.dp) ) Text( text, style = MaterialTheme.typography.bodyMedium, - color = MaterialTheme.colorScheme.outline, + color = if(isUrl) MaterialTheme.colorScheme.primary else MaterialTheme.colorScheme.outline, ) } } \ No newline at end of file diff --git a/app/src/main/java/app/revanced/manager/ui/component/bundle/BundleInformationDialog.kt b/app/src/main/java/app/revanced/manager/ui/component/bundle/BundleInformationDialog.kt index 15f5eae1..1e86aced 100644 --- a/app/src/main/java/app/revanced/manager/ui/component/bundle/BundleInformationDialog.kt +++ b/app/src/main/java/app/revanced/manager/ui/component/bundle/BundleInformationDialog.kt @@ -44,13 +44,14 @@ fun BundleInformationDialog( }.collectAsStateWithLifecycle(null) val patchCount by bundle.patchCountFlow.collectAsStateWithLifecycle(0) val version by bundle.versionFlow.collectAsStateWithLifecycle(null) + val bundleManifestAttributes = state.patchBundleOrNull()?.patchBundleManifestAttributes if (viewCurrentBundlePatches) { BundlePatchesDialog( onDismissRequest = { viewCurrentBundlePatches = false }, - bundle = bundle, + bundle = bundle ) } @@ -62,7 +63,7 @@ fun BundleInformationDialog( Scaffold( topBar = { BundleTopBar( - title = stringResource(R.string.patch_bundle_field), + title = bundleName, onBackClick = onDismissRequest, backIcon = { Icon( @@ -94,11 +95,11 @@ fun BundleInformationDialog( BaseBundleDialog( modifier = Modifier.padding(paddingValues), isDefault = bundle.isDefault, - name = bundleName, remoteUrl = bundle.asRemoteOrNull?.endpoint, patchCount = patchCount, version = version, autoUpdate = props?.autoUpdate == true, + bundleManifestAttributes = bundleManifestAttributes, onAutoUpdateChange = { composableScope.launch { bundle.asRemoteOrNull?.setAutoUpdate(it) diff --git a/app/src/main/java/app/revanced/manager/ui/model/BundleInfo.kt b/app/src/main/java/app/revanced/manager/ui/model/BundleInfo.kt index cfef366f..0fb01a76 100644 --- a/app/src/main/java/app/revanced/manager/ui/model/BundleInfo.kt +++ b/app/src/main/java/app/revanced/manager/ui/model/BundleInfo.kt @@ -23,8 +23,6 @@ data class BundleInfo( yieldAll(universal) } - val patchCount get() = compatible.size + incompatible.size + universal.size - fun patchSequence(allowIncompatible: Boolean) = if (allowIncompatible) { all } else { @@ -79,7 +77,7 @@ data class BundleInfo( targetList.add(it) } - BundleInfo(source.getName(), bundle.readManifestAttribute("Version"), source.uid, compatible, incompatible, universal) + BundleInfo(source.getName(), bundle.patchBundleManifestAttributes?.version, source.uid, compatible, incompatible, universal) } } diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index 253de2df..31730c8c 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -354,6 +354,7 @@ Auto update Automatically update this bundle when ReVanced starts View patches + View all %d patches Any version Any package Delete bundle From f7a4ae5791e1d9baff4f15333280d5662d7af9b6 Mon Sep 17 00:00:00 2001 From: brosssh <44944126+brosssh@users.noreply.github.com> Date: Tue, 8 Jul 2025 21:57:56 +0200 Subject: [PATCH 13/19] fix: Add missing header for "Updates" settings (#2642) --- .../manager/ui/screen/settings/update/UpdatesSettingsScreen.kt | 3 +++ 1 file changed, 3 insertions(+) diff --git a/app/src/main/java/app/revanced/manager/ui/screen/settings/update/UpdatesSettingsScreen.kt b/app/src/main/java/app/revanced/manager/ui/screen/settings/update/UpdatesSettingsScreen.kt index c3762772..c613a179 100644 --- a/app/src/main/java/app/revanced/manager/ui/screen/settings/update/UpdatesSettingsScreen.kt +++ b/app/src/main/java/app/revanced/manager/ui/screen/settings/update/UpdatesSettingsScreen.kt @@ -16,6 +16,7 @@ import androidx.compose.ui.res.stringResource import app.revanced.manager.R import app.revanced.manager.ui.component.AppTopBar import app.revanced.manager.ui.component.ColumnWithScrollbar +import app.revanced.manager.ui.component.GroupHeader import app.revanced.manager.ui.component.settings.BooleanItem import app.revanced.manager.ui.component.settings.SettingsListItem import app.revanced.manager.ui.viewmodel.UpdatesSettingsViewModel @@ -50,6 +51,8 @@ fun UpdatesSettingsScreen( .fillMaxSize() .padding(paddingValues) ) { + GroupHeader(stringResource(R.string.manager)) + SettingsListItem( modifier = Modifier.clickable { coroutineScope.launch { From 7148ee66f8bb533d466e498d7bd7a964b809441b Mon Sep 17 00:00:00 2001 From: oSumAtrIX Date: Sat, 10 May 2025 20:37:26 +0200 Subject: [PATCH 14/19] feat: Rename strings --- .../domain/bundles/PatchBundleSource.kt | 2 +- .../domain/bundles/RemotePatchBundle.kt | 2 +- .../repository/PatchBundleRepository.kt | 2 +- .../ui/component/ExceptionViewerDialog.kt | 2 +- .../ui/component/bundle/BaseBundleDialog.kt | 12 +-- .../bundle/BundleInformationDialog.kt | 8 +- .../manager/ui/component/bundle/BundleItem.kt | 8 +- .../component/bundle/BundlePatchesDialog.kt | 6 +- .../ui/component/bundle/BundleSelector.kt | 6 +- .../ui/component/bundle/ImportBundleDialog.kt | 12 +-- .../manager/ui/screen/DashboardScreen.kt | 12 +-- .../settings/DeveloperSettingsScreen.kt | 6 +- .../settings/ImportExportSettingsScreen.kt | 8 +- .../ui/viewmodel/DashboardViewModel.kt | 6 +- .../ui/viewmodel/DeveloperOptionsViewModel.kt | 2 +- .../ui/viewmodel/ImportExportViewModel.kt | 8 +- app/src/main/res/values/strings.xml | 87 ++++++++----------- 17 files changed, 89 insertions(+), 100 deletions(-) diff --git a/app/src/main/java/app/revanced/manager/domain/bundles/PatchBundleSource.kt b/app/src/main/java/app/revanced/manager/domain/bundles/PatchBundleSource.kt index 1bc8a7f8..bd779dd1 100644 --- a/app/src/main/java/app/revanced/manager/domain/bundles/PatchBundleSource.kt +++ b/app/src/main/java/app/revanced/manager/domain/bundles/PatchBundleSource.kt @@ -34,7 +34,7 @@ sealed class PatchBundleSource(initialName: String, val uid: Int, directory: Fil private val _nameFlow = MutableStateFlow(initialName) val nameFlow = - _nameFlow.map { it.ifEmpty { app.getString(if (isDefault) R.string.bundle_name_default else R.string.bundle_name_fallback) } } + _nameFlow.map { it.ifEmpty { app.getString(if (isDefault) R.string.patches_name_default else R.string.patches_name_fallback) } } suspend fun getName() = nameFlow.first() diff --git a/app/src/main/java/app/revanced/manager/domain/bundles/RemotePatchBundle.kt b/app/src/main/java/app/revanced/manager/domain/bundles/RemotePatchBundle.kt index 81289855..897312f7 100644 --- a/app/src/main/java/app/revanced/manager/domain/bundles/RemotePatchBundle.kt +++ b/app/src/main/java/app/revanced/manager/domain/bundles/RemotePatchBundle.kt @@ -50,7 +50,7 @@ sealed class RemotePatchBundle(name: String, id: Int, directory: File, val endpo suspend fun setAutoUpdate(value: Boolean) = configRepository.setAutoUpdate(uid, value) companion object { - const val updateFailMsg = "Failed to update patch bundle(s)" + const val updateFailMsg = "Failed to update patches" } } diff --git a/app/src/main/java/app/revanced/manager/domain/repository/PatchBundleRepository.kt b/app/src/main/java/app/revanced/manager/domain/repository/PatchBundleRepository.kt index 79bb5cea..3afe4e39 100644 --- a/app/src/main/java/app/revanced/manager/domain/repository/PatchBundleRepository.kt +++ b/app/src/main/java/app/revanced/manager/domain/repository/PatchBundleRepository.kt @@ -165,7 +165,7 @@ class PatchBundleRepository( getBundlesByType().forEach { it.downloadLatest() } suspend fun updateCheck() = - uiSafe(app, R.string.source_download_fail, "Failed to update bundles") { + uiSafe(app, R.string.patches_download_fail, "Failed to update bundles") { coroutineScope { if (!networkInfo.isSafe()) { Log.d(tag, "Skipping update check because the network is down or metered.") diff --git a/app/src/main/java/app/revanced/manager/ui/component/ExceptionViewerDialog.kt b/app/src/main/java/app/revanced/manager/ui/component/ExceptionViewerDialog.kt index bea8846f..3925c48b 100644 --- a/app/src/main/java/app/revanced/manager/ui/component/ExceptionViewerDialog.kt +++ b/app/src/main/java/app/revanced/manager/ui/component/ExceptionViewerDialog.kt @@ -30,7 +30,7 @@ fun ExceptionViewerDialog(text: String, onDismiss: () -> Unit) { Scaffold( topBar = { BundleTopBar( - title = stringResource(R.string.bundle_error), + title = stringResource(R.string.patches_error), onBackClick = onDismiss, backIcon = { Icon( diff --git a/app/src/main/java/app/revanced/manager/ui/component/bundle/BaseBundleDialog.kt b/app/src/main/java/app/revanced/manager/ui/component/bundle/BaseBundleDialog.kt index 0ec27d8d..3e07af24 100644 --- a/app/src/main/java/app/revanced/manager/ui/component/bundle/BaseBundleDialog.kt +++ b/app/src/main/java/app/revanced/manager/ui/component/bundle/BaseBundleDialog.kt @@ -92,8 +92,8 @@ fun BaseBundleDialog( if (remoteUrl != null) { BundleListItem( - headlineText = stringResource(R.string.bundle_auto_update), - supportingText = stringResource(R.string.bundle_auto_update_description), + headlineText = stringResource(R.string.auto_update), + supportingText = stringResource(R.string.auto_update_description), trailingContent = { HapticSwitch( checked = autoUpdate, @@ -113,7 +113,7 @@ fun BaseBundleDialog( if (showUrlInputDialog) { TextInputDialog( initial = url, - title = stringResource(R.string.bundle_input_source_url), + title = stringResource(R.string.patches_url), onDismissRequest = { showUrlInputDialog = false }, onConfirm = { showUrlInputDialog = false @@ -134,7 +134,7 @@ fun BaseBundleDialog( showUrlInputDialog = true } ), - headlineText = stringResource(R.string.bundle_input_source_url), + headlineText = stringResource(R.string.patches_url), supportingText = url.ifEmpty { stringResource(R.string.field_not_set) } @@ -143,8 +143,8 @@ fun BaseBundleDialog( val patchesClickable = patchCount > 0 BundleListItem( - headlineText = stringResource(R.string.bundle_view_patches), - supportingText = stringResource(R.string.bundle_view_all_patches, patchCount), + headlineText = stringResource(R.string.patches), + supportingText = stringResource(R.string.view_patches), modifier = Modifier.clickable( enabled = patchesClickable, onClick = onPatchesClick diff --git a/app/src/main/java/app/revanced/manager/ui/component/bundle/BundleInformationDialog.kt b/app/src/main/java/app/revanced/manager/ui/component/bundle/BundleInformationDialog.kt index 1e86aced..20d2d7c4 100644 --- a/app/src/main/java/app/revanced/manager/ui/component/bundle/BundleInformationDialog.kt +++ b/app/src/main/java/app/revanced/manager/ui/component/bundle/BundleInformationDialog.kt @@ -119,8 +119,8 @@ fun BundleInformationDialog( ) BundleListItem( - headlineText = stringResource(R.string.bundle_error), - supportingText = stringResource(R.string.bundle_error_description), + headlineText = stringResource(R.string.patches_error), + supportingText = stringResource(R.string.patches_error_description), trailingContent = { Icon( Icons.AutoMirrored.Outlined.ArrowRight, @@ -133,8 +133,8 @@ fun BundleInformationDialog( if (state is PatchBundleSource.State.Missing && !isLocal) { BundleListItem( - headlineText = stringResource(R.string.bundle_error), - supportingText = stringResource(R.string.bundle_not_downloaded), + headlineText = stringResource(R.string.patches_error), + supportingText = stringResource(R.string.patches_not_downloaded), modifier = Modifier.clickable(onClick = onUpdate) ) } diff --git a/app/src/main/java/app/revanced/manager/ui/component/bundle/BundleItem.kt b/app/src/main/java/app/revanced/manager/ui/component/bundle/BundleItem.kt index d50d6af2..3501e26e 100644 --- a/app/src/main/java/app/revanced/manager/ui/component/bundle/BundleItem.kt +++ b/app/src/main/java/app/revanced/manager/ui/component/bundle/BundleItem.kt @@ -67,8 +67,8 @@ fun BundleItem( onDelete() viewBundleDialogPage = false }, - title = stringResource(R.string.bundle_delete_single_dialog_title), - description = stringResource(R.string.bundle_delete_single_dialog_description, name), + title = stringResource(R.string.delete), + description = stringResource(R.string.patches_delete_single_dialog_description, name), icon = Icons.Outlined.Delete ) } @@ -100,8 +100,8 @@ fun BundleItem( Row { val icon = remember(state) { when (state) { - is PatchBundleSource.State.Failed -> Icons.Outlined.ErrorOutline to R.string.bundle_error - is PatchBundleSource.State.Missing -> Icons.Outlined.Warning to R.string.bundle_missing + is PatchBundleSource.State.Failed -> Icons.Outlined.ErrorOutline to R.string.patches_error + is PatchBundleSource.State.Missing -> Icons.Outlined.Warning to R.string.patches_missing is PatchBundleSource.State.Loaded -> null } } diff --git a/app/src/main/java/app/revanced/manager/ui/component/bundle/BundlePatchesDialog.kt b/app/src/main/java/app/revanced/manager/ui/component/bundle/BundlePatchesDialog.kt index 30446b9d..7d28237d 100644 --- a/app/src/main/java/app/revanced/manager/ui/component/bundle/BundlePatchesDialog.kt +++ b/app/src/main/java/app/revanced/manager/ui/component/bundle/BundlePatchesDialog.kt @@ -46,7 +46,7 @@ fun BundlePatchesDialog( Scaffold( topBar = { BundleTopBar( - title = stringResource(R.string.bundle_patches), + title = stringResource(R.string.patches), onBackClick = onDismissRequest, backIcon = { Icon( @@ -133,10 +133,10 @@ fun PatchItem( verticalAlignment = Alignment.CenterVertically ) { PatchInfoChip( - text = "$PACKAGE_ICON ${stringResource(R.string.bundle_view_patches_any_package)}" + text = "$PACKAGE_ICON ${stringResource(R.string.patches_view_any_package)}" ) PatchInfoChip( - text = "$VERSION_ICON ${stringResource(R.string.bundle_view_patches_any_version)}" + text = "$VERSION_ICON ${stringResource(R.string.patches_view_any_version)}" ) } } else { diff --git a/app/src/main/java/app/revanced/manager/ui/component/bundle/BundleSelector.kt b/app/src/main/java/app/revanced/manager/ui/component/bundle/BundleSelector.kt index c7cd7565..bcad3fe9 100644 --- a/app/src/main/java/app/revanced/manager/ui/component/bundle/BundleSelector.kt +++ b/app/src/main/java/app/revanced/manager/ui/component/bundle/BundleSelector.kt @@ -13,14 +13,14 @@ import androidx.compose.material3.Text import androidx.compose.runtime.Composable import androidx.compose.runtime.LaunchedEffect import androidx.compose.runtime.getValue -import androidx.compose.runtime.remember import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier +import androidx.compose.ui.res.stringResource import androidx.compose.ui.unit.dp import androidx.lifecycle.compose.collectAsStateWithLifecycle +import app.revanced.manager.R import app.revanced.manager.domain.bundles.PatchBundleSource import app.revanced.manager.domain.bundles.PatchBundleSource.Extensions.nameState -import kotlinx.coroutines.flow.map @OptIn(ExperimentalMaterial3Api::class) @Composable @@ -50,7 +50,7 @@ fun BundleSelector(bundles: List, onFinish: (PatchBundleSourc .fillMaxWidth() ) { Text( - text = "Select bundle", + text = stringResource(R.string.select), style = MaterialTheme.typography.titleLarge, color = MaterialTheme.colorScheme.onSurface ) diff --git a/app/src/main/java/app/revanced/manager/ui/component/bundle/ImportBundleDialog.kt b/app/src/main/java/app/revanced/manager/ui/component/bundle/ImportBundleDialog.kt index 821a2669..1a05d866 100644 --- a/app/src/main/java/app/revanced/manager/ui/component/bundle/ImportBundleDialog.kt +++ b/app/src/main/java/app/revanced/manager/ui/component/bundle/ImportBundleDialog.kt @@ -77,7 +77,7 @@ fun ImportPatchBundleDialog( AlertDialogExtended( onDismissRequest = onDismiss, title = { - Text(stringResource(if (currentStep == 0) R.string.select else R.string.add_patch_bundle)) + Text(stringResource(if (currentStep == 0) R.string.select else R.string.add_patches)) }, text = { steps[currentStep]() @@ -126,7 +126,7 @@ fun SelectBundleTypeStep( ) { Text( modifier = Modifier.padding(horizontal = 24.dp), - text = stringResource(R.string.select_bundle_type_dialog_description) + text = stringResource(R.string.select_patches_type_dialog_description) ) Column { ListItem( @@ -136,7 +136,7 @@ fun SelectBundleTypeStep( ), headlineContent = { Text(stringResource(R.string.enter_url)) }, overlineContent = { Text(stringResource(R.string.recommended)) }, - supportingContent = { Text(stringResource(R.string.remote_bundle_description)) }, + supportingContent = { Text(stringResource(R.string.remote_patches_description)) }, leadingContent = { HapticRadioButton( selected = bundleType == BundleType.Remote, @@ -152,7 +152,7 @@ fun SelectBundleTypeStep( onClick = { onBundleTypeSelected(BundleType.Local) } ), headlineContent = { Text(stringResource(R.string.select_from_storage)) }, - supportingContent = { Text(stringResource(R.string.local_bundle_description)) }, + supportingContent = { Text(stringResource(R.string.local_patches_description)) }, overlineContent = { }, leadingContent = { HapticRadioButton( @@ -185,7 +185,7 @@ fun ImportBundleStep( ) { ListItem( headlineContent = { - Text(stringResource(R.string.patch_bundle_field)) + Text(stringResource(R.string.patches)) }, supportingContent = { Text(stringResource(if (patchBundle != null) R.string.file_field_set else R.string.file_field_not_set)) }, trailingContent = { @@ -206,7 +206,7 @@ fun ImportBundleStep( OutlinedTextField( value = remoteUrl, onValueChange = onRemoteUrlChange, - label = { Text(stringResource(R.string.bundle_url)) } + label = { Text(stringResource(R.string.patches_url)) } ) } Column( diff --git a/app/src/main/java/app/revanced/manager/ui/screen/DashboardScreen.kt b/app/src/main/java/app/revanced/manager/ui/screen/DashboardScreen.kt index 0d14cf9e..03eff51f 100644 --- a/app/src/main/java/app/revanced/manager/ui/screen/DashboardScreen.kt +++ b/app/src/main/java/app/revanced/manager/ui/screen/DashboardScreen.kt @@ -79,7 +79,7 @@ enum class DashboardPage( val icon: ImageVector ) { DASHBOARD(R.string.tab_apps, Icons.Outlined.Apps), - BUNDLES(R.string.tab_bundles, Icons.Outlined.Source), + BUNDLES(R.string.tab_patches, Icons.Outlined.Source), } @SuppressLint("BatteryLife") @@ -93,7 +93,7 @@ fun DashboardScreen( onDownloaderPluginClick: () -> Unit, onAppClick: (String) -> Unit ) { - val bundlesSelectable by remember { derivedStateOf { vm.selectedSources.size > 0 } } + val bundlesSelectable by remember { derivedStateOf { vm.selectedSources.isNotEmpty() } } val availablePatches by vm.availablePatches.collectAsStateWithLifecycle(0) val showNewDownloaderPluginsNotification by vm.newDownloaderPluginsAvailable.collectAsStateWithLifecycle( false @@ -164,8 +164,8 @@ fun DashboardScreen( vm.selectedSources.forEach { if (!it.isDefault) vm.delete(it) } vm.cancelSourceSelection() }, - title = stringResource(R.string.bundle_delete_multiple_dialog_title), - description = stringResource(R.string.bundle_delete_multiple_dialog_description), + title = stringResource(R.string.delete), + description = stringResource(R.string.patches_delete_multiple_dialog_description), icon = Icons.Outlined.Delete ) } @@ -174,7 +174,7 @@ fun DashboardScreen( topBar = { if (bundlesSelectable) { BundleTopBar( - title = stringResource(R.string.bundles_selected, vm.selectedSources.size), + title = stringResource(R.string.patches_selected, vm.selectedSources.size), onBackClick = vm::cancelSourceSelection, backIcon = { Icon( @@ -239,7 +239,7 @@ fun DashboardScreen( when (pagerState.currentPage) { DashboardPage.DASHBOARD.ordinal -> { if (availablePatches < 1) { - androidContext.toast(androidContext.getString(R.string.patches_unavailable)) + androidContext.toast(androidContext.getString(R.string.no_patch_found)) composableScope.launch { pagerState.animateScrollToPage( DashboardPage.BUNDLES.ordinal diff --git a/app/src/main/java/app/revanced/manager/ui/screen/settings/DeveloperSettingsScreen.kt b/app/src/main/java/app/revanced/manager/ui/screen/settings/DeveloperSettingsScreen.kt index a6164ec8..ac80009a 100644 --- a/app/src/main/java/app/revanced/manager/ui/screen/settings/DeveloperSettingsScreen.kt +++ b/app/src/main/java/app/revanced/manager/ui/screen/settings/DeveloperSettingsScreen.kt @@ -48,13 +48,13 @@ fun DeveloperSettingsScreen( description = R.string.developer_options_description, ) - GroupHeader(stringResource(R.string.patch_bundles_section)) + GroupHeader(stringResource(R.string.patches)) SettingsListItem( - headlineContent = stringResource(R.string.patch_bundles_force_download), + headlineContent = stringResource(R.string.patches_force_download), modifier = Modifier.clickable(onClick = vm::redownloadBundles) ) SettingsListItem( - headlineContent = stringResource(R.string.patch_bundles_reset), + headlineContent = stringResource(R.string.patches_reset), modifier = Modifier.clickable(onClick = vm::redownloadBundles) ) } diff --git a/app/src/main/java/app/revanced/manager/ui/screen/settings/ImportExportSettingsScreen.kt b/app/src/main/java/app/revanced/manager/ui/screen/settings/ImportExportSettingsScreen.kt index 2d88bb37..9383d44a 100644 --- a/app/src/main/java/app/revanced/manager/ui/screen/settings/ImportExportSettingsScreen.kt +++ b/app/src/main/java/app/revanced/manager/ui/screen/settings/ImportExportSettingsScreen.kt @@ -240,8 +240,8 @@ fun ImportExportSettingsScreen( } } }, - headline = R.string.patch_selection_reset_bundle, - description = R.string.patch_selection_reset_bundle_description + headline = R.string.patch_selection_reset_patches, + description = R.string.patch_selection_reset_patches_description ) } } @@ -296,8 +296,8 @@ fun ImportExportSettingsScreen( } } }, - headline = R.string.patch_options_reset_bundle, - description = R.string.patch_options_reset_bundle_description, + headline = R.string.patch_options_reset, + description = R.string.patch_options_reset_all, ) } } diff --git a/app/src/main/java/app/revanced/manager/ui/viewmodel/DashboardViewModel.kt b/app/src/main/java/app/revanced/manager/ui/viewmodel/DashboardViewModel.kt index 0783e94d..ae38bd29 100644 --- a/app/src/main/java/app/revanced/manager/ui/viewmodel/DashboardViewModel.kt +++ b/app/src/main/java/app/revanced/manager/ui/viewmodel/DashboardViewModel.kt @@ -135,13 +135,13 @@ class DashboardViewModel( uiSafe( app, - R.string.source_download_fail, + R.string.patches_download_fail, RemotePatchBundle.updateFailMsg ) { if (bundle.update()) - app.toast(app.getString(R.string.bundle_update_success, bundle.getName())) + app.toast(app.getString(R.string.patches_update_success, bundle.getName())) else - app.toast(app.getString(R.string.bundle_update_unavailable, bundle.getName())) + app.toast(app.getString(R.string.patches_update_unavailable, bundle.getName())) } } } \ No newline at end of file diff --git a/app/src/main/java/app/revanced/manager/ui/viewmodel/DeveloperOptionsViewModel.kt b/app/src/main/java/app/revanced/manager/ui/viewmodel/DeveloperOptionsViewModel.kt index bc8d0527..8d9b11ae 100644 --- a/app/src/main/java/app/revanced/manager/ui/viewmodel/DeveloperOptionsViewModel.kt +++ b/app/src/main/java/app/revanced/manager/ui/viewmodel/DeveloperOptionsViewModel.kt @@ -16,7 +16,7 @@ class DeveloperOptionsViewModel( private val patchBundleRepository: PatchBundleRepository ) : ViewModel() { fun redownloadBundles() = viewModelScope.launch { - uiSafe(app, R.string.source_download_fail, RemotePatchBundle.updateFailMsg) { + uiSafe(app, R.string.patches_download_fail, RemotePatchBundle.updateFailMsg) { patchBundleRepository.redownloadRemoteBundles() } } diff --git a/app/src/main/java/app/revanced/manager/ui/viewmodel/ImportExportViewModel.kt b/app/src/main/java/app/revanced/manager/ui/viewmodel/ImportExportViewModel.kt index 3628f0d7..8ac7119c 100644 --- a/app/src/main/java/app/revanced/manager/ui/viewmodel/ImportExportViewModel.kt +++ b/app/src/main/java/app/revanced/manager/ui/viewmodel/ImportExportViewModel.kt @@ -61,8 +61,8 @@ sealed class ResetDialogState( ) class PatchSelectionBundle(dialogOptionName: String, onConfirm: () -> Unit) : ResetDialogState( - titleResId = R.string.patch_selection_reset_bundle, - descriptionResId = R.string.patch_selection_reset_bundle_dialog_description, + titleResId = R.string.patch_selection_reset_patches, + descriptionResId = R.string.patch_selection_reset_patches_dialog_description, onConfirm = onConfirm, dialogOptionName = dialogOptionName ) @@ -81,8 +81,8 @@ sealed class ResetDialogState( ) class PatchOptionBundle(dialogOptionName: String, onConfirm: () -> Unit) : ResetDialogState( - titleResId = R.string.patch_options_reset_bundle, - descriptionResId = R.string.patch_options_reset_bundle_dialog_description, + titleResId = R.string.patch_options_reset, + descriptionResId = R.string.patch_options_reset_dialog_description, onConfirm = onConfirm, dialogOptionName = dialogOptionName ) diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index 31730c8c..60bd51c4 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -14,26 +14,24 @@ Dashboard Settings Select an app - %1$d/%2$d selected + %1$d/%2$d selected New downloader plugins available. Click here to configure them. Patching on this device architecture is unsupported and will most likely fail. Import - Import patch bundle - Bundle patches - Patch bundle + Import patches Selected Not selected Not set - Missing - Error - Bundle could not be loaded. Click to view the error - Bundle has not been downloaded. Click here to download it - Default - Unnamed + Missing + Error + Patches could not be loaded. Click to view the error + Patches has not been downloaded. Click here to download it + Patches + Unnamed Android 11 bug The app installation permission must be granted ahead of time to avoid a bug in the Android 11 system that will negatively affect the user experience. @@ -139,16 +137,15 @@ Reset patch selection for app You are about to reset the patch selection for the app \"%s\". You will have to manually select each patch again. Resets patch selection for a single app - Resets patch selection for bundle - You are about to reset the patch selection for the bundle \"%s\". You will have to manually select each patch again. - Resets the patch selection for all patches in a bundle + Resets patch selection for a specific patches + You are about to reset the patch selection for \"%s\". You will have to manually select each patch again. + Resets the patch selection for a specific patches Reset patch options for app You are about to reset the patch options for the app \"%s\". You will have to reapply each option again. Resets patch options for a single app - Resets patch options for bundle - You are about to reset the patch options for the bundle \"%s\". You will have to reapply each option again. - Resets patch options for all patches in a bundle - Reset patch options + Reset patch options + You are about to reset the patch options for \"%s\". You will have to reapply each option again. + Reset patch options for all You are about to reset patch options. You will have to reapply each option again. Resets all patch options Plugins @@ -164,7 +161,7 @@ Search apps… Loading… - Downloading patch bundle… + Downloading patches… Options OK @@ -202,8 +199,8 @@ Exported logs API URL The API used to download necessary files. - Set custom API URL - Set the API URL of ReVanced Manager. ReVanced Manager uses the API to download patches and updates. + Change API URL + Change the API URL of ReVanced Manager. ReVanced Manager uses the API to download patches and updates. ReVanced Manager connects to the API to download patches and updates. Make sure that you trust it. Set Reset API URL @@ -213,26 +210,25 @@ CPU Architectures Memory limits %1$dMB (Normal) - %2$dMB (Large) - Patch bundles - Force download all patch bundles - Reset patch bundles + Force download all patches + Reset patches Patching Signing Storage - No patches are available. Check your bundles + No patch can be found. Check your patches Apps - Patch bundles + Patches Delete Refresh Continue anyways Download another version Download app Download APK file - Failed to download patch bundle: %s - Failed to load updated patch bundle: %s + Failed to download patches: %s + Failed to load updated patches: %s No patched apps found Tap on the patches to get more information about them - %s selected + %s selected Incompatible patches Universal patches Patch selection and options has been reset to recommended defaults @@ -348,20 +344,13 @@ Help us improve this application Developer options Options for debugging issues - Source URL - Successfully updated %s - No update available for %s - Auto update - Automatically update this bundle when ReVanced starts - View patches - View all %d patches - Any version - Any package - Delete bundle - Delete bundles - Are you sure you want to delete the bundle \"%s\"? - Are you sure you want to delete the selected bundles? - + Successfully updated %s + No update available for %s + View patches + Any version + Any package + Are you sure you want to delete \"%s\"? + Are you sure you want to delete the selected patches? About ReVanced Manager ReVanced Manager is an Android application that uses ReVanced Patcher to patch Android apps. It allows you to download and patch apps with custom patches, and manage the patching process. @@ -391,7 +380,7 @@ Save (%1$s) Update Empty - Tap on Update when prompted. \n ReVanced Manager will close when updating. + Tap on Update when prompted.\nReVanced Manager will close when updating. No changelogs found Just now %sm ago @@ -415,10 +404,9 @@ No contributors found Select Select or deselect all - Add new bundle - Add a new bundle from a URL or storage - Import local files from your storage, does not automatically update - Import remote files from a URL, can automatically update + Add new patches from URL or local files + Add patches from local storage. + Add patches from URL. Patches can automatically update. Recommended Installation failed @@ -443,9 +431,10 @@ About device Enter URL Next - Add patch bundle - Bundle URL Auto update + Add patches + Automatically update when a new version is available + Patches URL These patches are not compatible with the selected app version (%1$s).\n\nClick on the patches to see more details. Incompatible patch Any From b51d1ee47a0ee29f85e394747b0f264a89bf68d9 Mon Sep 17 00:00:00 2001 From: Pun Butrach Date: Mon, 14 Jul 2025 20:35:27 +0700 Subject: [PATCH 15/19] fix: Transparent status on fullscreen dialog (#2654) --- .../manager/ui/component/FullscreenDialog.kt | 12 ++++++++++-- 1 file changed, 10 insertions(+), 2 deletions(-) diff --git a/app/src/main/java/app/revanced/manager/ui/component/FullscreenDialog.kt b/app/src/main/java/app/revanced/manager/ui/component/FullscreenDialog.kt index 97fb98d9..539d599c 100644 --- a/app/src/main/java/app/revanced/manager/ui/component/FullscreenDialog.kt +++ b/app/src/main/java/app/revanced/manager/ui/component/FullscreenDialog.kt @@ -1,6 +1,7 @@ package app.revanced.manager.ui.component import android.view.WindowManager +import androidx.compose.foundation.isSystemInDarkTheme import androidx.compose.runtime.Composable import androidx.compose.runtime.LaunchedEffect import androidx.compose.ui.graphics.Color @@ -9,6 +10,7 @@ import androidx.compose.ui.platform.LocalView import androidx.compose.ui.window.Dialog import androidx.compose.ui.window.DialogProperties import androidx.compose.ui.window.DialogWindowProvider +import androidx.core.view.WindowCompat private val properties = DialogProperties( usePlatformDefaultWidth = false, @@ -22,11 +24,17 @@ fun FullscreenDialog(onDismissRequest: () -> Unit, content: @Composable () -> Un onDismissRequest = onDismissRequest, properties = properties ) { - val window = (LocalView.current.parent as DialogWindowProvider).window - LaunchedEffect(Unit) { + val view = LocalView.current + val isDarkTheme = isSystemInDarkTheme() + LaunchedEffect(isDarkTheme) { + val window = (view.parent as DialogWindowProvider).window window.statusBarColor = Color.Transparent.toArgb() window.navigationBarColor = Color.Transparent.toArgb() window.addFlags(WindowManager.LayoutParams.FLAG_LAYOUT_NO_LIMITS) + + val insetsController = WindowCompat.getInsetsController(window, view) + insetsController.isAppearanceLightStatusBars = !isDarkTheme + insetsController.isAppearanceLightNavigationBars = !isDarkTheme } content() From 789f9ec86747d5a8b48807737334df3ec3d1fdcd Mon Sep 17 00:00:00 2001 From: Ax333l Date: Tue, 15 Jul 2025 14:28:40 +0200 Subject: [PATCH 16/19] feat: allow bundles to use classes from other bundles (#1951) --- .../app/revanced/manager/data/redux/Redux.kt | 74 ++++ .../data/room/bundles/PatchBundleDao.kt | 17 +- .../data/room/bundles/PatchBundleEntity.kt | 4 +- .../revanced/manager/di/RepositoryModule.kt | 1 - .../revanced/manager/di/ViewModelModule.kt | 1 + .../domain/bundles/LocalPatchBundle.kt | 22 +- .../domain/bundles/PatchBundleSource.kt | 101 +---- .../domain/bundles/RemotePatchBundle.kt | 82 ++-- .../PatchBundlePersistenceRepository.kt | 58 --- .../repository/PatchBundleRepository.kt | 402 ++++++++++++++---- .../manager/patcher/patch/PatchBundle.kt | 128 +++--- .../manager/patcher/patch/PatchBundleInfo.kt | 145 +++++++ .../patcher/runtime/CoroutineRuntime.kt | 14 +- .../manager/patcher/runtime/ProcessRuntime.kt | 12 +- .../patcher/runtime/process/Parameters.kt | 3 +- .../patcher/runtime/process/PatcherProcess.kt | 7 +- .../ui/component/bundle/BaseBundleDialog.kt | 198 --------- .../bundle/BundleInformationDialog.kt | 261 +++++++++--- .../manager/ui/component/bundle/BundleItem.kt | 34 +- .../component/bundle/BundlePatchesDialog.kt | 29 +- .../ui/component/bundle/BundleSelector.kt | 20 +- .../ui/component/bundle/ImportBundleDialog.kt | 12 +- .../revanced/manager/ui/model/BundleInfo.kt | 111 ----- .../manager/ui/screen/BundleListScreen.kt | 95 +++-- .../manager/ui/screen/DashboardScreen.kt | 30 +- .../ui/screen/PatchesSelectorScreen.kt | 2 +- .../ui/screen/RequiredOptionsScreen.kt | 3 +- .../settings/DownloadsSettingsScreen.kt | 268 ++++++------ .../settings/ImportExportSettingsScreen.kt | 16 +- .../ui/viewmodel/BundleListViewModel.kt | 76 ++++ .../ui/viewmodel/DashboardViewModel.kt | 58 +-- .../ui/viewmodel/PatchesSelectorViewModel.kt | 17 +- .../ui/viewmodel/SelectedAppInfoViewModel.kt | 67 +-- .../main/java/app/revanced/manager/util/PM.kt | 6 +- .../java/app/revanced/manager/util/Util.kt | 6 +- app/src/main/res/values/strings.xml | 6 +- 36 files changed, 1324 insertions(+), 1062 deletions(-) create mode 100644 app/src/main/java/app/revanced/manager/data/redux/Redux.kt delete mode 100644 app/src/main/java/app/revanced/manager/domain/repository/PatchBundlePersistenceRepository.kt create mode 100644 app/src/main/java/app/revanced/manager/patcher/patch/PatchBundleInfo.kt delete mode 100644 app/src/main/java/app/revanced/manager/ui/component/bundle/BaseBundleDialog.kt delete mode 100644 app/src/main/java/app/revanced/manager/ui/model/BundleInfo.kt create mode 100644 app/src/main/java/app/revanced/manager/ui/viewmodel/BundleListViewModel.kt diff --git a/app/src/main/java/app/revanced/manager/data/redux/Redux.kt b/app/src/main/java/app/revanced/manager/data/redux/Redux.kt new file mode 100644 index 00000000..785dedc4 --- /dev/null +++ b/app/src/main/java/app/revanced/manager/data/redux/Redux.kt @@ -0,0 +1,74 @@ +package app.revanced.manager.data.redux + +import android.util.Log +import app.revanced.manager.util.tag +import kotlinx.coroutines.CancellationException +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.ExperimentalCoroutinesApi +import kotlinx.coroutines.channels.Channel +import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.flow.asStateFlow +import kotlinx.coroutines.launch +import kotlinx.coroutines.sync.Mutex +import kotlinx.coroutines.sync.withLock +import kotlinx.coroutines.withTimeoutOrNull + +// This file implements React Redux-like state management. + +class Store(private val coroutineScope: CoroutineScope, initialState: S) : ActionContext { + private val _state = MutableStateFlow(initialState) + val state = _state.asStateFlow() + + // Do not touch these without the lock. + private var isRunningActions = false + private val queueChannel = Channel>(capacity = 10) + private val lock = Mutex() + + suspend fun dispatch(action: Action) = lock.withLock { + Log.d(tag, "Dispatching $action") + queueChannel.send(action) + + if (isRunningActions) return@withLock + isRunningActions = true + coroutineScope.launch { + runActions() + } + } + + @OptIn(ExperimentalCoroutinesApi::class) + private suspend fun runActions() { + while (true) { + val action = withTimeoutOrNull(200L) { queueChannel.receive() } + if (action == null) { + Log.d(tag, "Stopping action runner") + lock.withLock { + // New actions may be dispatched during the timeout. + isRunningActions = !queueChannel.isEmpty + if (!isRunningActions) return + } + continue + } + + Log.d(tag, "Running $action") + _state.value = try { + with(action) { this@Store.execute(_state.value) } + } catch (c: CancellationException) { + // This is done without the lock, but cancellation usually means the store is no longer needed. + isRunningActions = false + throw c + } catch (e: Exception) { + action.catch(e) + continue + } + } + } +} + +interface ActionContext + +interface Action { + suspend fun ActionContext.execute(current: S): S + suspend fun catch(exception: Exception) { + Log.e(tag, "Got exception while executing $this", exception) + } +} \ No newline at end of file diff --git a/app/src/main/java/app/revanced/manager/data/room/bundles/PatchBundleDao.kt b/app/src/main/java/app/revanced/manager/data/room/bundles/PatchBundleDao.kt index bf8b6224..38513365 100644 --- a/app/src/main/java/app/revanced/manager/data/room/bundles/PatchBundleDao.kt +++ b/app/src/main/java/app/revanced/manager/data/room/bundles/PatchBundleDao.kt @@ -1,25 +1,15 @@ package app.revanced.manager.data.room.bundles import androidx.room.* -import kotlinx.coroutines.flow.Flow @Dao interface PatchBundleDao { @Query("SELECT * FROM patch_bundles") suspend fun all(): List - @Query("SELECT version, auto_update FROM patch_bundles WHERE uid = :uid") - fun getPropsById(uid: Int): Flow - @Query("UPDATE patch_bundles SET version = :patches WHERE uid = :uid") suspend fun updateVersionHash(uid: Int, patches: String?) - @Query("UPDATE patch_bundles SET auto_update = :value WHERE uid = :uid") - suspend fun setAutoUpdate(uid: Int, value: Boolean) - - @Query("UPDATE patch_bundles SET name = :value WHERE uid = :uid") - suspend fun setName(uid: Int, value: String) - @Query("DELETE FROM patch_bundles WHERE uid != 0") suspend fun purgeCustomBundles() @@ -32,6 +22,9 @@ interface PatchBundleDao { @Query("DELETE FROM patch_bundles WHERE uid = :uid") suspend fun remove(uid: Int) - @Insert - suspend fun add(source: PatchBundleEntity) + @Query("SELECT name, version, auto_update, source FROM patch_bundles WHERE uid = :uid") + suspend fun getProps(uid: Int): PatchBundleProperties? + + @Upsert + suspend fun upsert(source: PatchBundleEntity) } \ No newline at end of file diff --git a/app/src/main/java/app/revanced/manager/data/room/bundles/PatchBundleEntity.kt b/app/src/main/java/app/revanced/manager/data/room/bundles/PatchBundleEntity.kt index 8fa6e8a7..9119d500 100644 --- a/app/src/main/java/app/revanced/manager/data/room/bundles/PatchBundleEntity.kt +++ b/app/src/main/java/app/revanced/manager/data/room/bundles/PatchBundleEntity.kt @@ -38,7 +38,9 @@ data class PatchBundleEntity( @ColumnInfo(name = "auto_update") val autoUpdate: Boolean ) -data class BundleProperties( +data class PatchBundleProperties( + @ColumnInfo(name = "name") val name: String, @ColumnInfo(name = "version") val versionHash: String? = null, + @ColumnInfo(name = "source") val source: Source, @ColumnInfo(name = "auto_update") val autoUpdate: Boolean ) \ No newline at end of file diff --git a/app/src/main/java/app/revanced/manager/di/RepositoryModule.kt b/app/src/main/java/app/revanced/manager/di/RepositoryModule.kt index 159436d4..5fce77ec 100644 --- a/app/src/main/java/app/revanced/manager/di/RepositoryModule.kt +++ b/app/src/main/java/app/revanced/manager/di/RepositoryModule.kt @@ -15,7 +15,6 @@ val repositoryModule = module { createdAtStart() } singleOf(::NetworkInfo) - singleOf(::PatchBundlePersistenceRepository) singleOf(::PatchSelectionRepository) singleOf(::PatchOptionsRepository) singleOf(::PatchBundleRepository) { diff --git a/app/src/main/java/app/revanced/manager/di/ViewModelModule.kt b/app/src/main/java/app/revanced/manager/di/ViewModelModule.kt index 4846510f..6970e886 100644 --- a/app/src/main/java/app/revanced/manager/di/ViewModelModule.kt +++ b/app/src/main/java/app/revanced/manager/di/ViewModelModule.kt @@ -23,4 +23,5 @@ val viewModelModule = module { viewModelOf(::InstalledAppsViewModel) viewModelOf(::InstalledAppInfoViewModel) viewModelOf(::UpdatesSettingsViewModel) + viewModelOf(::BundleListViewModel) } diff --git a/app/src/main/java/app/revanced/manager/domain/bundles/LocalPatchBundle.kt b/app/src/main/java/app/revanced/manager/domain/bundles/LocalPatchBundle.kt index a940e05d..ec05c8df 100644 --- a/app/src/main/java/app/revanced/manager/domain/bundles/LocalPatchBundle.kt +++ b/app/src/main/java/app/revanced/manager/domain/bundles/LocalPatchBundle.kt @@ -1,21 +1,29 @@ package app.revanced.manager.domain.bundles +import app.revanced.manager.data.redux.ActionContext import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.withContext import java.io.File import java.io.InputStream -class LocalPatchBundle(name: String, id: Int, directory: File) : - PatchBundleSource(name, id, directory) { - suspend fun replace(patches: InputStream) { +class LocalPatchBundle( + name: String, + uid: Int, + error: Throwable?, + directory: File +) : PatchBundleSource(name, uid, error, directory) { + suspend fun ActionContext.replace(patches: InputStream) { withContext(Dispatchers.IO) { patchBundleOutputStream().use { outputStream -> patches.copyTo(outputStream) } } - - reload()?.also { - saveVersionHash(it.patchBundleManifestAttributes?.version) - } } + + override fun copy(error: Throwable?, name: String) = LocalPatchBundle( + name, + uid, + error, + directory + ) } diff --git a/app/src/main/java/app/revanced/manager/domain/bundles/PatchBundleSource.kt b/app/src/main/java/app/revanced/manager/domain/bundles/PatchBundleSource.kt index bd779dd1..8414e2e1 100644 --- a/app/src/main/java/app/revanced/manager/domain/bundles/PatchBundleSource.kt +++ b/app/src/main/java/app/revanced/manager/domain/bundles/PatchBundleSource.kt @@ -1,22 +1,10 @@ package app.revanced.manager.domain.bundles -import android.app.Application -import android.util.Log -import androidx.compose.runtime.Composable import androidx.compose.runtime.Stable -import androidx.lifecycle.compose.collectAsStateWithLifecycle -import app.revanced.manager.R -import app.revanced.manager.domain.repository.PatchBundlePersistenceRepository +import app.revanced.manager.data.redux.ActionContext import app.revanced.manager.patcher.patch.PatchBundle -import app.revanced.manager.util.tag import kotlinx.coroutines.Dispatchers -import kotlinx.coroutines.flow.MutableStateFlow -import kotlinx.coroutines.flow.asStateFlow -import kotlinx.coroutines.flow.first -import kotlinx.coroutines.flow.flowOn -import kotlinx.coroutines.flow.map -import org.koin.core.component.KoinComponent -import org.koin.core.component.inject +import kotlinx.coroutines.withContext import java.io.File import java.io.OutputStream @@ -24,27 +12,32 @@ import java.io.OutputStream * A [PatchBundle] source. */ @Stable -sealed class PatchBundleSource(initialName: String, val uid: Int, directory: File) : KoinComponent { - protected val configRepository: PatchBundlePersistenceRepository by inject() - private val app: Application by inject() +sealed class PatchBundleSource( + val name: String, + val uid: Int, + error: Throwable?, + protected val directory: File +) { protected val patchesFile = directory.resolve("patches.jar") - private val _state = MutableStateFlow(load()) - val state = _state.asStateFlow() + val state = when { + error != null -> State.Failed(error) + !hasInstalled() -> State.Missing + else -> State.Available(PatchBundle(patchesFile.absolutePath)) + } - private val _nameFlow = MutableStateFlow(initialName) - val nameFlow = - _nameFlow.map { it.ifEmpty { app.getString(if (isDefault) R.string.patches_name_default else R.string.patches_name_fallback) } } + val patchBundle get() = (state as? State.Available)?.bundle + val version get() = patchBundle?.manifestAttributes?.version + val isNameOutOfDate get() = patchBundle?.manifestAttributes?.name?.let { it != name } == true + val error get() = (state as? State.Failed)?.throwable - suspend fun getName() = nameFlow.first() + suspend fun ActionContext.deleteLocalFile() = withContext(Dispatchers.IO) { + patchesFile.delete() + } - val versionFlow = state.map { it.patchBundleOrNull()?.patchBundleManifestAttributes?.version } - val patchCountFlow = state.map { it.patchBundleOrNull()?.patches?.size ?: 0 } + abstract fun copy(error: Throwable? = this.error, name: String = this.name): PatchBundleSource - /** - * Returns true if the bundle has been downloaded to local storage. - */ - fun hasInstalled() = patchesFile.exists() + protected fun hasInstalled() = patchesFile.exists() protected fun patchBundleOutputStream(): OutputStream = with(patchesFile) { // Android 14+ requires dex containers to be readonly. @@ -56,62 +49,14 @@ sealed class PatchBundleSource(initialName: String, val uid: Int, directory: Fil } } - private fun load(): State { - if (!hasInstalled()) return State.Missing - - return try { - State.Loaded(PatchBundle(patchesFile)) - } catch (t: Throwable) { - Log.e(tag, "Failed to load patch bundle with UID $uid", t) - State.Failed(t) - } - } - - suspend fun reload(): PatchBundle? { - val newState = load() - _state.value = newState - - val bundle = newState.patchBundleOrNull() - // Try to read the name from the patch bundle manifest if the bundle does not have a name. - if (bundle != null && _nameFlow.value.isEmpty()) { - bundle.patchBundleManifestAttributes?.name?.let { setName(it) } - } - - return bundle - } - - /** - * Create a flow that emits the [app.revanced.manager.data.room.bundles.BundleProperties] of this [PatchBundleSource]. - * The flow will emit null if the associated [PatchBundleSource] is deleted. - */ - fun propsFlow() = configRepository.getProps(uid).flowOn(Dispatchers.Default) - suspend fun getProps() = propsFlow().first()!! - - suspend fun currentVersionHash() = getProps().versionHash - protected suspend fun saveVersionHash(version: String?) = - configRepository.updateVersionHash(uid, version) - - suspend fun setName(name: String) { - configRepository.setName(uid, name) - _nameFlow.value = name - } - sealed interface State { - fun patchBundleOrNull(): PatchBundle? = null - data object Missing : State data class Failed(val throwable: Throwable) : State - data class Loaded(val bundle: PatchBundle) : State { - override fun patchBundleOrNull() = bundle - } + data class Available(val bundle: PatchBundle) : State } companion object Extensions { val PatchBundleSource.isDefault inline get() = uid == 0 val PatchBundleSource.asRemoteOrNull inline get() = this as? RemotePatchBundle - val PatchBundleSource.nameState - @Composable inline get() = nameFlow.collectAsStateWithLifecycle( - "" - ) } } \ No newline at end of file diff --git a/app/src/main/java/app/revanced/manager/domain/bundles/RemotePatchBundle.kt b/app/src/main/java/app/revanced/manager/domain/bundles/RemotePatchBundle.kt index 897312f7..ebec8471 100644 --- a/app/src/main/java/app/revanced/manager/domain/bundles/RemotePatchBundle.kt +++ b/app/src/main/java/app/revanced/manager/domain/bundles/RemotePatchBundle.kt @@ -1,6 +1,6 @@ package app.revanced.manager.domain.bundles -import androidx.compose.runtime.Stable +import app.revanced.manager.data.redux.ActionContext import app.revanced.manager.network.api.ReVancedAPI import app.revanced.manager.network.dto.ReVancedAsset import app.revanced.manager.network.service.HttpService @@ -8,15 +8,24 @@ import app.revanced.manager.network.utils.getOrThrow import io.ktor.client.request.url import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.withContext +import org.koin.core.component.KoinComponent import org.koin.core.component.inject import java.io.File -@Stable -sealed class RemotePatchBundle(name: String, id: Int, directory: File, val endpoint: String) : - PatchBundleSource(name, id, directory) { +sealed class RemotePatchBundle( + name: String, + uid: Int, + protected val versionHash: String?, + error: Throwable?, + directory: File, + val endpoint: String, + val autoUpdate: Boolean, +) : PatchBundleSource(name, uid, error, directory), KoinComponent { protected val http: HttpService by inject() protected abstract suspend fun getLatestInfo(): ReVancedAsset + abstract fun copy(error: Throwable? = this.error, name: String = this.name, autoUpdate: Boolean = this.autoUpdate): RemotePatchBundle + override fun copy(error: Throwable?, name: String): RemotePatchBundle = copy(error, name, this.autoUpdate) private suspend fun download(info: ReVancedAsset) = withContext(Dispatchers.IO) { patchBundleOutputStream().use { @@ -25,47 +34,72 @@ sealed class RemotePatchBundle(name: String, id: Int, directory: File, val endpo } } - saveVersionHash(info.version) - reload() + info.version } - suspend fun downloadLatest() { - download(getLatestInfo()) - } + /** + * Downloads the latest version regardless if there is a new update available. + */ + suspend fun ActionContext.downloadLatest() = download(getLatestInfo()) - suspend fun update(): Boolean = withContext(Dispatchers.IO) { + suspend fun ActionContext.update(): String? = withContext(Dispatchers.IO) { val info = getLatestInfo() - if (hasInstalled() && info.version == currentVersionHash()) - return@withContext false + if (hasInstalled() && info.version == versionHash) + return@withContext null download(info) - true } - suspend fun deleteLocalFiles() = withContext(Dispatchers.Default) { - patchesFile.delete() - reload() - } - - suspend fun setAutoUpdate(value: Boolean) = configRepository.setAutoUpdate(uid, value) - companion object { const val updateFailMsg = "Failed to update patches" } } -class JsonPatchBundle(name: String, id: Int, directory: File, endpoint: String) : - RemotePatchBundle(name, id, directory, endpoint) { +class JsonPatchBundle( + name: String, + uid: Int, + versionHash: String?, + error: Throwable?, + directory: File, + endpoint: String, + autoUpdate: Boolean, +) : RemotePatchBundle(name, uid, versionHash, error, directory, endpoint, autoUpdate) { override suspend fun getLatestInfo() = withContext(Dispatchers.IO) { http.request { url(endpoint) }.getOrThrow() } + + override fun copy(error: Throwable?, name: String, autoUpdate: Boolean) = JsonPatchBundle( + name, + uid, + versionHash, + error, + directory, + endpoint, + autoUpdate, + ) } -class APIPatchBundle(name: String, id: Int, directory: File, endpoint: String) : - RemotePatchBundle(name, id, directory, endpoint) { +class APIPatchBundle( + name: String, + uid: Int, + versionHash: String?, + error: Throwable?, + directory: File, + endpoint: String, + autoUpdate: Boolean, +) : RemotePatchBundle(name, uid, versionHash, error, directory, endpoint, autoUpdate) { private val api: ReVancedAPI by inject() override suspend fun getLatestInfo() = api.getPatchesUpdate().getOrThrow() + override fun copy(error: Throwable?, name: String, autoUpdate: Boolean) = APIPatchBundle( + name, + uid, + versionHash, + error, + directory, + endpoint, + autoUpdate, + ) } \ No newline at end of file diff --git a/app/src/main/java/app/revanced/manager/domain/repository/PatchBundlePersistenceRepository.kt b/app/src/main/java/app/revanced/manager/domain/repository/PatchBundlePersistenceRepository.kt deleted file mode 100644 index 16fb5976..00000000 --- a/app/src/main/java/app/revanced/manager/domain/repository/PatchBundlePersistenceRepository.kt +++ /dev/null @@ -1,58 +0,0 @@ -package app.revanced.manager.domain.repository - -import app.revanced.manager.data.room.AppDatabase -import app.revanced.manager.data.room.AppDatabase.Companion.generateUid -import app.revanced.manager.data.room.bundles.PatchBundleEntity -import app.revanced.manager.data.room.bundles.Source -import kotlinx.coroutines.flow.distinctUntilChanged - -class PatchBundlePersistenceRepository(db: AppDatabase) { - private val dao = db.patchBundleDao() - - suspend fun loadConfiguration(): List { - val all = dao.all() - if (all.isEmpty()) { - dao.add(defaultSource) - return listOf(defaultSource) - } - - return all - } - - suspend fun reset() = dao.reset() - - suspend fun create(name: String, source: Source, autoUpdate: Boolean = false) = - PatchBundleEntity( - uid = generateUid(), - name = name, - versionHash = null, - source = source, - autoUpdate = autoUpdate - ).also { - dao.add(it) - } - - suspend fun delete(uid: Int) = dao.remove(uid) - - /** - * Sets the version hash used for updates. - */ - suspend fun updateVersionHash(uid: Int, versionHash: String?) = - dao.updateVersionHash(uid, versionHash) - - suspend fun setAutoUpdate(uid: Int, value: Boolean) = dao.setAutoUpdate(uid, value) - - suspend fun setName(uid: Int, name: String) = dao.setName(uid, name) - - fun getProps(id: Int) = dao.getPropsById(id).distinctUntilChanged() - - private companion object { - val defaultSource = PatchBundleEntity( - uid = 0, - name = "", - versionHash = null, - source = Source.API, - autoUpdate = false - ) - } -} \ No newline at end of file diff --git a/app/src/main/java/app/revanced/manager/domain/repository/PatchBundleRepository.kt b/app/src/main/java/app/revanced/manager/domain/repository/PatchBundleRepository.kt index 3afe4e39..ab8dbb19 100644 --- a/app/src/main/java/app/revanced/manager/domain/repository/PatchBundleRepository.kt +++ b/app/src/main/java/app/revanced/manager/domain/repository/PatchBundleRepository.kt @@ -3,55 +3,78 @@ package app.revanced.manager.domain.repository import android.app.Application import android.content.Context import android.util.Log +import androidx.annotation.StringRes import app.revanced.library.mostCommonCompatibleVersions import app.revanced.manager.R import app.revanced.manager.data.platform.NetworkInfo +import app.revanced.manager.data.redux.Action +import app.revanced.manager.data.redux.ActionContext +import app.revanced.manager.data.redux.Store +import app.revanced.manager.data.room.AppDatabase +import app.revanced.manager.data.room.AppDatabase.Companion.generateUid import app.revanced.manager.data.room.bundles.PatchBundleEntity +import app.revanced.manager.data.room.bundles.PatchBundleProperties +import app.revanced.manager.data.room.bundles.Source import app.revanced.manager.domain.bundles.APIPatchBundle import app.revanced.manager.domain.bundles.JsonPatchBundle import app.revanced.manager.data.room.bundles.Source as SourceInfo import app.revanced.manager.domain.bundles.LocalPatchBundle import app.revanced.manager.domain.bundles.RemotePatchBundle import app.revanced.manager.domain.bundles.PatchBundleSource +import app.revanced.manager.domain.bundles.PatchBundleSource.Extensions.isDefault import app.revanced.manager.domain.manager.PreferencesManager import app.revanced.manager.patcher.patch.PatchInfo -import app.revanced.manager.util.flatMapLatestAndCombine +import app.revanced.manager.patcher.patch.PatchBundle +import app.revanced.manager.patcher.patch.PatchBundleInfo +import app.revanced.manager.util.simpleMessage import app.revanced.manager.util.tag -import app.revanced.manager.util.uiSafe +import app.revanced.manager.util.toast +import kotlinx.collections.immutable.* +import kotlinx.coroutines.CancellationException +import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.async +import kotlinx.coroutines.awaitAll import kotlinx.coroutines.coroutineScope -import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.first import kotlinx.coroutines.flow.map -import kotlinx.coroutines.flow.update -import kotlinx.coroutines.launch import kotlinx.coroutines.withContext import java.io.InputStream +import kotlin.collections.joinToString +import kotlin.collections.map +import kotlin.text.ifEmpty class PatchBundleRepository( private val app: Application, - private val persistenceRepo: PatchBundlePersistenceRepository, private val networkInfo: NetworkInfo, private val prefs: PreferencesManager, + db: AppDatabase, ) { + private val dao = db.patchBundleDao() private val bundlesDir = app.getDir("patch_bundles", Context.MODE_PRIVATE) - private val _sources: MutableStateFlow> = - MutableStateFlow(emptyMap()) - val sources = _sources.map { it.values.toList() } + private val store = Store(CoroutineScope(Dispatchers.Default), State()) - val bundles = sources.flatMapLatestAndCombine( - combiner = { - it.mapNotNull { (uid, state) -> - val bundle = state.patchBundleOrNull() ?: return@mapNotNull null - uid to bundle - }.toMap() + val sources = store.state.map { it.sources.values.toList() } + val bundles = store.state.map { + it.sources.mapNotNull { (uid, src) -> + uid to (src.patchBundle ?: return@mapNotNull null) + }.toMap() + } + val bundleInfoFlow = store.state.map { it.info } + + fun scopedBundleInfoFlow(packageName: String, version: String?) = bundleInfoFlow.map { + it.map { (_, bundleInfo) -> + bundleInfo.forPackage( + packageName, + version + ) } - ) { - it.state.map { state -> it.uid to state } } - val suggestedVersions = bundles.map { + val patchCountsFlow = bundleInfoFlow.map { it.mapValues { (_, info) -> info.patches.size } } + + val suggestedVersions = bundleInfoFlow.map { val allPatches = it.values.flatMap { bundle -> bundle.patches.map(PatchInfo::toPatcherPatch) }.toSet() @@ -74,6 +97,100 @@ class PatchBundleRepository( } } + private suspend inline fun dispatchAction( + name: String, + crossinline block: suspend ActionContext.(current: State) -> State + ) { + store.dispatch(object : Action { + override suspend fun ActionContext.execute(current: State) = block(current) + override fun toString() = name + }) + } + + /** + * Performs a reload. Do not call this outside of a store action. + */ + private suspend fun doReload(): State { + val entities = loadFromDb().onEach { + Log.d(tag, "Bundle: $it") + } + + val sources = entities.associate { it.uid to it.load() }.toPersistentMap() + + val hasOutOfDateNames = sources.values.any { it.isNameOutOfDate } + if (hasOutOfDateNames) dispatchAction( + "Sync names" + ) { state -> + val nameChanges = state.sources.mapNotNull { (_, src) -> + if (!src.isNameOutOfDate) return@mapNotNull null + val newName = src.patchBundle?.manifestAttributes?.name?.takeIf { it != src.name } + ?: return@mapNotNull null + + src.uid to newName + } + val sources = state.sources.toMutableMap() + val info = state.info.toMutableMap() + nameChanges.forEach { (uid, name) -> + updateDb(uid) { it.copy(name = name) } + sources[uid] = sources[uid]!!.copy(name = name) + info[uid] = info[uid]?.copy(name = name) ?: return@forEach + } + + State(sources.toPersistentMap(), info.toPersistentMap()) + } + val info = loadMetadata(sources).toPersistentMap() + + return State(sources, info) + } + + suspend fun reload() = dispatchAction("Full reload") { + doReload() + } + + private suspend fun loadFromDb(): List { + val all = dao.all() + if (all.isEmpty()) { + dao.upsert(defaultSource) + return listOf(defaultSource) + } + + return all + } + + private suspend fun loadMetadata(sources: Map): Map { + // Map bundles -> sources + val map = sources.mapNotNull { (_, src) -> + (src.patchBundle ?: return@mapNotNull null) to src + }.toMap() + + val metadata = try { + PatchBundle.Loader.metadata(map.keys) + } catch (error: Throwable) { + val uids = map.values.map { it.uid } + + dispatchAction("Mark bundles as failed") { state -> + state.copy(sources = state.sources.mutate { + uids.forEach { uid -> + it[uid] = it[uid]?.copy(error = error) ?: return@forEach + } + }) + } + + Log.e(tag, "Failed to load bundles", error) + emptyMap() + } + + return metadata.entries.associate { (bundle, patches) -> + val src = map[bundle]!! + src.uid to PatchBundleInfo.Global( + src.name, + bundle.manifestAttributes?.version, + src.uid, + patches + ) + } + } + suspend fun isVersionAllowed(packageName: String, version: String) = withContext(Dispatchers.Default) { if (!prefs.suggestedVersionSafeguard.get()) return@withContext true @@ -89,96 +206,211 @@ class PatchBundleRepository( private fun PatchBundleEntity.load(): PatchBundleSource { val dir = directoryOf(uid) + val actualName = + name.ifEmpty { app.getString(if (uid == 0) R.string.patches_name_default else R.string.patches_name_fallback) } return when (source) { - is SourceInfo.Local -> LocalPatchBundle(name, uid, dir) - is SourceInfo.API -> APIPatchBundle(name, uid, dir, SourceInfo.API.SENTINEL) - is SourceInfo.Remote -> JsonPatchBundle( - name, + is SourceInfo.Local -> LocalPatchBundle(actualName, uid, null, dir) + is SourceInfo.API -> APIPatchBundle( + actualName, uid, + versionHash, + null, dir, - source.url.toString() + SourceInfo.API.SENTINEL, + autoUpdate, + ) + + is SourceInfo.Remote -> JsonPatchBundle( + actualName, + uid, + versionHash, + null, + dir, + source.url.toString(), + autoUpdate, ) } } - suspend fun reload() = withContext(Dispatchers.Default) { - val entities = persistenceRepo.loadConfiguration().onEach { - Log.d(tag, "Bundle: $it") + private suspend fun createEntity(name: String, source: Source, autoUpdate: Boolean = false) = + PatchBundleEntity( + uid = generateUid(), + name = name, + versionHash = null, + source = source, + autoUpdate = autoUpdate + ).also { + dao.upsert(it) } - _sources.value = entities.associate { - it.uid to it.load() - } + /** + * Updates a patch bundle in the database. Do not use this outside an action. + */ + private suspend fun updateDb( + uid: Int, + block: (PatchBundleProperties) -> PatchBundleProperties + ) { + val previous = dao.getProps(uid)!! + val new = block(previous) + dao.upsert( + PatchBundleEntity( + uid = uid, + name = new.name, + versionHash = new.versionHash, + source = new.source, + autoUpdate = new.autoUpdate, + ) + ) } - suspend fun reset() = withContext(Dispatchers.Default) { - persistenceRepo.reset() - _sources.value = emptyMap() - bundlesDir.apply { - deleteRecursively() - mkdirs() - } - - reload() + suspend fun reset() = dispatchAction("Reset") { state -> + dao.reset() + state.sources.keys.forEach { directoryOf(it).deleteRecursively() } + doReload() } - suspend fun remove(bundle: PatchBundleSource) = withContext(Dispatchers.Default) { - persistenceRepo.delete(bundle.uid) - directoryOf(bundle.uid).deleteRecursively() + suspend fun remove(vararg bundles: PatchBundleSource) = + dispatchAction("Remove (${bundles.map { it.uid }.joinToString(",")})") { state -> + val sources = state.sources.toMutableMap() + val info = state.info.toMutableMap() + bundles.forEach { + if (it.isDefault) return@forEach - _sources.update { - it.filterKeys { key -> - key != bundle.uid + dao.remove(it.uid) + directoryOf(it.uid).deleteRecursively() + sources.remove(it.uid) + info.remove(it.uid) } - } - } - private fun addBundle(patchBundle: PatchBundleSource) = - _sources.update { it.toMutableMap().apply { put(patchBundle.uid, patchBundle) } } - - suspend fun createLocal(patches: InputStream) = withContext(Dispatchers.Default) { - val uid = persistenceRepo.create("", SourceInfo.Local).uid - val bundle = LocalPatchBundle("", uid, directoryOf(uid)) - - bundle.replace(patches) - addBundle(bundle) - } - - suspend fun createRemote(url: String, autoUpdate: Boolean) = withContext(Dispatchers.Default) { - val entity = persistenceRepo.create("", SourceInfo.from(url), autoUpdate) - addBundle(entity.load()) - } - - private suspend inline fun getBundlesByType() = - sources.first().filterIsInstance() - - suspend fun reloadApiBundles() { - getBundlesByType().forEach { - it.deleteLocalFiles() + State(sources.toPersistentMap(), info.toPersistentMap()) } - reload() - } - - suspend fun redownloadRemoteBundles() = - getBundlesByType().forEach { it.downloadLatest() } - - suspend fun updateCheck() = - uiSafe(app, R.string.patches_download_fail, "Failed to update bundles") { - coroutineScope { - if (!networkInfo.isSafe()) { - Log.d(tag, "Skipping update check because the network is down or metered.") - return@coroutineScope + suspend fun createLocal(createStream: suspend () -> InputStream) = dispatchAction("Add bundle") { + with(createEntity("", SourceInfo.Local).load() as LocalPatchBundle) { + try { + createStream().use { patches -> replace(patches) } + } catch (e: Exception) { + if (e is CancellationException) throw e + Log.e(tag, "Got exception while importing bundle", e) + withContext(Dispatchers.Main) { + app.toast(app.getString(R.string.patches_replace_fail, e.simpleMessage())) } - getBundlesByType().forEach { - launch { - if (!it.getProps().autoUpdate) return@launch - Log.d(tag, "Updating patch bundle: ${it.getName()}") - it.update() + deleteLocalFile() + } + } + + doReload() + } + + suspend fun createRemote(url: String, autoUpdate: Boolean) = + dispatchAction("Add bundle ($url)") { state -> + val src = createEntity("", SourceInfo.from(url), autoUpdate).load() as RemotePatchBundle + update(src) + state.copy(sources = state.sources.put(src.uid, src)) + } + + suspend fun reloadApiBundles() = dispatchAction("Reload API bundles") { + this@PatchBundleRepository.sources.first().filterIsInstance().forEach { + with(it) { deleteLocalFile() } + updateDb(it.uid) { it.copy(versionHash = null) } + } + + doReload() + } + + suspend fun RemotePatchBundle.setAutoUpdate(value: Boolean) = + dispatchAction("Set auto update ($name, $value)") { state -> + updateDb(uid) { it.copy(autoUpdate = value) } + val newSrc = (state.sources[uid] as? RemotePatchBundle)?.copy(autoUpdate = value) + ?: return@dispatchAction state + + state.copy(sources = state.sources.put(uid, newSrc)) + } + + suspend fun update(vararg sources: RemotePatchBundle, showToast: Boolean = false) { + val uids = sources.map { it.uid }.toSet() + store.dispatch(Update(showToast = showToast) { it.uid in uids }) + } + + suspend fun redownloadRemoteBundles() = store.dispatch(Update(force = true)) + + /** + * Updates all bundles that should be automatically updated. + */ + suspend fun updateCheck() = store.dispatch(Update { it.autoUpdate }) + + private inner class Update( + private val force: Boolean = false, + private val showToast: Boolean = false, + private val predicate: (bundle: RemotePatchBundle) -> Boolean = { true }, + ) : Action { + private suspend fun toast(@StringRes id: Int, vararg args: Any?) = + withContext(Dispatchers.Main) { app.toast(app.getString(id, *args)) } + + override fun toString() = if (force) "Redownload remote bundles" else "Update check" + + override suspend fun ActionContext.execute( + current: State + ) = coroutineScope { + if (!networkInfo.isSafe()) { + Log.d(tag, "Skipping update check because the network is down or metered.") + return@coroutineScope current + } + + val updated = current.sources.values + .filterIsInstance() + .filter { predicate(it) } + .map { + async { + Log.d(tag, "Updating patch bundle: ${it.name}") + + val newVersion = with(it) { + if (force) downloadLatest() else update() + } ?: return@async null + + it to newVersion } } + .awaitAll() + .filterNotNull() + .toMap() + if (updated.isEmpty()) { + if (showToast) toast(R.string.patches_update_unavailable) + return@coroutineScope current } + + updated.forEach { (src, newVersionHash) -> + val name = src.patchBundle?.manifestAttributes?.name ?: src.name + + updateDb(src.uid) { + it.copy(versionHash = newVersionHash, name = name) + } + } + + if (showToast) toast(R.string.patches_update_success) + doReload() } + + override suspend fun catch(exception: Exception) { + Log.e(tag, "Failed to update patches", exception) + toast(R.string.patches_download_fail, exception.simpleMessage()) + } + } + + data class State( + val sources: PersistentMap = persistentMapOf(), + val info: PersistentMap = persistentMapOf() + ) + + private companion object { + val defaultSource = PatchBundleEntity( + uid = 0, + name = "", + versionHash = null, + source = Source.API, + autoUpdate = false + ) + } } \ No newline at end of file diff --git a/app/src/main/java/app/revanced/manager/patcher/patch/PatchBundle.kt b/app/src/main/java/app/revanced/manager/patcher/patch/PatchBundle.kt index e894f748..3a1a0172 100644 --- a/app/src/main/java/app/revanced/manager/patcher/patch/PatchBundle.kt +++ b/app/src/main/java/app/revanced/manager/patcher/patch/PatchBundle.kt @@ -1,80 +1,84 @@ package app.revanced.manager.patcher.patch -import android.util.Log -import app.revanced.manager.util.tag -import app.revanced.patcher.patch.Patch -import app.revanced.patcher.patch.PatchLoader +import kotlinx.parcelize.IgnoredOnParcel +import android.os.Parcelable +import app.revanced.patcher.patch.loadPatchesFromDex +import kotlinx.parcelize.Parcelize import java.io.File import java.io.IOException import java.util.jar.JarFile +import kotlin.collections.filter -class PatchBundleManifestAttributes( - val name: String?, - val version: String?, - val description: String?, - val source: String?, - val author: String?, - val contact: String?, - val website: String?, - val license: String? -) - -class PatchBundle(val patchesJar: File) { - private val loader = object : Iterable> { - private fun load(): Iterable> { - patchesJar.setReadOnly() - return PatchLoader.Dex(setOf(patchesJar)) - } - - override fun iterator(): Iterator> = load().iterator() - } - - init { - Log.d(tag, "Loaded patch bundle: $patchesJar") - } - - /** - * A list containing the metadata of every patch inside this bundle. - */ - val patches = loader.map(::PatchInfo) - +@Parcelize +data class PatchBundle(val patchesJar: String) : Parcelable { /** * The [java.util.jar.Manifest] of [patchesJar]. */ - private val manifest = try { - JarFile(patchesJar).use { it.manifest } - } catch (_: IOException) { - null + @IgnoredOnParcel + private val manifest by lazy { + try { + JarFile(patchesJar).use { it.manifest } + } catch (_: IOException) { + null + } } - val patchBundleManifestAttributes = if(manifest != null) - PatchBundleManifestAttributes( - name = readManifestAttribute("name"), - version = readManifestAttribute("version"), - description = readManifestAttribute("description"), - source = readManifestAttribute("source"), - author = readManifestAttribute("author"), - contact = readManifestAttribute("contact"), - website = readManifestAttribute("website"), - license = readManifestAttribute("license") - ) else + @IgnoredOnParcel + val manifestAttributes by lazy { + if (manifest != null) + ManifestAttributes( + name = readManifestAttribute("name"), + version = readManifestAttribute("version"), + description = readManifestAttribute("description"), + source = readManifestAttribute("source"), + author = readManifestAttribute("author"), + contact = readManifestAttribute("contact"), + website = readManifestAttribute("website"), + license = readManifestAttribute("license") + ) else null + } - private fun readManifestAttribute(name: String) = manifest?.mainAttributes?.getValue(name)?.takeIf { it.isNotBlank() } // If empty, set it to null instead. + private fun readManifestAttribute(name: String) = manifest?.mainAttributes?.getValue(name) + ?.takeIf { it.isNotBlank() } // If empty, set it to null instead. - /** - * Load all patches compatible with the specified package. - */ - fun patches(packageName: String) = loader.filter { patch -> - val compatiblePackages = patch.compatiblePackages - ?: // The patch has no compatibility constraints, which means it is universal. - return@filter true + data class ManifestAttributes( + val name: String?, + val version: String?, + val description: String?, + val source: String?, + val author: String?, + val contact: String?, + val website: String?, + val license: String? + ) - if (!compatiblePackages.any { (name, _) -> name == packageName }) { - // Patch is not compatible with this package. - return@filter false - } + object Loader { + private fun patches(bundles: Iterable) = + loadPatchesFromDex( + bundles.map { File(it.patchesJar) }.toSet() + ).byPatchesFile.mapKeys { (file, _) -> + val absPath = file.absolutePath + bundles.single { absPath == it.patchesJar } + } - true + fun metadata(bundles: Iterable) = + patches(bundles).mapValues { (_, patches) -> patches.map(::PatchInfo) } + + fun patches(bundles: Iterable, packageName: String) = + patches(bundles).mapValues { (_, patches) -> + patches.filter { patch -> + val compatiblePackages = patch.compatiblePackages + ?: // The patch has no compatibility constraints, which means it is universal. + return@filter true + + if (!compatiblePackages.any { (name, _) -> name == packageName }) { + // Patch is not compatible with this package. + return@filter false + } + + true + }.toSet() + } } } diff --git a/app/src/main/java/app/revanced/manager/patcher/patch/PatchBundleInfo.kt b/app/src/main/java/app/revanced/manager/patcher/patch/PatchBundleInfo.kt new file mode 100644 index 00000000..e31cecf6 --- /dev/null +++ b/app/src/main/java/app/revanced/manager/patcher/patch/PatchBundleInfo.kt @@ -0,0 +1,145 @@ +package app.revanced.manager.patcher.patch + +import app.revanced.manager.util.PatchSelection + +/** + * A base class for storing [PatchBundle] metadata. + */ +sealed class PatchBundleInfo { + /** + * The name of the bundle. + */ + abstract val name: String + + /** + * The version of the bundle. + */ + abstract val version: String? + + /** + * The unique ID of the bundle. + */ + abstract val uid: Int + + /** + * The patch list. + */ + abstract val patches: List + + /** + * Information about a bundle and all the patches it contains. + * + * @see [PatchBundleInfo] + */ + data class Global( + override val name: String, + override val version: String?, + override val uid: Int, + override val patches: List + ) : PatchBundleInfo() { + /** + * Create a [PatchBundleInfo.Scoped] that only contains information about patches that are relevant for a specific [packageName]. + */ + fun forPackage(packageName: String, version: String?): Scoped { + val relevantPatches = patches.filter { it.compatibleWith(packageName) } + val compatible = mutableListOf() + val incompatible = mutableListOf() + val universal = mutableListOf() + + relevantPatches.forEach { + val targetList = when { + it.compatiblePackages == null -> universal + it.supports( + packageName, + version + ) -> compatible + + else -> incompatible + } + + targetList.add(it) + } + + return Scoped( + name, + this.version, + uid, + relevantPatches, + compatible, + incompatible, + universal + ) + } + } + + /** + * Contains information about a bundle that is relevant for a specific package name. + * + * @param compatible Patches that are compatible with the specified package name and version. + * @param incompatible Patches that are compatible with the specified package name but not version. + * @param universal Patches that are compatible with all packages. + * @see [PatchBundleInfo.Global.forPackage] + * @see [PatchBundleInfo] + */ + data class Scoped( + override val name: String, + override val version: String?, + override val uid: Int, + override val patches: List, + val compatible: List, + val incompatible: List, + val universal: List + ) : PatchBundleInfo() { + fun patchSequence(allowIncompatible: Boolean) = if (allowIncompatible) { + patches.asSequence() + } else { + sequence { + yieldAll(compatible) + yieldAll(universal) + } + } + } + + companion object Extensions { + inline fun Iterable.toPatchSelection( + allowIncompatible: Boolean, + condition: (Int, PatchInfo) -> Boolean + ): PatchSelection = this.associate { bundle -> + val patches = + bundle.patchSequence(allowIncompatible) + .mapNotNullTo(mutableSetOf()) { patch -> + patch.name.takeIf { + condition( + bundle.uid, + patch + ) + } + } + + bundle.uid to patches + } + + /** + * Algorithm for determining whether all required options have been set. + */ + inline fun Iterable.requiredOptionsSet( + allowIncompatible: Boolean, + crossinline isSelected: (Scoped, PatchInfo) -> Boolean, + crossinline optionsForPatch: (Scoped, PatchInfo) -> Map? + ) = all bundle@{ bundle -> + bundle + .patchSequence(allowIncompatible) + .filter { isSelected(bundle, it) } + .all patch@{ + if (it.options.isNullOrEmpty()) return@patch true + val opts by lazy { optionsForPatch(bundle, it).orEmpty() } + + it.options.all option@{ option -> + if (!option.required || option.default != null) return@option true + + option.key in opts + } + } + } + } +} \ No newline at end of file diff --git a/app/src/main/java/app/revanced/manager/patcher/runtime/CoroutineRuntime.kt b/app/src/main/java/app/revanced/manager/patcher/runtime/CoroutineRuntime.kt index eb50bd35..50a96a1f 100644 --- a/app/src/main/java/app/revanced/manager/patcher/runtime/CoroutineRuntime.kt +++ b/app/src/main/java/app/revanced/manager/patcher/runtime/CoroutineRuntime.kt @@ -3,6 +3,7 @@ package app.revanced.manager.patcher.runtime import android.content.Context import app.revanced.manager.patcher.Session import app.revanced.manager.patcher.logger.Logger +import app.revanced.manager.patcher.patch.PatchBundle import app.revanced.manager.patcher.worker.ProgressEventHandler import app.revanced.manager.ui.model.State import app.revanced.manager.util.Options @@ -23,14 +24,17 @@ class CoroutineRuntime(private val context: Context) : Runtime(context) { onPatchCompleted: suspend () -> Unit, onProgress: ProgressEventHandler, ) { - val bundles = bundles() - val selectedBundles = selectedPatches.keys - val allPatches = bundles.filterKeys { selectedBundles.contains(it) } - .mapValues { (_, bundle) -> bundle.patches(packageName) } + val bundles = bundles() + val uids = bundles.entries.associate { (key, value) -> value to key } + + val allPatches = + PatchBundle.Loader.patches(bundles.values, packageName) + .mapKeys { (b, _) -> uids[b]!! } + .filterKeys { it in selectedBundles } val patchList = selectedPatches.flatMap { (bundle, selected) -> - allPatches[bundle]?.filter { selected.contains(it.name) } + allPatches[bundle]?.filter { it.name in selected } ?: throw IllegalArgumentException("Patch bundle $bundle does not exist") } diff --git a/app/src/main/java/app/revanced/manager/patcher/runtime/ProcessRuntime.kt b/app/src/main/java/app/revanced/manager/patcher/runtime/ProcessRuntime.kt index 95f32fd5..2e026298 100644 --- a/app/src/main/java/app/revanced/manager/patcher/runtime/ProcessRuntime.kt +++ b/app/src/main/java/app/revanced/manager/patcher/runtime/ProcessRuntime.kt @@ -142,8 +142,6 @@ class ProcessRuntime(private val context: Context) : Runtime(context) { } } - val bundles = bundles() - val parameters = Parameters( aaptPath = aaptPath, frameworkDir = frameworkPath, @@ -151,13 +149,11 @@ class ProcessRuntime(private val context: Context) : Runtime(context) { packageName = packageName, inputFile = inputFile, outputFile = outputFile, - configurations = selectedPatches.map { (id, patches) -> - val bundle = bundles[id]!! - + configurations = bundles().map { (uid, bundle) -> PatchConfiguration( - bundle.patchesJar.absolutePath, - patches, - options[id].orEmpty() + bundle, + selectedPatches[uid].orEmpty(), + options[uid].orEmpty() ) } ) diff --git a/app/src/main/java/app/revanced/manager/patcher/runtime/process/Parameters.kt b/app/src/main/java/app/revanced/manager/patcher/runtime/process/Parameters.kt index b00d558a..9cdd99e9 100644 --- a/app/src/main/java/app/revanced/manager/patcher/runtime/process/Parameters.kt +++ b/app/src/main/java/app/revanced/manager/patcher/runtime/process/Parameters.kt @@ -1,6 +1,7 @@ package app.revanced.manager.patcher.runtime.process import android.os.Parcelable +import app.revanced.manager.patcher.patch.PatchBundle import kotlinx.parcelize.Parcelize import kotlinx.parcelize.RawValue @@ -17,7 +18,7 @@ data class Parameters( @Parcelize data class PatchConfiguration( - val bundlePath: String, + val bundle: PatchBundle, val patches: Set, val options: @RawValue Map> ) : Parcelable \ No newline at end of file diff --git a/app/src/main/java/app/revanced/manager/patcher/runtime/process/PatcherProcess.kt b/app/src/main/java/app/revanced/manager/patcher/runtime/process/PatcherProcess.kt index 82508b31..f117f201 100644 --- a/app/src/main/java/app/revanced/manager/patcher/runtime/process/PatcherProcess.kt +++ b/app/src/main/java/app/revanced/manager/patcher/runtime/process/PatcherProcess.kt @@ -56,11 +56,10 @@ class PatcherProcess(private val context: Context) : IPatcherProcess.Stub() { logger.info("Memory limit: ${Runtime.getRuntime().maxMemory() / (1024 * 1024)}MB") + val allPatches = PatchBundle.Loader.patches(parameters.configurations.map { it.bundle }, parameters.packageName) val patchList = parameters.configurations.flatMap { config -> - val bundle = PatchBundle(File(config.bundlePath)) - - val patches = - bundle.patches(parameters.packageName).filter { it.name in config.patches } + val patches = (allPatches[config.bundle] ?: return@flatMap emptyList()) + .filter { it.name in config.patches } .associateBy { it.name } config.options.forEach { (patchName, opts) -> diff --git a/app/src/main/java/app/revanced/manager/ui/component/bundle/BaseBundleDialog.kt b/app/src/main/java/app/revanced/manager/ui/component/bundle/BaseBundleDialog.kt deleted file mode 100644 index 3e07af24..00000000 --- a/app/src/main/java/app/revanced/manager/ui/component/bundle/BaseBundleDialog.kt +++ /dev/null @@ -1,198 +0,0 @@ -package app.revanced.manager.ui.component.bundle - -import android.webkit.URLUtil -import androidx.compose.foundation.clickable -import androidx.compose.foundation.layout.Arrangement -import androidx.compose.foundation.layout.Column -import androidx.compose.foundation.layout.ColumnScope -import androidx.compose.foundation.layout.Row -import androidx.compose.foundation.layout.fillMaxWidth -import androidx.compose.foundation.layout.padding -import androidx.compose.foundation.layout.size -import androidx.compose.material.icons.Icons -import androidx.compose.material.icons.automirrored.outlined.ArrowRight -import androidx.compose.material.icons.automirrored.outlined.Send -import androidx.compose.material.icons.outlined.Commit -import androidx.compose.material.icons.outlined.Description -import androidx.compose.material.icons.outlined.Gavel -import androidx.compose.material.icons.outlined.Language -import androidx.compose.material.icons.outlined.Person -import androidx.compose.material.icons.outlined.Sell -import androidx.compose.material3.HorizontalDivider -import androidx.compose.material3.Icon -import androidx.compose.material3.MaterialTheme -import androidx.compose.material3.Text -import androidx.compose.runtime.Composable -import androidx.compose.runtime.getValue -import androidx.compose.runtime.mutableStateOf -import androidx.compose.runtime.saveable.rememberSaveable -import androidx.compose.runtime.setValue -import androidx.compose.ui.Alignment -import androidx.compose.ui.Modifier -import androidx.compose.ui.graphics.vector.ImageVector -import androidx.compose.ui.platform.LocalUriHandler -import androidx.compose.ui.res.stringResource -import androidx.compose.ui.unit.dp -import app.revanced.manager.R -import app.revanced.manager.patcher.patch.PatchBundleManifestAttributes -import app.revanced.manager.ui.component.ColumnWithScrollbar -import app.revanced.manager.ui.component.TextInputDialog -import app.revanced.manager.ui.component.haptics.HapticSwitch - -@Composable -fun BaseBundleDialog( - modifier: Modifier = Modifier, - isDefault: Boolean, - remoteUrl: String?, - onRemoteUrlChange: ((String) -> Unit)? = null, - patchCount: Int, - version: String?, - autoUpdate: Boolean, - bundleManifestAttributes: PatchBundleManifestAttributes?, - onAutoUpdateChange: (Boolean) -> Unit, - onPatchesClick: () -> Unit, - extraFields: @Composable ColumnScope.() -> Unit = {} -) { - ColumnWithScrollbar( - modifier = Modifier - .fillMaxWidth() - .then(modifier), - ) { - Column( - modifier = Modifier.padding(16.dp), - verticalArrangement = Arrangement.spacedBy(4.dp) - ) { - version?.let { - Tag(Icons.Outlined.Sell, it) - } - bundleManifestAttributes?.description?.let { - Tag(Icons.Outlined.Description, it) - } - bundleManifestAttributes?.source?.let { - Tag(Icons.Outlined.Commit, it) - } - bundleManifestAttributes?.author?.let { - Tag(Icons.Outlined.Person, it) - } - bundleManifestAttributes?.contact?.let { - Tag(Icons.AutoMirrored.Outlined.Send, it) - } - bundleManifestAttributes?.website?.let { - Tag(Icons.Outlined.Language, it, isUrl = true) - } - bundleManifestAttributes?.license?.let { - Tag(Icons.Outlined.Gavel, it) - } - } - - HorizontalDivider( - modifier = Modifier.padding(horizontal = 16.dp), - color = MaterialTheme.colorScheme.outlineVariant - ) - - if (remoteUrl != null) { - BundleListItem( - headlineText = stringResource(R.string.auto_update), - supportingText = stringResource(R.string.auto_update_description), - trailingContent = { - HapticSwitch( - checked = autoUpdate, - onCheckedChange = onAutoUpdateChange - ) - }, - modifier = Modifier.clickable { - onAutoUpdateChange(!autoUpdate) - } - ) - } - - remoteUrl?.takeUnless { isDefault }?.let { url -> - var showUrlInputDialog by rememberSaveable { - mutableStateOf(false) - } - if (showUrlInputDialog) { - TextInputDialog( - initial = url, - title = stringResource(R.string.patches_url), - onDismissRequest = { showUrlInputDialog = false }, - onConfirm = { - showUrlInputDialog = false - onRemoteUrlChange?.invoke(it) - }, - validator = { - if (it.isEmpty()) return@TextInputDialog false - - URLUtil.isValidUrl(it) - } - ) - } - - BundleListItem( - modifier = Modifier.clickable( - enabled = onRemoteUrlChange != null, - onClick = { - showUrlInputDialog = true - } - ), - headlineText = stringResource(R.string.patches_url), - supportingText = url.ifEmpty { - stringResource(R.string.field_not_set) - } - ) - } - - val patchesClickable = patchCount > 0 - BundleListItem( - headlineText = stringResource(R.string.patches), - supportingText = stringResource(R.string.view_patches), - modifier = Modifier.clickable( - enabled = patchesClickable, - onClick = onPatchesClick - ) - ) { - if (patchesClickable) { - Icon( - Icons.AutoMirrored.Outlined.ArrowRight, - stringResource(R.string.patches) - ) - } - } - - extraFields() - } -} - -@Composable -private fun Tag( - icon: ImageVector, - text: String, - isUrl: Boolean = false -) { - val uriHandler = LocalUriHandler.current - - Row( - horizontalArrangement = Arrangement.spacedBy(6.dp), - verticalAlignment = Alignment.CenterVertically, - modifier = if (isUrl) { - Modifier - .clickable { - try { - uriHandler.openUri(text) - } catch (_: Exception) {} - } - } - else - Modifier, - ) { - Icon( - imageVector = icon, - contentDescription = null, - modifier = Modifier.size(16.dp) - ) - Text( - text, - style = MaterialTheme.typography.bodyMedium, - color = if(isUrl) MaterialTheme.colorScheme.primary else MaterialTheme.colorScheme.outline, - ) - } -} \ No newline at end of file diff --git a/app/src/main/java/app/revanced/manager/ui/component/bundle/BundleInformationDialog.kt b/app/src/main/java/app/revanced/manager/ui/component/bundle/BundleInformationDialog.kt index 20d2d7c4..9e0bb7c9 100644 --- a/app/src/main/java/app/revanced/manager/ui/component/bundle/BundleInformationDialog.kt +++ b/app/src/main/java/app/revanced/manager/ui/component/bundle/BundleInformationDialog.kt @@ -1,69 +1,99 @@ package app.revanced.manager.ui.component.bundle +import android.webkit.URLUtil.isValidUrl import androidx.compose.foundation.clickable +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.size import androidx.compose.material.icons.Icons import androidx.compose.material.icons.automirrored.filled.ArrowBack import androidx.compose.material.icons.automirrored.outlined.ArrowRight +import androidx.compose.material.icons.automirrored.outlined.Send +import androidx.compose.material.icons.outlined.Commit import androidx.compose.material.icons.outlined.DeleteOutline +import androidx.compose.material.icons.outlined.Description +import androidx.compose.material.icons.outlined.Gavel +import androidx.compose.material.icons.outlined.Language +import androidx.compose.material.icons.outlined.Person +import androidx.compose.material.icons.outlined.Sell import androidx.compose.material.icons.outlined.Update import androidx.compose.material3.* +import androidx.compose.material3.HorizontalDivider +import androidx.compose.material3.Icon +import androidx.compose.material3.MaterialTheme import androidx.compose.runtime.* +import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.saveable.rememberSaveable +import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier +import androidx.compose.ui.graphics.vector.ImageVector +import androidx.compose.ui.platform.LocalUriHandler import androidx.compose.ui.res.stringResource -import androidx.lifecycle.compose.collectAsStateWithLifecycle +import androidx.compose.ui.unit.dp import app.revanced.manager.R +import app.revanced.manager.R.string.auto_update +import app.revanced.manager.R.string.auto_update_description +import app.revanced.manager.R.string.field_not_set +import app.revanced.manager.R.string.patches +import app.revanced.manager.R.string.patches_url +import app.revanced.manager.R.string.view_patches import app.revanced.manager.data.platform.NetworkInfo import app.revanced.manager.domain.bundles.LocalPatchBundle import app.revanced.manager.domain.bundles.PatchBundleSource import app.revanced.manager.domain.bundles.PatchBundleSource.Extensions.asRemoteOrNull import app.revanced.manager.domain.bundles.PatchBundleSource.Extensions.isDefault -import app.revanced.manager.domain.bundles.PatchBundleSource.Extensions.nameState +import app.revanced.manager.domain.repository.PatchBundleRepository +import app.revanced.manager.ui.component.ColumnWithScrollbar import app.revanced.manager.ui.component.ExceptionViewerDialog import app.revanced.manager.ui.component.FullscreenDialog +import app.revanced.manager.ui.component.TextInputDialog +import app.revanced.manager.ui.component.haptics.HapticSwitch import kotlinx.coroutines.launch import org.koin.compose.koinInject @OptIn(ExperimentalMaterial3Api::class) @Composable fun BundleInformationDialog( + src: PatchBundleSource, + patchCount: Int, onDismissRequest: () -> Unit, onDeleteRequest: () -> Unit, - bundle: PatchBundleSource, onUpdate: () -> Unit, ) { + val bundleRepo = koinInject() val networkInfo = koinInject() val hasNetwork = remember { networkInfo.isConnected() } val composableScope = rememberCoroutineScope() var viewCurrentBundlePatches by remember { mutableStateOf(false) } - val isLocal = bundle is LocalPatchBundle - val state by bundle.state.collectAsStateWithLifecycle() - val props by remember(bundle) { - bundle.propsFlow() - }.collectAsStateWithLifecycle(null) - val patchCount by bundle.patchCountFlow.collectAsStateWithLifecycle(0) - val version by bundle.versionFlow.collectAsStateWithLifecycle(null) - val bundleManifestAttributes = state.patchBundleOrNull()?.patchBundleManifestAttributes + val isLocal = src is LocalPatchBundle + val bundleManifestAttributes = src.patchBundle?.manifestAttributes + val (autoUpdate, endpoint) = src.asRemoteOrNull?.let { it.autoUpdate to it.endpoint } ?: (null to null) + + fun onAutoUpdateChange(new: Boolean) = composableScope.launch { + with(bundleRepo) { + src.asRemoteOrNull?.setAutoUpdate(new) + } + } if (viewCurrentBundlePatches) { BundlePatchesDialog( + src = src, onDismissRequest = { viewCurrentBundlePatches = false - }, - bundle = bundle + } ) } FullscreenDialog( onDismissRequest = onDismissRequest, ) { - val bundleName by bundle.nameState - Scaffold( topBar = { BundleTopBar( - title = bundleName, + title = src.name, onBackClick = onDismissRequest, backIcon = { Icon( @@ -72,7 +102,7 @@ fun BundleInformationDialog( ) }, actions = { - if (!bundle.isDefault) { + if (!src.isDefault) { IconButton(onClick = onDeleteRequest) { Icon( Icons.Outlined.DeleteOutline, @@ -92,54 +122,175 @@ fun BundleInformationDialog( ) }, ) { paddingValues -> - BaseBundleDialog( - modifier = Modifier.padding(paddingValues), - isDefault = bundle.isDefault, - remoteUrl = bundle.asRemoteOrNull?.endpoint, - patchCount = patchCount, - version = version, - autoUpdate = props?.autoUpdate == true, - bundleManifestAttributes = bundleManifestAttributes, - onAutoUpdateChange = { - composableScope.launch { - bundle.asRemoteOrNull?.setAutoUpdate(it) + ColumnWithScrollbar( + modifier = Modifier + .fillMaxWidth() + .padding(paddingValues), + ) { + Column( + modifier = Modifier.padding(16.dp), + verticalArrangement = Arrangement.spacedBy(4.dp) + ) { + Tag(Icons.Outlined.Sell, src.name) + bundleManifestAttributes?.description?.let { + Tag(Icons.Outlined.Description, it) } - }, - onPatchesClick = { - viewCurrentBundlePatches = true - }, - extraFields = { - (state as? PatchBundleSource.State.Failed)?.throwable?.let { - var showDialog by rememberSaveable { - mutableStateOf(false) + bundleManifestAttributes?.source?.let { + Tag(Icons.Outlined.Commit, it) + } + bundleManifestAttributes?.author?.let { + Tag(Icons.Outlined.Person, it) + } + bundleManifestAttributes?.contact?.let { + Tag(Icons.AutoMirrored.Outlined.Send, it) + } + bundleManifestAttributes?.website?.let { + Tag(Icons.Outlined.Language, it, isUrl = true) + } + bundleManifestAttributes?.license?.let { + Tag(Icons.Outlined.Gavel, it) + } + } + + HorizontalDivider( + modifier = Modifier.padding(horizontal = 16.dp), + color = MaterialTheme.colorScheme.outlineVariant + ) + + if (autoUpdate != null) { + BundleListItem( + headlineText = stringResource(auto_update), + supportingText = stringResource(auto_update_description), + trailingContent = { + HapticSwitch( + checked = autoUpdate, + onCheckedChange = ::onAutoUpdateChange + ) + }, + modifier = Modifier.clickable { + onAutoUpdateChange(!autoUpdate) } - if (showDialog) ExceptionViewerDialog( - onDismiss = { showDialog = false }, - text = remember(it) { it.stackTraceToString() } - ) + ) + } - BundleListItem( - headlineText = stringResource(R.string.patches_error), - supportingText = stringResource(R.string.patches_error_description), - trailingContent = { - Icon( - Icons.AutoMirrored.Outlined.ArrowRight, - null - ) + endpoint?.takeUnless { src.isDefault }?.let { url -> + var showUrlInputDialog by rememberSaveable { + mutableStateOf(false) + } + if (showUrlInputDialog) { + TextInputDialog( + initial = url, + title = stringResource(patches_url), + onDismissRequest = { showUrlInputDialog = false }, + onConfirm = { + showUrlInputDialog = false + TODO("Not implemented.") }, - modifier = Modifier.clickable { showDialog = true } + validator = { + if (it.isEmpty()) return@TextInputDialog false + + isValidUrl(it) + } ) } - if (state is PatchBundleSource.State.Missing && !isLocal) { - BundleListItem( - headlineText = stringResource(R.string.patches_error), - supportingText = stringResource(R.string.patches_not_downloaded), - modifier = Modifier.clickable(onClick = onUpdate) + BundleListItem( + modifier = Modifier.clickable( + enabled = false, + onClick = { + showUrlInputDialog = true + } + ), + headlineText = stringResource(patches_url), + supportingText = url.ifEmpty { + stringResource(field_not_set) + } + ) + } + + val patchesClickable = patchCount > 0 + BundleListItem( + headlineText = stringResource(patches), + supportingText = stringResource(view_patches), + modifier = Modifier.clickable( + enabled = patchesClickable, + onClick = { + viewCurrentBundlePatches = true + } + ) + ) { + if (patchesClickable) { + Icon( + Icons.AutoMirrored.Outlined.ArrowRight, + stringResource(patches) ) } } - ) + + src.error?.let { + var showDialog by rememberSaveable { + mutableStateOf(false) + } + if (showDialog) ExceptionViewerDialog( + onDismiss = { showDialog = false }, + text = remember(it) { it.stackTraceToString() } + ) + + BundleListItem( + headlineText = stringResource(R.string.patches_error), + supportingText = stringResource(R.string.patches_error_description), + trailingContent = { + Icon( + Icons.AutoMirrored.Outlined.ArrowRight, + null + ) + }, + modifier = Modifier.clickable { showDialog = true } + ) + } + if (src.state is PatchBundleSource.State.Missing && !isLocal) { + BundleListItem( + headlineText = stringResource(R.string.patches_error), + supportingText = stringResource(R.string.patches_not_downloaded), + modifier = Modifier.clickable(onClick = onUpdate) + ) + } + } } } +} + +@Composable +private fun Tag( + icon: ImageVector, + text: String, + isUrl: Boolean = false +) { + val uriHandler = LocalUriHandler.current + + Row( + horizontalArrangement = Arrangement.spacedBy(6.dp), + verticalAlignment = Alignment.CenterVertically, + modifier = if (isUrl) { + Modifier + .clickable { + try { + uriHandler.openUri(text) + } catch (_: Exception) { + } + } + } else + Modifier, + ) { + Icon( + imageVector = icon, + contentDescription = null, + modifier = Modifier.size(16.dp) + ) + Text( + text, + style = MaterialTheme.typography.bodyMedium, + color = if (isUrl) MaterialTheme.colorScheme.primary else MaterialTheme.colorScheme.outline, + ) + } } \ No newline at end of file diff --git a/app/src/main/java/app/revanced/manager/ui/component/bundle/BundleItem.kt b/app/src/main/java/app/revanced/manager/ui/component/bundle/BundleItem.kt index 3501e26e..958616af 100644 --- a/app/src/main/java/app/revanced/manager/ui/component/bundle/BundleItem.kt +++ b/app/src/main/java/app/revanced/manager/ui/component/bundle/BundleItem.kt @@ -24,38 +24,32 @@ import androidx.compose.ui.Modifier import androidx.compose.ui.res.pluralStringResource import androidx.compose.ui.res.stringResource import androidx.compose.ui.unit.dp -import androidx.lifecycle.compose.collectAsStateWithLifecycle import app.revanced.manager.R import app.revanced.manager.domain.bundles.PatchBundleSource -import app.revanced.manager.domain.bundles.PatchBundleSource.Extensions.nameState import app.revanced.manager.ui.component.ConfirmDialog import app.revanced.manager.ui.component.haptics.HapticCheckbox -import kotlinx.coroutines.flow.map @OptIn(ExperimentalFoundationApi::class) @Composable fun BundleItem( - bundle: PatchBundleSource, - onDelete: () -> Unit, - onUpdate: () -> Unit, + src: PatchBundleSource, + patchCount: Int, selectable: Boolean, - onSelect: () -> Unit, isBundleSelected: Boolean, toggleSelection: (Boolean) -> Unit, + onSelect: () -> Unit, + onDelete: () -> Unit, + onUpdate: () -> Unit, ) { var viewBundleDialogPage by rememberSaveable { mutableStateOf(false) } var showDeleteConfirmationDialog by rememberSaveable { mutableStateOf(false) } - val state by bundle.state.collectAsStateWithLifecycle() - - val version by bundle.versionFlow.collectAsStateWithLifecycle(null) - val patchCount by bundle.patchCountFlow.collectAsStateWithLifecycle(0) - val name by bundle.nameState if (viewBundleDialogPage) { BundleInformationDialog( + src = src, + patchCount = patchCount, onDismissRequest = { viewBundleDialogPage = false }, onDeleteRequest = { showDeleteConfirmationDialog = true }, - bundle = bundle, onUpdate = onUpdate, ) } @@ -68,7 +62,7 @@ fun BundleItem( viewBundleDialogPage = false }, title = stringResource(R.string.delete), - description = stringResource(R.string.patches_delete_single_dialog_description, name), + description = stringResource(R.string.patches_delete_single_dialog_description, src.name), icon = Icons.Outlined.Delete ) } @@ -90,19 +84,19 @@ fun BundleItem( } } else null, - headlineContent = { Text(name) }, + headlineContent = { Text(src.name) }, supportingContent = { - if (state is PatchBundleSource.State.Loaded) { + if (src.state is PatchBundleSource.State.Available) { Text(pluralStringResource(R.plurals.patch_count, patchCount, patchCount)) } }, trailingContent = { Row { - val icon = remember(state) { - when (state) { + val icon = remember(src.state) { + when (src.state) { is PatchBundleSource.State.Failed -> Icons.Outlined.ErrorOutline to R.string.patches_error is PatchBundleSource.State.Missing -> Icons.Outlined.Warning to R.string.patches_missing - is PatchBundleSource.State.Loaded -> null + is PatchBundleSource.State.Available -> null } } @@ -115,7 +109,7 @@ fun BundleItem( ) } - version?.let { Text(text = it) } + src.version?.let { Text(text = it) } } }, ) diff --git a/app/src/main/java/app/revanced/manager/ui/component/bundle/BundlePatchesDialog.kt b/app/src/main/java/app/revanced/manager/ui/component/bundle/BundlePatchesDialog.kt index 7d28237d..1055578c 100644 --- a/app/src/main/java/app/revanced/manager/ui/component/bundle/BundlePatchesDialog.kt +++ b/app/src/main/java/app/revanced/manager/ui/component/bundle/BundlePatchesDialog.kt @@ -12,6 +12,7 @@ import androidx.compose.material3.* import androidx.compose.runtime.Composable import androidx.compose.runtime.getValue import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.remember import androidx.compose.runtime.saveable.rememberSaveable import androidx.compose.runtime.setValue import androidx.compose.ui.Alignment @@ -25,20 +26,26 @@ import androidx.compose.ui.unit.dp import androidx.lifecycle.compose.collectAsStateWithLifecycle import app.revanced.manager.R import app.revanced.manager.domain.bundles.PatchBundleSource +import app.revanced.manager.domain.repository.PatchBundleRepository import app.revanced.manager.patcher.patch.PatchInfo import app.revanced.manager.ui.component.ArrowButton import app.revanced.manager.ui.component.FullscreenDialog import app.revanced.manager.ui.component.LazyColumnWithScrollbar +import kotlinx.coroutines.flow.mapNotNull +import org.koin.compose.koinInject @OptIn(ExperimentalMaterial3Api::class) @Composable fun BundlePatchesDialog( onDismissRequest: () -> Unit, - bundle: PatchBundleSource, + src: PatchBundleSource, ) { var showAllVersions by rememberSaveable { mutableStateOf(false) } var showOptions by rememberSaveable { mutableStateOf(false) } - val state by bundle.state.collectAsStateWithLifecycle() + val patchBundleRepository: PatchBundleRepository = koinInject() + val patches by remember(src.uid) { + patchBundleRepository.bundleInfoFlow.mapNotNull { it[src.uid]?.patches } + }.collectAsStateWithLifecycle(emptyList()) FullscreenDialog( onDismissRequest = onDismissRequest, @@ -64,16 +71,14 @@ fun BundlePatchesDialog( verticalArrangement = Arrangement.spacedBy(12.dp), contentPadding = PaddingValues(16.dp) ) { - state.patchBundleOrNull()?.let { bundle -> - items(bundle.patches) { patch -> - PatchItem( - patch, - showAllVersions, - onExpandVersions = { showAllVersions = !showAllVersions }, - showOptions, - onExpandOptions = { showOptions = !showOptions } - ) - } + items(patches) { patch -> + PatchItem( + patch, + showAllVersions, + onExpandVersions = { showAllVersions = !showAllVersions }, + showOptions, + onExpandOptions = { showOptions = !showOptions } + ) } } } diff --git a/app/src/main/java/app/revanced/manager/ui/component/bundle/BundleSelector.kt b/app/src/main/java/app/revanced/manager/ui/component/bundle/BundleSelector.kt index bcad3fe9..7bcb1017 100644 --- a/app/src/main/java/app/revanced/manager/ui/component/bundle/BundleSelector.kt +++ b/app/src/main/java/app/revanced/manager/ui/component/bundle/BundleSelector.kt @@ -12,26 +12,23 @@ import androidx.compose.material3.ModalBottomSheet import androidx.compose.material3.Text import androidx.compose.runtime.Composable import androidx.compose.runtime.LaunchedEffect -import androidx.compose.runtime.getValue import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.res.stringResource import androidx.compose.ui.unit.dp -import androidx.lifecycle.compose.collectAsStateWithLifecycle import app.revanced.manager.R import app.revanced.manager.domain.bundles.PatchBundleSource -import app.revanced.manager.domain.bundles.PatchBundleSource.Extensions.nameState @OptIn(ExperimentalMaterial3Api::class) @Composable -fun BundleSelector(bundles: List, onFinish: (PatchBundleSource?) -> Unit) { - LaunchedEffect(bundles) { - if (bundles.size == 1) { - onFinish(bundles[0]) +fun BundleSelector(sources: List, onFinish: (PatchBundleSource?) -> Unit) { + LaunchedEffect(sources) { + if (sources.size == 1) { + onFinish(sources[0]) } } - if (bundles.size < 2) { + if (sources.size < 2) { return } @@ -55,10 +52,7 @@ fun BundleSelector(bundles: List, onFinish: (PatchBundleSourc color = MaterialTheme.colorScheme.onSurface ) } - bundles.forEach { - val name by it.nameState - val version by it.versionFlow.collectAsStateWithLifecycle(null) - + sources.forEach { Row( verticalAlignment = Alignment.CenterVertically, horizontalArrangement = Arrangement.Center, @@ -70,7 +64,7 @@ fun BundleSelector(bundles: List, onFinish: (PatchBundleSourc } ) { Text( - "$name $version", + "${it.name} ${it.version}", style = MaterialTheme.typography.titleMedium, color = MaterialTheme.colorScheme.onSurface ) diff --git a/app/src/main/java/app/revanced/manager/ui/component/bundle/ImportBundleDialog.kt b/app/src/main/java/app/revanced/manager/ui/component/bundle/ImportBundleDialog.kt index 1a05d866..943a9f9c 100644 --- a/app/src/main/java/app/revanced/manager/ui/component/bundle/ImportBundleDialog.kt +++ b/app/src/main/java/app/revanced/manager/ui/component/bundle/ImportBundleDialog.kt @@ -23,10 +23,14 @@ import app.revanced.manager.ui.component.AlertDialogExtended import app.revanced.manager.ui.component.TextHorizontalPadding import app.revanced.manager.ui.component.haptics.HapticCheckbox import app.revanced.manager.ui.component.haptics.HapticRadioButton -import app.revanced.manager.ui.model.BundleType import app.revanced.manager.util.BIN_MIMETYPE import app.revanced.manager.util.transparentListItemColors +private enum class BundleType { + Local, + Remote +} + @Composable fun ImportPatchBundleDialog( onDismiss: () -> Unit, @@ -37,7 +41,7 @@ fun ImportPatchBundleDialog( var bundleType by rememberSaveable { mutableStateOf(BundleType.Remote) } var patchBundle by rememberSaveable { mutableStateOf(null) } var remoteUrl by rememberSaveable { mutableStateOf("") } - var autoUpdate by rememberSaveable { mutableStateOf(false) } + var autoUpdate by rememberSaveable { mutableStateOf(true) } val patchActivityLauncher = rememberLauncherForActivityResult(ActivityResultContracts.GetContent()) { uri -> @@ -117,7 +121,7 @@ fun ImportPatchBundleDialog( } @Composable -fun SelectBundleTypeStep( +private fun SelectBundleTypeStep( bundleType: BundleType, onBundleTypeSelected: (BundleType) -> Unit ) { @@ -168,7 +172,7 @@ fun SelectBundleTypeStep( @OptIn(ExperimentalMaterial3Api::class) @Composable -fun ImportBundleStep( +private fun ImportBundleStep( bundleType: BundleType, patchBundle: Uri?, remoteUrl: String, diff --git a/app/src/main/java/app/revanced/manager/ui/model/BundleInfo.kt b/app/src/main/java/app/revanced/manager/ui/model/BundleInfo.kt deleted file mode 100644 index 0fb01a76..00000000 --- a/app/src/main/java/app/revanced/manager/ui/model/BundleInfo.kt +++ /dev/null @@ -1,111 +0,0 @@ -package app.revanced.manager.ui.model - -import app.revanced.manager.domain.repository.PatchBundleRepository -import app.revanced.manager.patcher.patch.PatchInfo -import app.revanced.manager.util.PatchSelection -import app.revanced.manager.util.flatMapLatestAndCombine -import kotlinx.coroutines.flow.map - -/** - * A data class that contains patch bundle metadata for use by UI code. - */ -data class BundleInfo( - val name: String, - val version: String?, - val uid: Int, - val compatible: List, - val incompatible: List, - val universal: List -) { - val all = sequence { - yieldAll(compatible) - yieldAll(incompatible) - yieldAll(universal) - } - - fun patchSequence(allowIncompatible: Boolean) = if (allowIncompatible) { - all - } else { - sequence { - yieldAll(compatible) - yieldAll(universal) - } - } - - companion object Extensions { - inline fun Iterable.toPatchSelection( - allowIncompatible: Boolean, - condition: (Int, PatchInfo) -> Boolean - ): PatchSelection = this.associate { bundle -> - val patches = - bundle.patchSequence(allowIncompatible) - .mapNotNullTo(mutableSetOf()) { patch -> - patch.name.takeIf { - condition( - bundle.uid, - patch - ) - } - } - - bundle.uid to patches - } - - fun PatchBundleRepository.bundleInfoFlow(packageName: String, version: String?) = - sources.flatMapLatestAndCombine( - combiner = { it.filterNotNull() } - ) { source -> - // Regenerate bundle information whenever this source updates. - source.state.map { state -> - val bundle = state.patchBundleOrNull() ?: return@map null - - val compatible = mutableListOf() - val incompatible = mutableListOf() - val universal = mutableListOf() - - bundle.patches.filter { it.compatibleWith(packageName) }.forEach { - val targetList = when { - it.compatiblePackages == null -> universal - it.supports( - packageName, - version - ) -> compatible - - else -> incompatible - } - - targetList.add(it) - } - - BundleInfo(source.getName(), bundle.patchBundleManifestAttributes?.version, source.uid, compatible, incompatible, universal) - } - } - - /** - * Algorithm for determining whether all required options have been set. - */ - inline fun Iterable.requiredOptionsSet( - crossinline isSelected: (BundleInfo, PatchInfo) -> Boolean, - crossinline optionsForPatch: (BundleInfo, PatchInfo) -> Map? - ) = all bundle@{ bundle -> - bundle - .all - .filter { isSelected(bundle, it) } - .all patch@{ - if (it.options.isNullOrEmpty()) return@patch true - val opts by lazy { optionsForPatch(bundle, it).orEmpty() } - - it.options.all option@{ option -> - if (!option.required || option.default != null) return@option true - - option.key in opts - } - } - } - } -} - -enum class BundleType { - Local, - Remote -} \ No newline at end of file diff --git a/app/src/main/java/app/revanced/manager/ui/screen/BundleListScreen.kt b/app/src/main/java/app/revanced/manager/ui/screen/BundleListScreen.kt index 8704a064..4f23271c 100644 --- a/app/src/main/java/app/revanced/manager/ui/screen/BundleListScreen.kt +++ b/app/src/main/java/app/revanced/manager/ui/screen/BundleListScreen.kt @@ -3,59 +3,74 @@ package app.revanced.manager.ui.screen import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.foundation.lazy.items +import androidx.compose.material3.ExperimentalMaterial3Api +import androidx.compose.material3.pulltorefresh.PullToRefreshBox import androidx.compose.runtime.Composable -import androidx.compose.runtime.remember -import androidx.compose.runtime.snapshots.SnapshotStateList +import androidx.compose.runtime.LaunchedEffect +import androidx.compose.runtime.getValue import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier -import app.revanced.manager.domain.bundles.PatchBundleSource +import androidx.lifecycle.compose.collectAsStateWithLifecycle import app.revanced.manager.ui.component.LazyColumnWithScrollbar import app.revanced.manager.ui.component.bundle.BundleItem +import app.revanced.manager.ui.viewmodel.BundleListViewModel +import app.revanced.manager.util.EventEffect +import kotlinx.coroutines.flow.Flow +import org.koin.androidx.compose.koinViewModel +@OptIn(ExperimentalMaterial3Api::class) @Composable fun BundleListScreen( - onDelete: (PatchBundleSource) -> Unit, - onUpdate: (PatchBundleSource) -> Unit, - sources: List, - selectedSources: SnapshotStateList, - bundlesSelectable: Boolean, + viewModel: BundleListViewModel = koinViewModel(), + eventsFlow: Flow, + setSelectedSourceCount: (Int) -> Unit ) { - val sortedSources = remember(sources) { - sources.sortedByDescending { source -> - source.state.value.patchBundleOrNull()?.patches?.size ?: 0 - } + val patchCounts by viewModel.patchCounts.collectAsStateWithLifecycle(emptyMap()) + val sources by viewModel.sources.collectAsStateWithLifecycle(emptyList()) + + EventEffect(eventsFlow) { + viewModel.handleEvent(it) + } + LaunchedEffect(viewModel.selectedSources.size) { + setSelectedSourceCount(viewModel.selectedSources.size) } - LazyColumnWithScrollbar( - modifier = Modifier.fillMaxSize(), - horizontalAlignment = Alignment.CenterHorizontally, - verticalArrangement = Arrangement.Top, + PullToRefreshBox( + onRefresh = viewModel::refresh, + isRefreshing = viewModel.isRefreshing ) { - items( - sortedSources, - key = { it.uid } - ) { source -> - BundleItem( - bundle = source, - onDelete = { - onDelete(source) - }, - onUpdate = { - onUpdate(source) - }, - selectable = bundlesSelectable, - onSelect = { - selectedSources.add(source) - }, - isBundleSelected = selectedSources.contains(source), - toggleSelection = { bundleIsNotSelected -> - if (bundleIsNotSelected) { - selectedSources.add(source) - } else { - selectedSources.remove(source) + LazyColumnWithScrollbar( + modifier = Modifier.fillMaxSize(), + horizontalAlignment = Alignment.CenterHorizontally, + verticalArrangement = Arrangement.Top, + ) { + items( + sources, + key = { it.uid } + ) { source -> + BundleItem( + src = source, + patchCount = patchCounts[source.uid] ?: 0, + onDelete = { + viewModel.delete(source) + }, + onUpdate = { + viewModel.update(source) + }, + selectable = viewModel.selectedSources.size > 0, + onSelect = { + viewModel.selectedSources.add(source.uid) + }, + isBundleSelected = source.uid in viewModel.selectedSources, + toggleSelection = { bundleIsNotSelected -> + if (bundleIsNotSelected) { + viewModel.selectedSources.add(source.uid) + } else { + viewModel.selectedSources.remove(source.uid) + } } - } - ) + ) + } } } } \ No newline at end of file diff --git a/app/src/main/java/app/revanced/manager/ui/screen/DashboardScreen.kt b/app/src/main/java/app/revanced/manager/ui/screen/DashboardScreen.kt index 03eff51f..0496c1a9 100644 --- a/app/src/main/java/app/revanced/manager/ui/screen/DashboardScreen.kt +++ b/app/src/main/java/app/revanced/manager/ui/screen/DashboardScreen.kt @@ -44,6 +44,7 @@ import androidx.compose.runtime.Composable import androidx.compose.runtime.LaunchedEffect import androidx.compose.runtime.derivedStateOf import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableIntStateOf import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.remember import androidx.compose.runtime.rememberCoroutineScope @@ -56,7 +57,6 @@ import androidx.compose.ui.res.stringResource import androidx.compose.ui.unit.dp import androidx.lifecycle.compose.collectAsStateWithLifecycle import app.revanced.manager.R -import app.revanced.manager.domain.bundles.PatchBundleSource.Extensions.isDefault import app.revanced.manager.patcher.aapt.Aapt import app.revanced.manager.ui.component.AlertDialogExtended import app.revanced.manager.ui.component.AppTopBar @@ -93,7 +93,8 @@ fun DashboardScreen( onDownloaderPluginClick: () -> Unit, onAppClick: (String) -> Unit ) { - val bundlesSelectable by remember { derivedStateOf { vm.selectedSources.isNotEmpty() } } + var selectedSourceCount by rememberSaveable { mutableIntStateOf(0) } + val bundlesSelectable by remember { derivedStateOf { selectedSourceCount > 0 } } val availablePatches by vm.availablePatches.collectAsStateWithLifecycle(0) val showNewDownloaderPluginsNotification by vm.newDownloaderPluginsAvailable.collectAsStateWithLifecycle( false @@ -160,10 +161,7 @@ fun DashboardScreen( if (showDeleteConfirmationDialog) { ConfirmDialog( onDismiss = { showDeleteConfirmationDialog = false }, - onConfirm = { - vm.selectedSources.forEach { if (!it.isDefault) vm.delete(it) } - vm.cancelSourceSelection() - }, + onConfirm = vm::deleteSources, title = stringResource(R.string.delete), description = stringResource(R.string.patches_delete_multiple_dialog_description), icon = Icons.Outlined.Delete @@ -174,7 +172,7 @@ fun DashboardScreen( topBar = { if (bundlesSelectable) { BundleTopBar( - title = stringResource(R.string.patches_selected, vm.selectedSources.size), + title = stringResource(R.string.patches_selected, selectedSourceCount), onBackClick = vm::cancelSourceSelection, backIcon = { Icon( @@ -194,10 +192,7 @@ fun DashboardScreen( ) } IconButton( - onClick = { - vm.selectedSources.forEach { vm.update(it) } - vm.cancelSourceSelection() - } + onClick = vm::updateSources ) { Icon( Icons.Outlined.Refresh, @@ -349,18 +344,9 @@ fun DashboardScreen( } } - val sources by vm.sources.collectAsStateWithLifecycle(initialValue = emptyList()) - BundleListScreen( - onDelete = { - vm.delete(it) - }, - onUpdate = { - vm.update(it) - }, - sources = sources, - selectedSources = vm.selectedSources, - bundlesSelectable = bundlesSelectable + eventsFlow = vm.bundleListEventsFlow, + setSelectedSourceCount = { selectedSourceCount = it } ) } } diff --git a/app/src/main/java/app/revanced/manager/ui/screen/PatchesSelectorScreen.kt b/app/src/main/java/app/revanced/manager/ui/screen/PatchesSelectorScreen.kt index 2bb29e4a..db0667a0 100644 --- a/app/src/main/java/app/revanced/manager/ui/screen/PatchesSelectorScreen.kt +++ b/app/src/main/java/app/revanced/manager/ui/screen/PatchesSelectorScreen.kt @@ -413,7 +413,7 @@ fun PatchesSelectorScreen( style = MaterialTheme.typography.bodyMedium ) Text( - text = bundle.version!!, + text = bundle.version.orEmpty(), style = MaterialTheme.typography.bodySmall, color = MaterialTheme.colorScheme.onSurfaceVariant ) diff --git a/app/src/main/java/app/revanced/manager/ui/screen/RequiredOptionsScreen.kt b/app/src/main/java/app/revanced/manager/ui/screen/RequiredOptionsScreen.kt index 1daa4c5f..4f054589 100644 --- a/app/src/main/java/app/revanced/manager/ui/screen/RequiredOptionsScreen.kt +++ b/app/src/main/java/app/revanced/manager/ui/screen/RequiredOptionsScreen.kt @@ -30,12 +30,12 @@ import androidx.compose.ui.unit.dp import androidx.lifecycle.compose.collectAsStateWithLifecycle import app.revanced.manager.R import app.revanced.manager.patcher.patch.Option +import app.revanced.manager.patcher.patch.PatchBundleInfo.Extensions.requiredOptionsSet import app.revanced.manager.ui.component.AppTopBar import app.revanced.manager.ui.component.LazyColumnWithScrollbar import app.revanced.manager.ui.component.haptics.HapticExtendedFloatingActionButton import app.revanced.manager.ui.component.haptics.HapticTab import app.revanced.manager.ui.component.patches.OptionItem -import app.revanced.manager.ui.model.BundleInfo.Extensions.requiredOptionsSet import app.revanced.manager.ui.viewmodel.PatchesSelectorViewModel import app.revanced.manager.util.Options import app.revanced.manager.util.PatchSelection @@ -62,6 +62,7 @@ fun RequiredOptionsScreen( val showContinueButton by remember { derivedStateOf { bundles.requiredOptionsSet( + allowIncompatible = vm.allowIncompatiblePatches, isSelected = { bundle, patch -> vm.isSelected(bundle.uid, patch) }, optionsForPatch = { bundle, patch -> vm.getOptions(bundle.uid, patch) } ) diff --git a/app/src/main/java/app/revanced/manager/ui/screen/settings/DownloadsSettingsScreen.kt b/app/src/main/java/app/revanced/manager/ui/screen/settings/DownloadsSettingsScreen.kt index 3d57ee50..bbe5a4cd 100644 --- a/app/src/main/java/app/revanced/manager/ui/screen/settings/DownloadsSettingsScreen.kt +++ b/app/src/main/java/app/revanced/manager/ui/screen/settings/DownloadsSettingsScreen.kt @@ -2,7 +2,6 @@ package app.revanced.manager.ui.screen.settings import androidx.annotation.StringRes import androidx.compose.foundation.clickable -import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.padding @@ -19,9 +18,7 @@ import androidx.compose.material3.Scaffold import androidx.compose.material3.Text import androidx.compose.material3.TextButton import androidx.compose.material3.TopAppBarDefaults -import androidx.compose.material3.pulltorefresh.PullToRefreshDefaults -import androidx.compose.material3.pulltorefresh.pullToRefresh -import androidx.compose.material3.pulltorefresh.rememberPullToRefreshState +import androidx.compose.material3.pulltorefresh.PullToRefreshBox import androidx.compose.material3.rememberTopAppBarState import androidx.compose.runtime.Composable import androidx.compose.runtime.getValue @@ -29,13 +26,11 @@ import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.remember import androidx.compose.runtime.saveable.rememberSaveable import androidx.compose.runtime.setValue -import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.input.nestedscroll.nestedScroll import androidx.compose.ui.res.stringResource import androidx.compose.ui.text.style.TextAlign import androidx.compose.ui.unit.dp -import androidx.compose.ui.zIndex import androidx.lifecycle.compose.collectAsStateWithLifecycle import app.revanced.manager.R import app.revanced.manager.network.downloader.DownloaderPluginState @@ -57,7 +52,6 @@ fun DownloadsSettingsScreen( onBackClick: () -> Unit, viewModel: DownloadsViewModel = koinViewModel() ) { - val pullRefreshState = rememberPullToRefreshState() val downloadedApps by viewModel.downloadedApps.collectAsStateWithLifecycle(emptyList()) val pluginStates by viewModel.downloaderPluginStates.collectAsStateWithLifecycle() val scrollBehavior = TopAppBarDefaults.pinnedScrollBehavior(rememberTopAppBarState()) @@ -90,152 +84,138 @@ fun DownloadsSettingsScreen( }, modifier = Modifier.nestedScroll(scrollBehavior.nestedScrollConnection), ) { paddingValues -> - Box( - contentAlignment = Alignment.TopCenter, - modifier = Modifier - .padding(paddingValues) - .fillMaxWidth() - .zIndex(1f) + PullToRefreshBox( + onRefresh = viewModel::refreshPlugins, + isRefreshing = viewModel.isRefreshingPlugins, + modifier = Modifier.padding(paddingValues) ) { - PullToRefreshDefaults.Indicator( - state = pullRefreshState, - isRefreshing = viewModel.isRefreshingPlugins - ) - } - - LazyColumnWithScrollbar( - modifier = Modifier - .fillMaxSize() - .padding(paddingValues) - .pullToRefresh( - isRefreshing = viewModel.isRefreshingPlugins, - state = pullRefreshState, - onRefresh = viewModel::refreshPlugins - ) - ) { - item { - GroupHeader(stringResource(R.string.downloader_plugins)) - } - pluginStates.forEach { (packageName, state) -> - item(key = packageName) { - var showDialog by rememberSaveable { - mutableStateOf(false) - } - - fun dismiss() { - showDialog = false - } - - val packageInfo = - remember(packageName) { - viewModel.pm.getPackageInfo( - packageName - ) - } ?: return@item - - if (showDialog) { - val signature = - remember(packageName) { - val androidSignature = - viewModel.pm.getSignature(packageName) - val hash = MessageDigest.getInstance("SHA-256") - .digest(androidSignature.toByteArray()) - hash.toHexString(format = HexFormat.UpperCase) - } - - when (state) { - is DownloaderPluginState.Loaded -> TrustDialog( - title = R.string.downloader_plugin_revoke_trust_dialog_title, - body = stringResource( - R.string.downloader_plugin_trust_dialog_body, - packageName, - signature - ), - onDismiss = ::dismiss, - onConfirm = { - viewModel.revokePluginTrust(packageName) - dismiss() - } - ) - - is DownloaderPluginState.Failed -> ExceptionViewerDialog( - text = remember(state.throwable) { - state.throwable.stackTraceToString() - }, - onDismiss = ::dismiss - ) - - is DownloaderPluginState.Untrusted -> TrustDialog( - title = R.string.downloader_plugin_trust_dialog_title, - body = stringResource( - R.string.downloader_plugin_trust_dialog_body, - packageName, - signature - ), - onDismiss = ::dismiss, - onConfirm = { - viewModel.trustPlugin(packageName) - dismiss() - } - ) + LazyColumnWithScrollbar( + modifier = Modifier.fillMaxSize() + ) { + item { + GroupHeader(stringResource(R.string.downloader_plugins)) + } + pluginStates.forEach { (packageName, state) -> + item(key = packageName) { + var showDialog by rememberSaveable { + mutableStateOf(false) } + + fun dismiss() { + showDialog = false + } + + val packageInfo = + remember(packageName) { + viewModel.pm.getPackageInfo( + packageName + ) + } ?: return@item + + if (showDialog) { + val signature = + remember(packageName) { + val androidSignature = + viewModel.pm.getSignature(packageName) + val hash = MessageDigest.getInstance("SHA-256") + .digest(androidSignature.toByteArray()) + hash.toHexString(format = HexFormat.UpperCase) + } + + when (state) { + is DownloaderPluginState.Loaded -> TrustDialog( + title = R.string.downloader_plugin_revoke_trust_dialog_title, + body = stringResource( + R.string.downloader_plugin_trust_dialog_body, + packageName, + signature + ), + onDismiss = ::dismiss, + onConfirm = { + viewModel.revokePluginTrust(packageName) + dismiss() + } + ) + + is DownloaderPluginState.Failed -> ExceptionViewerDialog( + text = remember(state.throwable) { + state.throwable.stackTraceToString() + }, + onDismiss = ::dismiss + ) + + is DownloaderPluginState.Untrusted -> TrustDialog( + title = R.string.downloader_plugin_trust_dialog_title, + body = stringResource( + R.string.downloader_plugin_trust_dialog_body, + packageName, + signature + ), + onDismiss = ::dismiss, + onConfirm = { + viewModel.trustPlugin(packageName) + dismiss() + } + ) + } + } + + SettingsListItem( + modifier = Modifier.clickable { showDialog = true }, + headlineContent = { + AppLabel( + packageInfo = packageInfo, + style = MaterialTheme.typography.titleLarge + ) + }, + supportingContent = stringResource( + when (state) { + is DownloaderPluginState.Loaded -> R.string.downloader_plugin_state_trusted + is DownloaderPluginState.Failed -> R.string.downloader_plugin_state_failed + is DownloaderPluginState.Untrusted -> R.string.downloader_plugin_state_untrusted + } + ), + trailingContent = { Text(packageInfo.versionName!!) } + ) } + } + if (pluginStates.isEmpty()) { + item { + Text( + stringResource(R.string.downloader_no_plugins_installed), + modifier = Modifier.fillMaxWidth(), + textAlign = TextAlign.Center + ) + } + } + + item { + GroupHeader(stringResource(R.string.downloaded_apps)) + } + items(downloadedApps, key = { it.packageName to it.version }) { app -> + val selected = app in viewModel.appSelection SettingsListItem( - modifier = Modifier.clickable { showDialog = true }, - headlineContent = { - AppLabel( - packageInfo = packageInfo, - style = MaterialTheme.typography.titleLarge + modifier = Modifier.clickable { viewModel.toggleApp(app) }, + headlineContent = app.packageName, + leadingContent = (@Composable { + HapticCheckbox( + checked = selected, + onCheckedChange = { viewModel.toggleApp(app) } ) - }, - supportingContent = stringResource( - when (state) { - is DownloaderPluginState.Loaded -> R.string.downloader_plugin_state_trusted - is DownloaderPluginState.Failed -> R.string.downloader_plugin_state_failed - is DownloaderPluginState.Untrusted -> R.string.downloader_plugin_state_untrusted - } - ), - trailingContent = { Text(packageInfo.versionName!!) } + }).takeIf { viewModel.appSelection.isNotEmpty() }, + supportingContent = app.version, + tonalElevation = if (selected) 8.dp else 0.dp ) } - } - if (pluginStates.isEmpty()) { - item { - Text( - stringResource(R.string.downloader_no_plugins_installed), - modifier = Modifier.fillMaxWidth(), - textAlign = TextAlign.Center - ) - } - } - - item { - GroupHeader(stringResource(R.string.downloaded_apps)) - } - items(downloadedApps, key = { it.packageName to it.version }) { app -> - val selected = app in viewModel.appSelection - - SettingsListItem( - modifier = Modifier.clickable { viewModel.toggleApp(app) }, - headlineContent = app.packageName, - leadingContent = (@Composable { - HapticCheckbox( - checked = selected, - onCheckedChange = { viewModel.toggleApp(app) } + if (downloadedApps.isEmpty()) { + item { + Text( + stringResource(R.string.downloader_settings_no_apps), + modifier = Modifier.fillMaxWidth(), + textAlign = TextAlign.Center ) - }).takeIf { viewModel.appSelection.isNotEmpty() }, - supportingContent = app.version, - tonalElevation = if (selected) 8.dp else 0.dp - ) - } - if (downloadedApps.isEmpty()) { - item { - Text( - stringResource(R.string.downloader_settings_no_apps), - modifier = Modifier.fillMaxWidth(), - textAlign = TextAlign.Center - ) + } } } } diff --git a/app/src/main/java/app/revanced/manager/ui/screen/settings/ImportExportSettingsScreen.kt b/app/src/main/java/app/revanced/manager/ui/screen/settings/ImportExportSettingsScreen.kt index 9383d44a..181e8dcd 100644 --- a/app/src/main/java/app/revanced/manager/ui/screen/settings/ImportExportSettingsScreen.kt +++ b/app/src/main/java/app/revanced/manager/ui/screen/settings/ImportExportSettingsScreen.kt @@ -227,12 +227,12 @@ fun ImportExportSettingsScreen( GroupItem( onClick = { selectorDialog = { - BundleSelector(bundles = patchBundles) { bundle -> - bundle?.also { + BundleSelector(sources = patchBundles) { src -> + src?.also { coroutineScope.launch { vm.resetDialogState = - ResetDialogState.PatchSelectionBundle(bundle.getName()) { - vm.resetSelectionForPatchBundle(bundle) + ResetDialogState.PatchSelectionBundle(it.name) { + vm.resetSelectionForPatchBundle(it) } } } @@ -283,12 +283,12 @@ fun ImportExportSettingsScreen( GroupItem( onClick = { selectorDialog = { - BundleSelector(bundles = patchBundles) { bundle -> - bundle?.also { + BundleSelector(sources = patchBundles) { src -> + src?.also { coroutineScope.launch { vm.resetDialogState = - ResetDialogState.PatchOptionBundle(bundle.getName()) { - vm.resetOptionsForBundle(bundle) + ResetDialogState.PatchOptionBundle(src.name) { + vm.resetOptionsForBundle(src) } } } diff --git a/app/src/main/java/app/revanced/manager/ui/viewmodel/BundleListViewModel.kt b/app/src/main/java/app/revanced/manager/ui/viewmodel/BundleListViewModel.kt new file mode 100644 index 00000000..49bc0c4b --- /dev/null +++ b/app/src/main/java/app/revanced/manager/ui/viewmodel/BundleListViewModel.kt @@ -0,0 +1,76 @@ +package app.revanced.manager.ui.viewmodel + +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.setValue +import androidx.lifecycle.ViewModel +import androidx.lifecycle.viewModelScope +import app.revanced.manager.domain.bundles.PatchBundleSource +import app.revanced.manager.domain.bundles.RemotePatchBundle +import app.revanced.manager.domain.repository.PatchBundleRepository +import app.revanced.manager.util.mutableStateSetOf +import kotlinx.coroutines.flow.combine +import kotlinx.coroutines.flow.first +import kotlinx.coroutines.launch +import org.koin.core.component.KoinComponent +import org.koin.core.component.get + +class BundleListViewModel : ViewModel(), KoinComponent { + private val patchBundleRepository: PatchBundleRepository = get() + val patchCounts = patchBundleRepository.patchCountsFlow + var isRefreshing by mutableStateOf(false) + private set + + val sources = combine( + patchBundleRepository.sources, + patchBundleRepository.patchCountsFlow + ) { sources, patchCounts -> + isRefreshing = false + sources.sortedByDescending { patchCounts[it.uid] ?: 0 } + } + + val selectedSources = mutableStateSetOf() + + fun refresh() = viewModelScope.launch { + isRefreshing = true + patchBundleRepository.reload() + } + + private suspend fun getSelectedSources() = patchBundleRepository.sources + .first() + .filter { it.uid in selectedSources } + .also { + selectedSources.clear() + } + + fun handleEvent(event: Event) { + when (event) { + Event.CANCEL -> selectedSources.clear() + Event.DELETE_SELECTED -> viewModelScope.launch { + patchBundleRepository.remove(*getSelectedSources().toTypedArray()) + } + + Event.UPDATE_SELECTED -> viewModelScope.launch { + patchBundleRepository.update( + *getSelectedSources().filterIsInstance().toTypedArray(), + showToast = true, + ) + } + } + } + + fun delete(src: PatchBundleSource) = + viewModelScope.launch { patchBundleRepository.remove(src) } + + fun update(src: PatchBundleSource) = viewModelScope.launch { + if (src !is RemotePatchBundle) return@launch + + patchBundleRepository.update(src, showToast = true) + } + + enum class Event { + DELETE_SELECTED, + UPDATE_SELECTED, + CANCEL, + } +} \ No newline at end of file diff --git a/app/src/main/java/app/revanced/manager/ui/viewmodel/DashboardViewModel.kt b/app/src/main/java/app/revanced/manager/ui/viewmodel/DashboardViewModel.kt index ae38bd29..26f86092 100644 --- a/app/src/main/java/app/revanced/manager/ui/viewmodel/DashboardViewModel.kt +++ b/app/src/main/java/app/revanced/manager/ui/viewmodel/DashboardViewModel.kt @@ -1,5 +1,6 @@ package app.revanced.manager.ui.viewmodel +import android.annotation.SuppressLint import android.app.Application import android.content.ContentResolver import android.net.Uri @@ -24,8 +25,10 @@ import app.revanced.manager.network.api.ReVancedAPI import app.revanced.manager.util.PM import app.revanced.manager.util.toast import app.revanced.manager.util.uiSafe +import kotlinx.coroutines.channels.Channel import kotlinx.coroutines.flow.first import kotlinx.coroutines.flow.map +import kotlinx.coroutines.flow.receiveAsFlow import kotlinx.coroutines.launch class DashboardViewModel( @@ -38,13 +41,12 @@ class DashboardViewModel( private val pm: PM, ) : ViewModel() { val availablePatches = - patchBundleRepository.bundles.map { it.values.sumOf { bundle -> bundle.patches.size } } + patchBundleRepository.bundleInfoFlow.map { it.values.sumOf { bundle -> bundle.patches.size } } private val contentResolver: ContentResolver = app.contentResolver private val powerManager = app.getSystemService()!! - val sources = patchBundleRepository.sources - val selectedSources = mutableStateListOf() - val newDownloaderPluginsAvailable = downloaderPluginRepository.newPluginPackageNames.map { it.isNotEmpty() } + val newDownloaderPluginsAvailable = + downloaderPluginRepository.newPluginPackageNames.map { it.isNotEmpty() } /** * Android 11 kills the app process after granting the "install apps" permission, which is a problem for the patcher screen. @@ -59,6 +61,9 @@ class DashboardViewModel( var showBatteryOptimizationsWarning by mutableStateOf(false) private set + private val bundleListEventsChannel = Channel() + val bundleListEventsFlow = bundleListEventsChannel.receiveAsFlow() + init { viewModelScope.launch { checkForManagerUpdates() @@ -70,10 +75,6 @@ class DashboardViewModel( downloaderPluginRepository.acknowledgeAllNewPlugins() } - fun dismissUpdateDialog() { - updatedManagerVersion = null - } - private suspend fun checkForManagerUpdates() { if (!prefs.managerAutoUpdates.get() || !networkInfo.isConnected()) return @@ -83,7 +84,8 @@ class DashboardViewModel( } fun updateBatteryOptimizationsWarning() { - showBatteryOptimizationsWarning = !powerManager.isIgnoringBatteryOptimizations(app.packageName) + showBatteryOptimizationsWarning = + !powerManager.isIgnoringBatteryOptimizations(app.packageName) } fun setShowManagerUpdateDialogOnLaunch(value: Boolean) { @@ -112,36 +114,20 @@ class DashboardViewModel( } } - - fun cancelSourceSelection() { - selectedSources.clear() + private fun sendEvent(event: BundleListViewModel.Event) { + viewModelScope.launch { bundleListEventsChannel.send(event) } } - fun createLocalSource(patchBundle: Uri) = - viewModelScope.launch { - contentResolver.openInputStream(patchBundle)!!.use { patchesStream -> - patchBundleRepository.createLocal(patchesStream) - } - } + fun cancelSourceSelection() = sendEvent(BundleListViewModel.Event.CANCEL) + fun updateSources() = sendEvent(BundleListViewModel.Event.UPDATE_SELECTED) + fun deleteSources() = sendEvent(BundleListViewModel.Event.DELETE_SELECTED) - fun createRemoteSource(apiUrl: String, autoUpdate: Boolean) = - viewModelScope.launch { patchBundleRepository.createRemote(apiUrl, autoUpdate) } + @SuppressLint("Recycle") + fun createLocalSource(patchBundle: Uri) = viewModelScope.launch { + patchBundleRepository.createLocal { contentResolver.openInputStream(patchBundle)!! } + } - fun delete(bundle: PatchBundleSource) = - viewModelScope.launch { patchBundleRepository.remove(bundle) } - - fun update(bundle: PatchBundleSource) = viewModelScope.launch { - if (bundle !is RemotePatchBundle) return@launch - - uiSafe( - app, - R.string.patches_download_fail, - RemotePatchBundle.updateFailMsg - ) { - if (bundle.update()) - app.toast(app.getString(R.string.patches_update_success, bundle.getName())) - else - app.toast(app.getString(R.string.patches_update_unavailable, bundle.getName())) - } + fun createRemoteSource(apiUrl: String, autoUpdate: Boolean) = viewModelScope.launch { + patchBundleRepository.createRemote(apiUrl, autoUpdate) } } \ No newline at end of file diff --git a/app/src/main/java/app/revanced/manager/ui/viewmodel/PatchesSelectorViewModel.kt b/app/src/main/java/app/revanced/manager/ui/viewmodel/PatchesSelectorViewModel.kt index dc43c28b..fb458d43 100644 --- a/app/src/main/java/app/revanced/manager/ui/viewmodel/PatchesSelectorViewModel.kt +++ b/app/src/main/java/app/revanced/manager/ui/viewmodel/PatchesSelectorViewModel.kt @@ -17,10 +17,9 @@ import androidx.lifecycle.viewmodel.compose.saveable import app.revanced.manager.R import app.revanced.manager.domain.manager.PreferencesManager import app.revanced.manager.domain.repository.PatchBundleRepository +import app.revanced.manager.patcher.patch.PatchBundleInfo +import app.revanced.manager.patcher.patch.PatchBundleInfo.Extensions.toPatchSelection import app.revanced.manager.patcher.patch.PatchInfo -import app.revanced.manager.ui.model.BundleInfo -import app.revanced.manager.ui.model.BundleInfo.Extensions.bundleInfoFlow -import app.revanced.manager.ui.model.BundleInfo.Extensions.toPatchSelection import app.revanced.manager.ui.model.navigation.SelectedApplicationInfo import app.revanced.manager.util.Options import app.revanced.manager.util.PatchSelection @@ -63,7 +62,7 @@ class PatchesSelectorViewModel(input: SelectedApplicationInfo.PatchesSelector.Vi val allowIncompatiblePatches = get().disablePatchVersionCompatCheck.getBlocking() val bundlesFlow = - get().bundleInfoFlow(packageName, input.app.version) + get().scopedBundleInfoFlow(packageName, input.app.version) init { viewModelScope.launch { @@ -76,11 +75,11 @@ class PatchesSelectorViewModel(input: SelectedApplicationInfo.PatchesSelector.Vi return@launch } - fun BundleInfo.hasDefaultPatches() = + fun PatchBundleInfo.Scoped.hasDefaultPatches() = patchSequence(allowIncompatiblePatches).any { it.include } // Don't show the warning if there are no default patches. - selectionWarningEnabled = bundlesFlow.first().any(BundleInfo::hasDefaultPatches) + selectionWarningEnabled = bundlesFlow.first().any(PatchBundleInfo.Scoped::hasDefaultPatches) } } @@ -123,7 +122,7 @@ class PatchesSelectorViewModel(input: SelectedApplicationInfo.PatchesSelector.Vi // This is for the required options screen. private val requiredOptsPatchesDeferred = viewModelScope.async(start = CoroutineStart.LAZY) { bundlesFlow.first().map { bundle -> - bundle to bundle.all.filter { patch -> + bundle to bundle.patchSequence(allowIncompatiblePatches).filter { patch -> val opts by lazy { getOptions(bundle.uid, patch).orEmpty() } @@ -136,14 +135,14 @@ class PatchesSelectorViewModel(input: SelectedApplicationInfo.PatchesSelector.Vi } val requiredOptsPatches = flow { emit(requiredOptsPatchesDeferred.await()) } - fun selectionIsValid(bundles: List) = bundles.any { bundle -> + fun selectionIsValid(bundles: List) = bundles.any { bundle -> bundle.patchSequence(allowIncompatiblePatches).any { patch -> isSelected(bundle.uid, patch) } } fun isSelected(bundle: Int, patch: PatchInfo) = customPatchSelection?.let { selection -> - selection[bundle]?.contains(patch.name) ?: false + selection[bundle]?.contains(patch.name) == true } ?: patch.include fun togglePatch(bundle: Int, patch: PatchInfo) = viewModelScope.launch { diff --git a/app/src/main/java/app/revanced/manager/ui/viewmodel/SelectedAppInfoViewModel.kt b/app/src/main/java/app/revanced/manager/ui/viewmodel/SelectedAppInfoViewModel.kt index 1b9b08c2..8b9003a9 100644 --- a/app/src/main/java/app/revanced/manager/ui/viewmodel/SelectedAppInfoViewModel.kt +++ b/app/src/main/java/app/revanced/manager/ui/viewmodel/SelectedAppInfoViewModel.kt @@ -28,15 +28,14 @@ import app.revanced.manager.domain.repository.InstalledAppRepository import app.revanced.manager.domain.repository.PatchBundleRepository import app.revanced.manager.domain.repository.PatchOptionsRepository import app.revanced.manager.domain.repository.PatchSelectionRepository +import app.revanced.manager.patcher.patch.PatchBundleInfo +import app.revanced.manager.patcher.patch.PatchBundleInfo.Extensions.toPatchSelection import app.revanced.manager.network.downloader.LoadedDownloaderPlugin import app.revanced.manager.network.downloader.ParceledDownloaderData +import app.revanced.manager.patcher.patch.PatchBundleInfo.Extensions.requiredOptionsSet import app.revanced.manager.plugin.downloader.GetScope import app.revanced.manager.plugin.downloader.PluginHostApi import app.revanced.manager.plugin.downloader.UserInteractionException -import app.revanced.manager.ui.model.BundleInfo -import app.revanced.manager.ui.model.BundleInfo.Extensions.bundleInfoFlow -import app.revanced.manager.ui.model.BundleInfo.Extensions.requiredOptionsSet -import app.revanced.manager.ui.model.BundleInfo.Extensions.toPatchSelection import app.revanced.manager.ui.model.SelectedApp import app.revanced.manager.ui.model.navigation.Patcher import app.revanced.manager.ui.model.navigation.SelectedApplicationInfo @@ -125,16 +124,19 @@ class SelectedAppInfoViewModel( suggestedVersions[input.app.packageName] } + val bundleInfoFlow by derivedStateOf { + bundleRepository.scopedBundleInfoFlow(packageName, selectedApp.version) + } + var options: Options by savedStateHandle.saveable { val state = mutableStateOf(emptyMap()) viewModelScope.launch { if (!persistConfiguration) return@launch // TODO: save options for patched apps. + val bundlePatches = bundleInfoFlow.first() + .associate { it.uid to it.patches.associateBy { patch -> patch.name } } options = withContext(Dispatchers.Default) { - val bundlePatches = bundleRepository.bundles.first() - .mapValues { (_, bundle) -> bundle.patches.associateBy { it.name } } - optionsRepository.getOptions(packageName, bundlePatches) } } @@ -176,10 +178,6 @@ class SelectedAppInfoViewModel( } } - val bundleInfoFlow by derivedStateOf { - bundleRepository.bundleInfoFlow(packageName, selectedApp.version) - } - fun showSourceSelector() { dismissSourceSelector() showSourceSelector = true @@ -266,9 +264,11 @@ class SelectedAppInfoViewModel( selectedAppInfo = info } + fun getOptionsFiltered(bundles: List) = options.filtered(bundles) suspend fun hasSetRequiredOptions(patchSelection: PatchSelection) = bundleInfoFlow .first() .requiredOptionsSet( + allowIncompatible = prefs.disablePatchVersionCompatCheck.get(), isSelected = { bundle, patch -> patch.name in patchSelection[bundle.uid]!! }, optionsForPatch = { bundle, patch -> options[bundle.uid]?.get(patch.name) }, ) @@ -283,23 +283,23 @@ class SelectedAppInfoViewModel( ) } - fun getOptionsFiltered(bundles: List) = options.filtered(bundles) - - fun getPatches(bundles: List, allowIncompatible: Boolean) = + fun getPatches(bundles: List, allowIncompatible: Boolean) = selectionState.patches(bundles, allowIncompatible) fun getCustomPatches( - bundles: List, + bundles: List, allowIncompatible: Boolean ): PatchSelection? = (selectionState as? SelectionState.Customized)?.patches(bundles, allowIncompatible) - fun updateConfiguration(selection: PatchSelection?, options: Options) = viewModelScope.launch { - val bundles = bundleInfoFlow.first() + fun updateConfiguration( + selection: PatchSelection?, + options: Options + ) = viewModelScope.launch { selectionState = selection?.let(SelectionState::Customized) ?: SelectionState.Default - val filteredOptions = options.filtered(bundles) + val filteredOptions = options.filtered(bundleInfoFlow.first()) this@SelectedAppInfoViewModel.options = filteredOptions if (!persistConfiguration) return@launch @@ -319,34 +319,35 @@ class SelectedAppInfoViewModel( /** * Returns a copy with all nonexistent options removed. */ - private fun Options.filtered(bundles: List): Options = buildMap options@{ - bundles.forEach bundles@{ bundle -> - val bundleOptions = this@filtered[bundle.uid] ?: return@bundles + private fun Options.filtered(bundles: List): Options = + buildMap options@{ + bundles.forEach bundles@{ bundle -> + val bundleOptions = this@filtered[bundle.uid] ?: return@bundles - val patches = bundle.all.associateBy { it.name } + val patches = bundle.patches.associateBy { it.name } - this@options[bundle.uid] = buildMap bundleOptions@{ - bundleOptions.forEach patch@{ (patchName, values) -> - // Get all valid option keys for the patch. - val validOptionKeys = - patches[patchName]?.options?.map { it.key }?.toSet() ?: return@patch + this@options[bundle.uid] = buildMap bundleOptions@{ + bundleOptions.forEach patch@{ (patchName, values) -> + // Get all valid option keys for the patch. + val validOptionKeys = + patches[patchName]?.options?.map { it.key }?.toSet() ?: return@patch - this@bundleOptions[patchName] = values.filterKeys { key -> - key in validOptionKeys + this@bundleOptions[patchName] = values.filterKeys { key -> + key in validOptionKeys + } } } } } - } } } private sealed interface SelectionState : Parcelable { - fun patches(bundles: List, allowIncompatible: Boolean): PatchSelection + fun patches(bundles: List, allowIncompatible: Boolean): PatchSelection @Parcelize data class Customized(val patchSelection: PatchSelection) : SelectionState { - override fun patches(bundles: List, allowIncompatible: Boolean) = + override fun patches(bundles: List, allowIncompatible: Boolean) = bundles.toPatchSelection( allowIncompatible ) { uid, patch -> @@ -356,7 +357,7 @@ private sealed interface SelectionState : Parcelable { @Parcelize data object Default : SelectionState { - override fun patches(bundles: List, allowIncompatible: Boolean) = + override fun patches(bundles: List, allowIncompatible: Boolean) = bundles.toPatchSelection(allowIncompatible) { _, patch -> patch.include } } } diff --git a/app/src/main/java/app/revanced/manager/util/PM.kt b/app/src/main/java/app/revanced/manager/util/PM.kt index 3ac633af..c2f16402 100644 --- a/app/src/main/java/app/revanced/manager/util/PM.kt +++ b/app/src/main/java/app/revanced/manager/util/PM.kt @@ -44,10 +44,10 @@ class PM( ) { private val scope = CoroutineScope(Dispatchers.IO) - val appList = patchBundleRepository.bundles.map { bundles -> + val appList = patchBundleRepository.bundleInfoFlow.map { bundles -> val compatibleApps = scope.async { - val compatiblePackages = bundles.values - .flatMap { it.patches } + val compatiblePackages = bundles + .flatMap { (_, bundle) -> bundle.patches } .flatMap { it.compatiblePackages.orEmpty() } .groupingBy { it.packageName } .eachCount() diff --git a/app/src/main/java/app/revanced/manager/util/Util.kt b/app/src/main/java/app/revanced/manager/util/Util.kt index 2c509aa5..5b2dfa33 100644 --- a/app/src/main/java/app/revanced/manager/util/Util.kt +++ b/app/src/main/java/app/revanced/manager/util/Util.kt @@ -116,10 +116,10 @@ inline fun LifecycleOwner.launchAndRepeatWithViewLifecycle( */ @OptIn(ExperimentalCoroutinesApi::class) inline fun Flow>.flatMapLatestAndCombine( - crossinline combiner: (Array) -> C, - crossinline transformer: (T) -> Flow, + crossinline combiner: suspend (Array) -> C, + crossinline transformer: suspend (T) -> Flow, ): Flow = flatMapLatest { iterable -> - combine(iterable.map(transformer)) { + combine(iterable.map { transformer(it) }) { combiner(it) } } diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index 60bd51c4..79f9daee 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -225,7 +225,7 @@ Download app Download APK file Failed to download patches: %s - Failed to load updated patches: %s + Failed to import patches: %s No patched apps found Tap on the patches to get more information about them %s selected @@ -344,8 +344,8 @@ Help us improve this application Developer options Options for debugging issues - Successfully updated %s - No update available for %s + Update successful + No update available View patches Any version Any package From 486ed5967f20941aa7e89fe93f299973be392e74 Mon Sep 17 00:00:00 2001 From: brosssh <44944126+brosssh@users.noreply.github.com> Date: Tue, 15 Jul 2025 15:32:49 +0200 Subject: [PATCH 17/19] fix: Show selection warning also on patch option (#2643) --- .../ui/component/patches/OptionFields.kt | 52 +++++++++++++++---- .../patches/SelectionWarningDialog.kt | 17 ++++++ .../ui/screen/PatchesSelectorScreen.kt | 23 +++----- .../ui/screen/RequiredOptionsScreen.kt | 3 +- app/src/main/res/values/strings.xml | 4 +- 5 files changed, 69 insertions(+), 30 deletions(-) create mode 100644 app/src/main/java/app/revanced/manager/ui/component/patches/SelectionWarningDialog.kt diff --git a/app/src/main/java/app/revanced/manager/ui/component/patches/OptionFields.kt b/app/src/main/java/app/revanced/manager/ui/component/patches/OptionFields.kt index eaacc38c..2511dda2 100644 --- a/app/src/main/java/app/revanced/manager/ui/component/patches/OptionFields.kt +++ b/app/src/main/java/app/revanced/manager/ui/component/patches/OptionFields.kt @@ -14,7 +14,6 @@ import androidx.compose.foundation.layout.fillMaxHeight import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.padding import androidx.compose.foundation.lazy.LazyColumn -import androidx.compose.foundation.lazy.LazyListItemInfo import androidx.compose.foundation.lazy.items import androidx.compose.foundation.lazy.itemsIndexed import androidx.compose.foundation.lazy.rememberLazyListState @@ -74,13 +73,11 @@ import app.revanced.manager.util.saver.snapshotStateListSaver import app.revanced.manager.util.saver.snapshotStateSetSaver import app.revanced.manager.util.toast import app.revanced.manager.util.transparentListItemColors -import kotlinx.coroutines.CoroutineScope import kotlinx.parcelize.Parcelize import org.koin.compose.koinInject import org.koin.core.component.KoinComponent import org.koin.core.component.get import sh.calvin.reorderable.ReorderableItem -import sh.calvin.reorderable.rememberReorderableLazyColumnState import sh.calvin.reorderable.rememberReorderableLazyListState import java.io.Serializable import kotlin.random.Random @@ -91,15 +88,28 @@ private class OptionEditorScope( val option: Option, val openDialog: () -> Unit, val dismissDialog: () -> Unit, + val selectionWarningEnabled: Boolean, + val showSelectionWarning: () -> Unit, val value: T?, - val setValue: (T?) -> Unit, + val setValue: (T?) -> Unit ) { fun submitDialog(value: T?) { setValue(value) dismissDialog() } - fun clickAction() = editor.clickAction(this) + fun checkSafeguard(block: () -> Unit) { + if (!option.required && selectionWarningEnabled) + showSelectionWarning() + else + block() + } + + fun clickAction() { + checkSafeguard { + editor.clickAction(this) + } + } @Composable fun ListItemTrailingContent() = editor.ListItemTrailingContent(this) @@ -113,7 +123,7 @@ private interface OptionEditor { @Composable fun ListItemTrailingContent(scope: OptionEditorScope) { - IconButton(onClick = { clickAction(scope) }) { + IconButton(onClick = { scope.checkSafeguard { clickAction(scope) } }) { Icon(Icons.Outlined.Edit, stringResource(R.string.edit)) } } @@ -141,11 +151,14 @@ private inline fun WithOptionEditor( option: Option, value: T?, noinline setValue: (T?) -> Unit, + selectionWarningEnabled: Boolean, crossinline onDismissDialog: @DisallowComposableCalls () -> Unit = {}, block: OptionEditorScope.() -> Unit ) { var showDialog by rememberSaveable { mutableStateOf(false) } - val scope = remember(editor, option, value, setValue) { + var showSelectionWarningDialog by rememberSaveable { mutableStateOf(false) } + + val scope = remember(editor, option, value, setValue, selectionWarningEnabled) { OptionEditorScope( editor, option, @@ -154,11 +167,18 @@ private inline fun WithOptionEditor( showDialog = false onDismissDialog() }, + selectionWarningEnabled, + showSelectionWarning = { showSelectionWarningDialog = true }, value, setValue ) } + if (showSelectionWarningDialog) + SelectionWarningDialog( + onDismiss = { showSelectionWarningDialog = false } + ) + if (showDialog) scope.Dialog() scope.block() @@ -169,6 +189,7 @@ fun OptionItem( option: Option, value: T?, setValue: (T?) -> Unit, + selectionWarningEnabled: Boolean ) { val editor = remember(option.type, option.presets) { @Suppress("UNCHECKED_CAST") @@ -181,7 +202,7 @@ fun OptionItem( else baseOptionEditor } - WithOptionEditor(editor, option, value, setValue) { + WithOptionEditor(editor, option, value, setValue, selectionWarningEnabled) { ListItem( modifier = Modifier.clickable(onClick = ::clickAction), headlineContent = { Text(option.title) }, @@ -300,7 +321,7 @@ private object StringOptionEditor : OptionEditor { private abstract class NumberOptionEditor : OptionEditor { @Composable - protected abstract fun NumberDialog( + abstract fun NumberDialog( title: String, current: T?, validator: (T?) -> Boolean, @@ -354,7 +375,14 @@ private object BooleanOptionEditor : OptionEditor { @Composable override fun ListItemTrailingContent(scope: OptionEditorScope) { - HapticSwitch(checked = scope.current, onCheckedChange = scope.setValue) + HapticSwitch( + checked = scope.current, + onCheckedChange = { value -> + scope.checkSafeguard { + scope.setValue(value) + } + } + ) } @Composable @@ -393,6 +421,7 @@ private class PresetOptionEditor(private val innerEditor: OptionEditor< scope.option, scope.value, scope.setValue, + scope.selectionWarningEnabled, onDismissDialog = scope.dismissDialog ) inner@{ var hidePresetsDialog by rememberSaveable { @@ -614,7 +643,8 @@ private class ListOptionEditor(private val elementEditor: Opti elementEditor, elementOption, value = item.value, - setValue = { items[index] = item.copy(value = it) } + setValue = { items[index] = item.copy(value = it) }, + selectionWarningEnabled = scope.selectionWarningEnabled ) { ListItem( modifier = Modifier.combinedClickable( diff --git a/app/src/main/java/app/revanced/manager/ui/component/patches/SelectionWarningDialog.kt b/app/src/main/java/app/revanced/manager/ui/component/patches/SelectionWarningDialog.kt new file mode 100644 index 00000000..4ec4d415 --- /dev/null +++ b/app/src/main/java/app/revanced/manager/ui/component/patches/SelectionWarningDialog.kt @@ -0,0 +1,17 @@ +package app.revanced.manager.ui.component.patches + +import androidx.compose.runtime.Composable +import androidx.compose.ui.res.stringResource +import app.revanced.manager.R +import app.revanced.manager.ui.component.SafeguardDialog + +@Composable +fun SelectionWarningDialog( + onDismiss: () -> Unit +) { + SafeguardDialog( + onDismiss = onDismiss, + title = R.string.warning, + body = stringResource(R.string.selection_warning_description), + ) +} \ No newline at end of file diff --git a/app/src/main/java/app/revanced/manager/ui/screen/PatchesSelectorScreen.kt b/app/src/main/java/app/revanced/manager/ui/screen/PatchesSelectorScreen.kt index db0667a0..035c862e 100644 --- a/app/src/main/java/app/revanced/manager/ui/screen/PatchesSelectorScreen.kt +++ b/app/src/main/java/app/revanced/manager/ui/screen/PatchesSelectorScreen.kt @@ -73,6 +73,7 @@ import app.revanced.manager.ui.component.haptics.HapticCheckbox import app.revanced.manager.ui.component.haptics.HapticExtendedFloatingActionButton import app.revanced.manager.ui.component.haptics.HapticTab import app.revanced.manager.ui.component.patches.OptionItem +import app.revanced.manager.ui.component.patches.SelectionWarningDialog import app.revanced.manager.ui.viewmodel.PatchesSelectorViewModel import app.revanced.manager.ui.viewmodel.PatchesSelectorViewModel.Companion.SHOW_INCOMPATIBLE import app.revanced.manager.ui.viewmodel.PatchesSelectorViewModel.Companion.SHOW_UNIVERSAL @@ -181,7 +182,8 @@ fun PatchesSelectorScreen( patch = patch, values = viewModel.getOptions(bundle, patch), reset = { viewModel.resetOptions(bundle, patch) }, - set = { key, value -> viewModel.setOption(bundle, patch, key, value) } + set = { key, value -> viewModel.setOption(bundle, patch, key, value) }, + selectionWarningEnabled = viewModel.selectionWarningEnabled ) } @@ -215,9 +217,7 @@ fun PatchesSelectorScreen( ) { patch -> PatchItem( patch = patch, - onOptionsDialog = { - viewModel.optionsDialog = uid to patch - }, + onOptionsDialog = { viewModel.optionsDialog = uid to patch }, selected = compatible && viewModel.isSelected( uid, patch @@ -472,17 +472,6 @@ fun PatchesSelectorScreen( } } -@Composable -private fun SelectionWarningDialog( - onDismiss: () -> Unit -) { - SafeguardDialog( - onDismiss = onDismiss, - title = R.string.warning, - body = stringResource(R.string.selection_warning_description), - ) -} - @Composable private fun UniversalPatchWarningDialog( onDismiss: () -> Unit @@ -612,6 +601,7 @@ private fun OptionsDialog( reset: () -> Unit, set: (String, Any?) -> Unit, onDismissRequest: () -> Unit, + selectionWarningEnabled: Boolean ) = FullscreenDialog(onDismissRequest = onDismissRequest) { Scaffold( topBar = { @@ -642,7 +632,8 @@ private fun OptionsDialog( value = value, setValue = { set(key, it) - } + }, + selectionWarningEnabled = selectionWarningEnabled ) } } diff --git a/app/src/main/java/app/revanced/manager/ui/screen/RequiredOptionsScreen.kt b/app/src/main/java/app/revanced/manager/ui/screen/RequiredOptionsScreen.kt index 4f054589..459e61ea 100644 --- a/app/src/main/java/app/revanced/manager/ui/screen/RequiredOptionsScreen.kt +++ b/app/src/main/java/app/revanced/manager/ui/screen/RequiredOptionsScreen.kt @@ -154,7 +154,8 @@ fun RequiredOptionsScreen( value = value, setValue = { new -> vm.setOption(bundle.uid, it, key, new) - } + }, + selectionWarningEnabled = vm.selectionWarningEnabled ) } } diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index 79f9daee..1117b5e9 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -95,8 +95,8 @@ Require suggested app version Enforce selection of the suggested app version Selecting an app that is not the suggested version may cause unexpected issues.\n\nDo you want to proceed anyways? - Allow changing patch selection - Do not prevent selecting or deselecting patches + Allow changing patch selection and options + Do not prevent selecting or deselecting patches and customization of options Changing the selection of patches may cause unexpected issues.\n\nEnable anyways? Allow using universal patches Do not prevent using universal patches From 47e4ed833686fb13b7bde9636e11c74065a8af71 Mon Sep 17 00:00:00 2001 From: oSumAtrIX Date: Tue, 15 Jul 2025 17:36:34 +0200 Subject: [PATCH 18/19] feat: Rename "Patch bundle" to "Patches" (#2541) Co-authored-by: Ax333l --- .../screen/settings/ImportExportSettingsScreen.kt | 4 ++-- .../manager/ui/viewmodel/ImportExportViewModel.kt | 4 ++-- app/src/main/res/values/strings.xml | 15 ++++++++------- 3 files changed, 12 insertions(+), 11 deletions(-) diff --git a/app/src/main/java/app/revanced/manager/ui/screen/settings/ImportExportSettingsScreen.kt b/app/src/main/java/app/revanced/manager/ui/screen/settings/ImportExportSettingsScreen.kt index 181e8dcd..165c3edd 100644 --- a/app/src/main/java/app/revanced/manager/ui/screen/settings/ImportExportSettingsScreen.kt +++ b/app/src/main/java/app/revanced/manager/ui/screen/settings/ImportExportSettingsScreen.kt @@ -296,8 +296,8 @@ fun ImportExportSettingsScreen( } } }, - headline = R.string.patch_options_reset, - description = R.string.patch_options_reset_all, + headline = R.string.patch_options_reset_patches, + description = R.string.patch_options_reset_patches_description, ) } } diff --git a/app/src/main/java/app/revanced/manager/ui/viewmodel/ImportExportViewModel.kt b/app/src/main/java/app/revanced/manager/ui/viewmodel/ImportExportViewModel.kt index 8ac7119c..6e933696 100644 --- a/app/src/main/java/app/revanced/manager/ui/viewmodel/ImportExportViewModel.kt +++ b/app/src/main/java/app/revanced/manager/ui/viewmodel/ImportExportViewModel.kt @@ -81,8 +81,8 @@ sealed class ResetDialogState( ) class PatchOptionBundle(dialogOptionName: String, onConfirm: () -> Unit) : ResetDialogState( - titleResId = R.string.patch_options_reset, - descriptionResId = R.string.patch_options_reset_dialog_description, + titleResId = R.string.patch_options_reset_patches, + descriptionResId = R.string.patch_options_reset_patches_dialog_description, onConfirm = onConfirm, dialogOptionName = dialogOptionName ) diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index 1117b5e9..c1a3d648 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -131,21 +131,22 @@ Reset patch options Reset the stored patch options Patch selection has been reset - Reset all patch selection + Reset patch selection globally You are about to reset all the patch selections. You will need to manually select each patch again. - Reset all the patch selections + Resets all the patch selections Reset patch selection for app You are about to reset the patch selection for the app \"%s\". You will have to manually select each patch again. Resets patch selection for a single app - Resets patch selection for a specific patches + Reset patch selection (single) You are about to reset the patch selection for \"%s\". You will have to manually select each patch again. - Resets the patch selection for a specific patches + Resets the patch selection for a specific collection of patches Reset patch options for app You are about to reset the patch options for the app \"%s\". You will have to reapply each option again. Resets patch options for a single app - Reset patch options - You are about to reset the patch options for \"%s\". You will have to reapply each option again. - Reset patch options for all + Reset patch options (single) + You are about to reset the patch options for \"%s\". You will have to reapply each option again. + Resets the patch options for a specific collection of patches + Reset patch options globally You are about to reset patch options. You will have to reapply each option again. Resets all patch options Plugins From 244674a603ea008d51ac26d60a7eef17c546935a Mon Sep 17 00:00:00 2001 From: Ax333l Date: Tue, 15 Jul 2025 17:56:24 +0200 Subject: [PATCH 19/19] chore: remove unused dependency --- app/build.gradle.kts | 4 ---- .../java/app/revanced/manager/network/service/HttpService.kt | 1 - gradle/libs.versions.toml | 5 ----- 3 files changed, 10 deletions(-) diff --git a/app/build.gradle.kts b/app/build.gradle.kts index 2da5205f..2de52228 100644 --- a/app/build.gradle.kts +++ b/app/build.gradle.kts @@ -40,10 +40,6 @@ dependencies { // Placeholder implementation(libs.placeholder.material3) - // HTML Scraper - implementation(libs.skrapeit.dsl) - implementation(libs.skrapeit.parser) - // Coil (async image loading, network image) implementation(libs.coil.compose) implementation(libs.coil.appiconloader) diff --git a/app/src/main/java/app/revanced/manager/network/service/HttpService.kt b/app/src/main/java/app/revanced/manager/network/service/HttpService.kt index e0b69aa6..e0dd4499 100644 --- a/app/src/main/java/app/revanced/manager/network/service/HttpService.kt +++ b/app/src/main/java/app/revanced/manager/network/service/HttpService.kt @@ -17,7 +17,6 @@ import io.ktor.http.isSuccess import io.ktor.utils.io.ByteReadChannel import io.ktor.utils.io.core.isNotEmpty import io.ktor.utils.io.core.readBytes -import it.skrape.core.htmlDocument import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.withContext import kotlinx.serialization.json.Json diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index 245dda89..a41ac459 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -29,7 +29,6 @@ dev-tools-gradle-plugin = "2.1.10-1.0.29" about-libraries-gradle-plugin = "12.1.2" coil = "2.7.0" app-icon-loader-coil = "1.5.0" -skrapeit = "1.2.2" libsu = "6.0.0" scrollbars = "1.0.4" enumutil = "1.1.1" @@ -99,10 +98,6 @@ ktor-okhttp = { group = "io.ktor", name = "ktor-client-okhttp", version.ref = "k ktor-content-negotiation = { group = "io.ktor", name = "ktor-client-content-negotiation", version.ref = "ktor" } ktor-serialization = { group = "io.ktor", name = "ktor-serialization-kotlinx-json", version.ref = "ktor" } -# HTML Scraper -skrapeit-dsl = { group = "it.skrape", name = "skrapeit-dsl", version.ref = "skrapeit" } -skrapeit-parser = { group = "it.skrape", name = "skrapeit-html-parser", version.ref = "skrapeit" } - # Markdown markdown-renderer = { group = "com.mikepenz", name = "multiplatform-markdown-renderer-m3", version.ref = "markdown-renderer" }