├── .gitignore ├── .idea ├── .gitignore ├── compiler.xml ├── deploymentTargetSelector.xml ├── gradle.xml ├── inspectionProfiles │ └── Project_Default.xml ├── kotlinc.xml ├── migrations.xml ├── misc.xml ├── other.xml └── vcs.xml ├── LICENSE ├── README.md ├── app ├── .gitignore ├── build.gradle.kts ├── proguard-rules.pro └── src │ ├── androidTest │ └── java │ │ └── wat │ │ └── app │ │ └── taskmanager │ │ └── ExampleInstrumentedTest.kt │ ├── main │ ├── AndroidManifest.xml │ ├── java │ │ ├── android │ │ │ ├── BinderInterface.java │ │ │ └── app │ │ │ │ └── IActivityManager.java │ │ └── wat │ │ │ └── app │ │ │ └── taskmanager │ │ │ ├── MainActivity.kt │ │ │ ├── prefs │ │ │ └── Settings.kt │ │ │ ├── screens │ │ │ ├── about │ │ │ │ └── AboutScreen.kt │ │ │ ├── detail │ │ │ │ └── AppDetailScreen.kt │ │ │ ├── hides │ │ │ │ └── HideAppListScreen.kt │ │ │ └── main │ │ │ │ ├── AppCell.kt │ │ │ │ ├── AppInfo.kt │ │ │ │ ├── InfoText.kt │ │ │ │ ├── MainEvent.kt │ │ │ │ ├── MainScreen.kt │ │ │ │ ├── MainState.kt │ │ │ │ └── MainViewModel.kt │ │ │ ├── shizuku │ │ │ ├── ShizukuAPI.kt │ │ │ ├── ShizukuState.kt │ │ │ └── ShizukuTools.kt │ │ │ └── utils │ │ │ ├── Composables.kt │ │ │ ├── Const.kt │ │ │ ├── PackageUtils.kt │ │ │ └── Update.kt │ └── res │ │ ├── drawable │ │ └── ic_launcher_foreground.xml │ │ ├── mipmap-anydpi-v26 │ │ ├── ic_launcher.xml │ │ └── ic_launcher_round.xml │ │ ├── mipmap-hdpi │ │ ├── ic_launcher.webp │ │ └── ic_launcher_round.webp │ │ ├── mipmap-mdpi │ │ ├── ic_launcher.webp │ │ └── ic_launcher_round.webp │ │ ├── mipmap-xhdpi │ │ ├── ic_launcher.webp │ │ └── ic_launcher_round.webp │ │ ├── mipmap-xxhdpi │ │ ├── ic_launcher.webp │ │ └── ic_launcher_round.webp │ │ ├── mipmap-xxxhdpi │ │ ├── ic_launcher.webp │ │ └── ic_launcher_round.webp │ │ └── values │ │ ├── ic_launcher_background.xml │ │ └── theme.xml │ └── test │ └── java │ └── wat │ └── app │ └── taskmanager │ └── ExampleUnitTest.kt ├── build.gradle.kts ├── gradle.properties ├── gradle ├── libs.versions.toml └── wrapper │ ├── gradle-wrapper.jar │ └── gradle-wrapper.properties ├── gradlew ├── gradlew.bat ├── lib-components ├── .gitignore ├── build.gradle.kts ├── consumer-rules.pro ├── proguard-rules.pro └── src │ └── main │ ├── AndroidManifest.xml │ └── java │ └── bakuen │ └── wear │ └── components │ ├── AutoSize.kt │ ├── Background.kt │ ├── Button.kt │ ├── Density.kt │ ├── Icon.kt │ ├── Modifier.kt │ ├── Shapes.kt │ ├── SimpleViewModel.kt │ ├── Space.kt │ ├── SwipeToDismiss.kt │ ├── Text.kt │ ├── TextField.kt │ ├── Theme.kt │ ├── material3 │ ├── ExperimentalMaterial3Api.kt │ ├── ProgressIndicator.kt │ ├── internal │ │ └── AccessibilityUtil.kt │ └── tokens │ │ ├── CircularProgressIndicatorTokens.kt │ │ ├── ColorSchemeKeyTokens.kt │ │ ├── LinearProgressIndicatorTokens.kt │ │ ├── MotionTokens.kt │ │ ├── ProgressIndicatorTokens.kt │ │ └── ShapeKeyTokens.kt │ └── wear │ └── Modifier.kt ├── lib-navigator ├── .gitignore ├── build.gradle.kts ├── consumer-rules.pro ├── proguard-rules.pro └── src │ └── main │ ├── AndroidManifest.xml │ └── java │ └── bakuen │ └── lib │ └── navigator │ ├── ContentNode.kt │ ├── Dialog.kt │ ├── EventChannel.kt │ ├── NavigationEvent.kt │ ├── NavigationWrapper.kt │ ├── Navigator.kt │ └── SwipeToDismiss.kt ├── lib-protostore ├── .gitignore ├── build.gradle.kts ├── consumer-rules.pro ├── proguard-rules.pro └── src │ └── main │ ├── AndroidManifest.xml │ └── java │ └── bakuen │ └── lib │ └── protostore │ ├── ContextProvider.kt │ └── ProtoStore.kt └── settings.gradle.kts /.gitignore: -------------------------------------------------------------------------------- 1 | *.iml 2 | .gradle 3 | /local.properties 4 | /.idea/caches 5 | /.idea/libraries 6 | /.idea/modules.xml 7 | /.idea/workspace.xml 8 | /.idea/navEditor.xml 9 | /.idea/assetWizardSettings.xml 10 | .DS_Store 11 | /build 12 | /captures 13 | .externalNativeBuild 14 | .cxx 15 | local.properties 16 | -------------------------------------------------------------------------------- /.idea/.gitignore: -------------------------------------------------------------------------------- 1 | # Default ignored files 2 | /shelf/ 3 | /workspace.xml 4 | -------------------------------------------------------------------------------- /.idea/compiler.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /.idea/deploymentTargetSelector.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 16 | 17 | 18 | -------------------------------------------------------------------------------- /.idea/gradle.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 21 | 22 | -------------------------------------------------------------------------------- /.idea/inspectionProfiles/Project_Default.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 32 | -------------------------------------------------------------------------------- /.idea/kotlinc.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 6 | -------------------------------------------------------------------------------- /.idea/migrations.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 9 | 10 | -------------------------------------------------------------------------------- /.idea/misc.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 10 | -------------------------------------------------------------------------------- /.idea/other.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 6 | -------------------------------------------------------------------------------- /.idea/vcs.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | GNU GENERAL PUBLIC LICENSE 2 | Version 3, 29 June 2007 3 | 4 | Copyright (C) 2007 Free Software Foundation, Inc. 5 | Everyone is permitted to copy and distribute verbatim copies 6 | of this license document, but changing it is not allowed. 7 | 8 | Preamble 9 | 10 | The GNU General Public License is a free, copyleft license for 11 | software and other kinds of works. 12 | 13 | The licenses for most software and other practical works are designed 14 | to take away your freedom to share and change the works. By contrast, 15 | the GNU General Public License is intended to guarantee your freedom to 16 | share and change all versions of a program--to make sure it remains free 17 | software for all its users. We, the Free Software Foundation, use the 18 | GNU General Public License for most of our software; it applies also to 19 | any other work released this way by its authors. You can apply it to 20 | your programs, too. 21 | 22 | When we speak of free software, we are referring to freedom, not 23 | price. Our General Public Licenses are designed to make sure that you 24 | have the freedom to distribute copies of free software (and charge for 25 | them if you wish), that you receive source code or can get it if you 26 | want it, that you can change the software or use pieces of it in new 27 | free programs, and that you know you can do these things. 28 | 29 | To protect your rights, we need to prevent others from denying you 30 | these rights or asking you to surrender the rights. Therefore, you have 31 | certain responsibilities if you distribute copies of the software, or if 32 | you modify it: responsibilities to respect the freedom of others. 33 | 34 | For example, if you distribute copies of such a program, whether 35 | gratis or for a fee, you must pass on to the recipients the same 36 | freedoms that you received. You must make sure that they, too, receive 37 | or can get the source code. And you must show them these terms so they 38 | know their rights. 39 | 40 | Developers that use the GNU GPL protect your rights with two steps: 41 | (1) assert copyright on the software, and (2) offer you this License 42 | giving you legal permission to copy, distribute and/or modify it. 43 | 44 | For the developers' and authors' protection, the GPL clearly explains 45 | that there is no warranty for this free software. For both users' and 46 | authors' sake, the GPL requires that modified versions be marked as 47 | changed, so that their problems will not be attributed erroneously to 48 | authors of previous versions. 49 | 50 | Some devices are designed to deny users access to install or run 51 | modified versions of the software inside them, although the manufacturer 52 | can do so. This is fundamentally incompatible with the aim of 53 | protecting users' freedom to change the software. The systematic 54 | pattern of such abuse occurs in the area of products for individuals to 55 | use, which is precisely where it is most unacceptable. Therefore, we 56 | have designed this version of the GPL to prohibit the practice for those 57 | products. If such problems arise substantially in other domains, we 58 | stand ready to extend this provision to those domains in future versions 59 | of the GPL, as needed to protect the freedom of users. 60 | 61 | Finally, every program is threatened constantly by software patents. 62 | States should not allow patents to restrict development and use of 63 | software on general-purpose computers, but in those that do, we wish to 64 | avoid the special danger that patents applied to a free program could 65 | make it effectively proprietary. To prevent this, the GPL assures that 66 | patents cannot be used to render the program non-free. 67 | 68 | The precise terms and conditions for copying, distribution and 69 | modification follow. 70 | 71 | TERMS AND CONDITIONS 72 | 73 | 0. Definitions. 74 | 75 | "This License" refers to version 3 of the GNU General Public License. 76 | 77 | "Copyright" also means copyright-like laws that apply to other kinds of 78 | works, such as semiconductor masks. 79 | 80 | "The Program" refers to any copyrightable work licensed under this 81 | License. Each licensee is addressed as "you". "Licensees" and 82 | "recipients" may be individuals or organizations. 83 | 84 | To "modify" a work means to copy from or adapt all or part of the work 85 | in a fashion requiring copyright permission, other than the making of an 86 | exact copy. The resulting work is called a "modified version" of the 87 | earlier work or a work "based on" the earlier work. 88 | 89 | A "covered work" means either the unmodified Program or a work based 90 | on the Program. 91 | 92 | To "propagate" a work means to do anything with it that, without 93 | permission, would make you directly or secondarily liable for 94 | infringement under applicable copyright law, except executing it on a 95 | computer or modifying a private copy. Propagation includes copying, 96 | distribution (with or without modification), making available to the 97 | public, and in some countries other activities as well. 98 | 99 | To "convey" a work means any kind of propagation that enables other 100 | parties to make or receive copies. Mere interaction with a user through 101 | a computer network, with no transfer of a copy, is not conveying. 102 | 103 | An interactive user interface displays "Appropriate Legal Notices" 104 | to the extent that it includes a convenient and prominently visible 105 | feature that (1) displays an appropriate copyright notice, and (2) 106 | tells the user that there is no warranty for the work (except to the 107 | extent that warranties are provided), that licensees may convey the 108 | work under this License, and how to view a copy of this License. If 109 | the interface presents a list of user commands or options, such as a 110 | menu, a prominent item in the list meets this criterion. 111 | 112 | 1. Source Code. 113 | 114 | The "source code" for a work means the preferred form of the work 115 | for making modifications to it. "Object code" means any non-source 116 | form of a work. 117 | 118 | A "Standard Interface" means an interface that either is an official 119 | standard defined by a recognized standards body, or, in the case of 120 | interfaces specified for a particular programming language, one that 121 | is widely used among developers working in that language. 122 | 123 | The "System Libraries" of an executable work include anything, other 124 | than the work as a whole, that (a) is included in the normal form of 125 | packaging a Major Component, but which is not part of that Major 126 | Component, and (b) serves only to enable use of the work with that 127 | Major Component, or to implement a Standard Interface for which an 128 | implementation is available to the public in source code form. A 129 | "Major Component", in this context, means a major essential component 130 | (kernel, window system, and so on) of the specific operating system 131 | (if any) on which the executable work runs, or a compiler used to 132 | produce the work, or an object code interpreter used to run it. 133 | 134 | The "Corresponding Source" for a work in object code form means all 135 | the source code needed to generate, install, and (for an executable 136 | work) run the object code and to modify the work, including scripts to 137 | control those activities. However, it does not include the work's 138 | System Libraries, or general-purpose tools or generally available free 139 | programs which are used unmodified in performing those activities but 140 | which are not part of the work. For example, Corresponding Source 141 | includes interface definition files associated with source files for 142 | the work, and the source code for shared libraries and dynamically 143 | linked subprograms that the work is specifically designed to require, 144 | such as by intimate data communication or control flow between those 145 | subprograms and other parts of the work. 146 | 147 | The Corresponding Source need not include anything that users 148 | can regenerate automatically from other parts of the Corresponding 149 | Source. 150 | 151 | The Corresponding Source for a work in source code form is that 152 | same work. 153 | 154 | 2. Basic Permissions. 155 | 156 | All rights granted under this License are granted for the term of 157 | copyright on the Program, and are irrevocable provided the stated 158 | conditions are met. This License explicitly affirms your unlimited 159 | permission to run the unmodified Program. The output from running a 160 | covered work is covered by this License only if the output, given its 161 | content, constitutes a covered work. This License acknowledges your 162 | rights of fair use or other equivalent, as provided by copyright law. 163 | 164 | You may make, run and propagate covered works that you do not 165 | convey, without conditions so long as your license otherwise remains 166 | in force. You may convey covered works to others for the sole purpose 167 | of having them make modifications exclusively for you, or provide you 168 | with facilities for running those works, provided that you comply with 169 | the terms of this License in conveying all material for which you do 170 | not control copyright. Those thus making or running the covered works 171 | for you must do so exclusively on your behalf, under your direction 172 | and control, on terms that prohibit them from making any copies of 173 | your copyrighted material outside their relationship with you. 174 | 175 | Conveying under any other circumstances is permitted solely under 176 | the conditions stated below. Sublicensing is not allowed; section 10 177 | makes it unnecessary. 178 | 179 | 3. Protecting Users' Legal Rights From Anti-Circumvention Law. 180 | 181 | No covered work shall be deemed part of an effective technological 182 | measure under any applicable law fulfilling obligations under article 183 | 11 of the WIPO copyright treaty adopted on 20 December 1996, or 184 | similar laws prohibiting or restricting circumvention of such 185 | measures. 186 | 187 | When you convey a covered work, you waive any legal power to forbid 188 | circumvention of technological measures to the extent such circumvention 189 | is effected by exercising rights under this License with respect to 190 | the covered work, and you disclaim any intention to limit operation or 191 | modification of the work as a means of enforcing, against the work's 192 | users, your or third parties' legal rights to forbid circumvention of 193 | technological measures. 194 | 195 | 4. Conveying Verbatim Copies. 196 | 197 | You may convey verbatim copies of the Program's source code as you 198 | receive it, in any medium, provided that you conspicuously and 199 | appropriately publish on each copy an appropriate copyright notice; 200 | keep intact all notices stating that this License and any 201 | non-permissive terms added in accord with section 7 apply to the code; 202 | keep intact all notices of the absence of any warranty; and give all 203 | recipients a copy of this License along with the Program. 204 | 205 | You may charge any price or no price for each copy that you convey, 206 | and you may offer support or warranty protection for a fee. 207 | 208 | 5. Conveying Modified Source Versions. 209 | 210 | You may convey a work based on the Program, or the modifications to 211 | produce it from the Program, in the form of source code under the 212 | terms of section 4, provided that you also meet all of these conditions: 213 | 214 | a) The work must carry prominent notices stating that you modified 215 | it, and giving a relevant date. 216 | 217 | b) The work must carry prominent notices stating that it is 218 | released under this License and any conditions added under section 219 | 7. This requirement modifies the requirement in section 4 to 220 | "keep intact all notices". 221 | 222 | c) You must license the entire work, as a whole, under this 223 | License to anyone who comes into possession of a copy. This 224 | License will therefore apply, along with any applicable section 7 225 | additional terms, to the whole of the work, and all its parts, 226 | regardless of how they are packaged. This License gives no 227 | permission to license the work in any other way, but it does not 228 | invalidate such permission if you have separately received it. 229 | 230 | d) If the work has interactive user interfaces, each must display 231 | Appropriate Legal Notices; however, if the Program has interactive 232 | interfaces that do not display Appropriate Legal Notices, your 233 | work need not make them do so. 234 | 235 | A compilation of a covered work with other separate and independent 236 | works, which are not by their nature extensions of the covered work, 237 | and which are not combined with it such as to form a larger program, 238 | in or on a volume of a storage or distribution medium, is called an 239 | "aggregate" if the compilation and its resulting copyright are not 240 | used to limit the access or legal rights of the compilation's users 241 | beyond what the individual works permit. Inclusion of a covered work 242 | in an aggregate does not cause this License to apply to the other 243 | parts of the aggregate. 244 | 245 | 6. Conveying Non-Source Forms. 246 | 247 | You may convey a covered work in object code form under the terms 248 | of sections 4 and 5, provided that you also convey the 249 | machine-readable Corresponding Source under the terms of this License, 250 | in one of these ways: 251 | 252 | a) Convey the object code in, or embodied in, a physical product 253 | (including a physical distribution medium), accompanied by the 254 | Corresponding Source fixed on a durable physical medium 255 | customarily used for software interchange. 256 | 257 | b) Convey the object code in, or embodied in, a physical product 258 | (including a physical distribution medium), accompanied by a 259 | written offer, valid for at least three years and valid for as 260 | long as you offer spare parts or customer support for that product 261 | model, to give anyone who possesses the object code either (1) a 262 | copy of the Corresponding Source for all the software in the 263 | product that is covered by this License, on a durable physical 264 | medium customarily used for software interchange, for a price no 265 | more than your reasonable cost of physically performing this 266 | conveying of source, or (2) access to copy the 267 | Corresponding Source from a network server at no charge. 268 | 269 | c) Convey individual copies of the object code with a copy of the 270 | written offer to provide the Corresponding Source. This 271 | alternative is allowed only occasionally and noncommercially, and 272 | only if you received the object code with such an offer, in accord 273 | with subsection 6b. 274 | 275 | d) Convey the object code by offering access from a designated 276 | place (gratis or for a charge), and offer equivalent access to the 277 | Corresponding Source in the same way through the same place at no 278 | further charge. You need not require recipients to copy the 279 | Corresponding Source along with the object code. If the place to 280 | copy the object code is a network server, the Corresponding Source 281 | may be on a different server (operated by you or a third party) 282 | that supports equivalent copying facilities, provided you maintain 283 | clear directions next to the object code saying where to find the 284 | Corresponding Source. Regardless of what server hosts the 285 | Corresponding Source, you remain obligated to ensure that it is 286 | available for as long as needed to satisfy these requirements. 287 | 288 | e) Convey the object code using peer-to-peer transmission, provided 289 | you inform other peers where the object code and Corresponding 290 | Source of the work are being offered to the general public at no 291 | charge under subsection 6d. 292 | 293 | A separable portion of the object code, whose source code is excluded 294 | from the Corresponding Source as a System Library, need not be 295 | included in conveying the object code work. 296 | 297 | A "User Product" is either (1) a "consumer product", which means any 298 | tangible personal property which is normally used for personal, family, 299 | or household purposes, or (2) anything designed or sold for incorporation 300 | into a dwelling. In determining whether a product is a consumer product, 301 | doubtful cases shall be resolved in favor of coverage. For a particular 302 | product received by a particular user, "normally used" refers to a 303 | typical or common use of that class of product, regardless of the status 304 | of the particular user or of the way in which the particular user 305 | actually uses, or expects or is expected to use, the product. A product 306 | is a consumer product regardless of whether the product has substantial 307 | commercial, industrial or non-consumer uses, unless such uses represent 308 | the only significant mode of use of the product. 309 | 310 | "Installation Information" for a User Product means any methods, 311 | procedures, authorization keys, or other information required to install 312 | and execute modified versions of a covered work in that User Product from 313 | a modified version of its Corresponding Source. The information must 314 | suffice to ensure that the continued functioning of the modified object 315 | code is in no case prevented or interfered with solely because 316 | modification has been made. 317 | 318 | If you convey an object code work under this section in, or with, or 319 | specifically for use in, a User Product, and the conveying occurs as 320 | part of a transaction in which the right of possession and use of the 321 | User Product is transferred to the recipient in perpetuity or for a 322 | fixed term (regardless of how the transaction is characterized), the 323 | Corresponding Source conveyed under this section must be accompanied 324 | by the Installation Information. But this requirement does not apply 325 | if neither you nor any third party retains the ability to install 326 | modified object code on the User Product (for example, the work has 327 | been installed in ROM). 328 | 329 | The requirement to provide Installation Information does not include a 330 | requirement to continue to provide support service, warranty, or updates 331 | for a work that has been modified or installed by the recipient, or for 332 | the User Product in which it has been modified or installed. Access to a 333 | network may be denied when the modification itself materially and 334 | adversely affects the operation of the network or violates the rules and 335 | protocols for communication across the network. 336 | 337 | Corresponding Source conveyed, and Installation Information provided, 338 | in accord with this section must be in a format that is publicly 339 | documented (and with an implementation available to the public in 340 | source code form), and must require no special password or key for 341 | unpacking, reading or copying. 342 | 343 | 7. Additional Terms. 344 | 345 | "Additional permissions" are terms that supplement the terms of this 346 | License by making exceptions from one or more of its conditions. 347 | Additional permissions that are applicable to the entire Program shall 348 | be treated as though they were included in this License, to the extent 349 | that they are valid under applicable law. If additional permissions 350 | apply only to part of the Program, that part may be used separately 351 | under those permissions, but the entire Program remains governed by 352 | this License without regard to the additional permissions. 353 | 354 | When you convey a copy of a covered work, you may at your option 355 | remove any additional permissions from that copy, or from any part of 356 | it. (Additional permissions may be written to require their own 357 | removal in certain cases when you modify the work.) You may place 358 | additional permissions on material, added by you to a covered work, 359 | for which you have or can give appropriate copyright permission. 360 | 361 | Notwithstanding any other provision of this License, for material you 362 | add to a covered work, you may (if authorized by the copyright holders of 363 | that material) supplement the terms of this License with terms: 364 | 365 | a) Disclaiming warranty or limiting liability differently from the 366 | terms of sections 15 and 16 of this License; or 367 | 368 | b) Requiring preservation of specified reasonable legal notices or 369 | author attributions in that material or in the Appropriate Legal 370 | Notices displayed by works containing it; or 371 | 372 | c) Prohibiting misrepresentation of the origin of that material, or 373 | requiring that modified versions of such material be marked in 374 | reasonable ways as different from the original version; or 375 | 376 | d) Limiting the use for publicity purposes of names of licensors or 377 | authors of the material; or 378 | 379 | e) Declining to grant rights under trademark law for use of some 380 | trade names, trademarks, or service marks; or 381 | 382 | f) Requiring indemnification of licensors and authors of that 383 | material by anyone who conveys the material (or modified versions of 384 | it) with contractual assumptions of liability to the recipient, for 385 | any liability that these contractual assumptions directly impose on 386 | those licensors and authors. 387 | 388 | All other non-permissive additional terms are considered "further 389 | restrictions" within the meaning of section 10. If the Program as you 390 | received it, or any part of it, contains a notice stating that it is 391 | governed by this License along with a term that is a further 392 | restriction, you may remove that term. If a license document contains 393 | a further restriction but permits relicensing or conveying under this 394 | License, you may add to a covered work material governed by the terms 395 | of that license document, provided that the further restriction does 396 | not survive such relicensing or conveying. 397 | 398 | If you add terms to a covered work in accord with this section, you 399 | must place, in the relevant source files, a statement of the 400 | additional terms that apply to those files, or a notice indicating 401 | where to find the applicable terms. 402 | 403 | Additional terms, permissive or non-permissive, may be stated in the 404 | form of a separately written license, or stated as exceptions; 405 | the above requirements apply either way. 406 | 407 | 8. Termination. 408 | 409 | You may not propagate or modify a covered work except as expressly 410 | provided under this License. Any attempt otherwise to propagate or 411 | modify it is void, and will automatically terminate your rights under 412 | this License (including any patent licenses granted under the third 413 | paragraph of section 11). 414 | 415 | However, if you cease all violation of this License, then your 416 | license from a particular copyright holder is reinstated (a) 417 | provisionally, unless and until the copyright holder explicitly and 418 | finally terminates your license, and (b) permanently, if the copyright 419 | holder fails to notify you of the violation by some reasonable means 420 | prior to 60 days after the cessation. 421 | 422 | Moreover, your license from a particular copyright holder is 423 | reinstated permanently if the copyright holder notifies you of the 424 | violation by some reasonable means, this is the first time you have 425 | received notice of violation of this License (for any work) from that 426 | copyright holder, and you cure the violation prior to 30 days after 427 | your receipt of the notice. 428 | 429 | Termination of your rights under this section does not terminate the 430 | licenses of parties who have received copies or rights from you under 431 | this License. If your rights have been terminated and not permanently 432 | reinstated, you do not qualify to receive new licenses for the same 433 | material under section 10. 434 | 435 | 9. Acceptance Not Required for Having Copies. 436 | 437 | You are not required to accept this License in order to receive or 438 | run a copy of the Program. Ancillary propagation of a covered work 439 | occurring solely as a consequence of using peer-to-peer transmission 440 | to receive a copy likewise does not require acceptance. However, 441 | nothing other than this License grants you permission to propagate or 442 | modify any covered work. These actions infringe copyright if you do 443 | not accept this License. Therefore, by modifying or propagating a 444 | covered work, you indicate your acceptance of this License to do so. 445 | 446 | 10. Automatic Licensing of Downstream Recipients. 447 | 448 | Each time you convey a covered work, the recipient automatically 449 | receives a license from the original licensors, to run, modify and 450 | propagate that work, subject to this License. You are not responsible 451 | for enforcing compliance by third parties with this License. 452 | 453 | An "entity transaction" is a transaction transferring control of an 454 | organization, or substantially all assets of one, or subdividing an 455 | organization, or merging organizations. If propagation of a covered 456 | work results from an entity transaction, each party to that 457 | transaction who receives a copy of the work also receives whatever 458 | licenses to the work the party's predecessor in interest had or could 459 | give under the previous paragraph, plus a right to possession of the 460 | Corresponding Source of the work from the predecessor in interest, if 461 | the predecessor has it or can get it with reasonable efforts. 462 | 463 | You may not impose any further restrictions on the exercise of the 464 | rights granted or affirmed under this License. For example, you may 465 | not impose a license fee, royalty, or other charge for exercise of 466 | rights granted under this License, and you may not initiate litigation 467 | (including a cross-claim or counterclaim in a lawsuit) alleging that 468 | any patent claim is infringed by making, using, selling, offering for 469 | sale, or importing the Program or any portion of it. 470 | 471 | 11. Patents. 472 | 473 | A "contributor" is a copyright holder who authorizes use under this 474 | License of the Program or a work on which the Program is based. The 475 | work thus licensed is called the contributor's "contributor version". 476 | 477 | A contributor's "essential patent claims" are all patent claims 478 | owned or controlled by the contributor, whether already acquired or 479 | hereafter acquired, that would be infringed by some manner, permitted 480 | by this License, of making, using, or selling its contributor version, 481 | but do not include claims that would be infringed only as a 482 | consequence of further modification of the contributor version. For 483 | purposes of this definition, "control" includes the right to grant 484 | patent sublicenses in a manner consistent with the requirements of 485 | this License. 486 | 487 | Each contributor grants you a non-exclusive, worldwide, royalty-free 488 | patent license under the contributor's essential patent claims, to 489 | make, use, sell, offer for sale, import and otherwise run, modify and 490 | propagate the contents of its contributor version. 491 | 492 | In the following three paragraphs, a "patent license" is any express 493 | agreement or commitment, however denominated, not to enforce a patent 494 | (such as an express permission to practice a patent or covenant not to 495 | sue for patent infringement). To "grant" such a patent license to a 496 | party means to make such an agreement or commitment not to enforce a 497 | patent against the party. 498 | 499 | If you convey a covered work, knowingly relying on a patent license, 500 | and the Corresponding Source of the work is not available for anyone 501 | to copy, free of charge and under the terms of this License, through a 502 | publicly available network server or other readily accessible means, 503 | then you must either (1) cause the Corresponding Source to be so 504 | available, or (2) arrange to deprive yourself of the benefit of the 505 | patent license for this particular work, or (3) arrange, in a manner 506 | consistent with the requirements of this License, to extend the patent 507 | license to downstream recipients. "Knowingly relying" means you have 508 | actual knowledge that, but for the patent license, your conveying the 509 | covered work in a country, or your recipient's use of the covered work 510 | in a country, would infringe one or more identifiable patents in that 511 | country that you have reason to believe are valid. 512 | 513 | If, pursuant to or in connection with a single transaction or 514 | arrangement, you convey, or propagate by procuring conveyance of, a 515 | covered work, and grant a patent license to some of the parties 516 | receiving the covered work authorizing them to use, propagate, modify 517 | or convey a specific copy of the covered work, then the patent license 518 | you grant is automatically extended to all recipients of the covered 519 | work and works based on it. 520 | 521 | A patent license is "discriminatory" if it does not include within 522 | the scope of its coverage, prohibits the exercise of, or is 523 | conditioned on the non-exercise of one or more of the rights that are 524 | specifically granted under this License. You may not convey a covered 525 | work if you are a party to an arrangement with a third party that is 526 | in the business of distributing software, under which you make payment 527 | to the third party based on the extent of your activity of conveying 528 | the work, and under which the third party grants, to any of the 529 | parties who would receive the covered work from you, a discriminatory 530 | patent license (a) in connection with copies of the covered work 531 | conveyed by you (or copies made from those copies), or (b) primarily 532 | for and in connection with specific products or compilations that 533 | contain the covered work, unless you entered into that arrangement, 534 | or that patent license was granted, prior to 28 March 2007. 535 | 536 | Nothing in this License shall be construed as excluding or limiting 537 | any implied license or other defenses to infringement that may 538 | otherwise be available to you under applicable patent law. 539 | 540 | 12. No Surrender of Others' Freedom. 541 | 542 | If conditions are imposed on you (whether by court order, agreement or 543 | otherwise) that contradict the conditions of this License, they do not 544 | excuse you from the conditions of this License. If you cannot convey a 545 | covered work so as to satisfy simultaneously your obligations under this 546 | License and any other pertinent obligations, then as a consequence you may 547 | not convey it at all. For example, if you agree to terms that obligate you 548 | to collect a royalty for further conveying from those to whom you convey 549 | the Program, the only way you could satisfy both those terms and this 550 | License would be to refrain entirely from conveying the Program. 551 | 552 | 13. Use with the GNU Affero General Public License. 553 | 554 | Notwithstanding any other provision of this License, you have 555 | permission to link or combine any covered work with a work licensed 556 | under version 3 of the GNU Affero General Public License into a single 557 | combined work, and to convey the resulting work. The terms of this 558 | License will continue to apply to the part which is the covered work, 559 | but the special requirements of the GNU Affero General Public License, 560 | section 13, concerning interaction through a network will apply to the 561 | combination as such. 562 | 563 | 14. Revised Versions of this License. 564 | 565 | The Free Software Foundation may publish revised and/or new versions of 566 | the GNU General Public License from time to time. Such new versions will 567 | be similar in spirit to the present version, but may differ in detail to 568 | address new problems or concerns. 569 | 570 | Each version is given a distinguishing version number. If the 571 | Program specifies that a certain numbered version of the GNU General 572 | Public License "or any later version" applies to it, you have the 573 | option of following the terms and conditions either of that numbered 574 | version or of any later version published by the Free Software 575 | Foundation. If the Program does not specify a version number of the 576 | GNU General Public License, you may choose any version ever published 577 | by the Free Software Foundation. 578 | 579 | If the Program specifies that a proxy can decide which future 580 | versions of the GNU General Public License can be used, that proxy's 581 | public statement of acceptance of a version permanently authorizes you 582 | to choose that version for the Program. 583 | 584 | Later license versions may give you additional or different 585 | permissions. However, no additional obligations are imposed on any 586 | author or copyright holder as a result of your choosing to follow a 587 | later version. 588 | 589 | 15. Disclaimer of Warranty. 590 | 591 | THERE IS NO WARRANTY FOR THE PROGRAM, TO THE EXTENT PERMITTED BY 592 | APPLICABLE LAW. EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT 593 | HOLDERS AND/OR OTHER PARTIES PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY 594 | OF ANY KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, 595 | THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR 596 | PURPOSE. THE ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE PROGRAM 597 | IS WITH YOU. SHOULD THE PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF 598 | ALL NECESSARY SERVICING, REPAIR OR CORRECTION. 599 | 600 | 16. Limitation of Liability. 601 | 602 | IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING 603 | WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MODIFIES AND/OR CONVEYS 604 | THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, INCLUDING ANY 605 | GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING OUT OF THE 606 | USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED TO LOSS OF 607 | DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY YOU OR THIRD 608 | PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER PROGRAMS), 609 | EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF 610 | SUCH DAMAGES. 611 | 612 | 17. Interpretation of Sections 15 and 16. 613 | 614 | If the disclaimer of warranty and limitation of liability provided 615 | above cannot be given local legal effect according to their terms, 616 | reviewing courts shall apply local law that most closely approximates 617 | an absolute waiver of all civil liability in connection with the 618 | Program, unless a warranty or assumption of liability accompanies a 619 | copy of the Program in return for a fee. 620 | 621 | END OF TERMS AND CONDITIONS 622 | 623 | How to Apply These Terms to Your New Programs 624 | 625 | If you develop a new program, and you want it to be of the greatest 626 | possible use to the public, the best way to achieve this is to make it 627 | free software which everyone can redistribute and change under these terms. 628 | 629 | To do so, attach the following notices to the program. It is safest 630 | to attach them to the start of each source file to most effectively 631 | state the exclusion of warranty; and each file should have at least 632 | the "copyright" line and a pointer to where the full notice is found. 633 | 634 | 635 | Copyright (C) 636 | 637 | This program is free software: you can redistribute it and/or modify 638 | it under the terms of the GNU General Public License as published by 639 | the Free Software Foundation, either version 3 of the License, or 640 | (at your option) any later version. 641 | 642 | This program is distributed in the hope that it will be useful, 643 | but WITHOUT ANY WARRANTY; without even the implied warranty of 644 | MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 645 | GNU General Public License for more details. 646 | 647 | You should have received a copy of the GNU General Public License 648 | along with this program. If not, see . 649 | 650 | Also add information on how to contact you by electronic and paper mail. 651 | 652 | If the program does terminal interaction, make it output a short 653 | notice like this when it starts in an interactive mode: 654 | 655 | Copyright (C) 656 | This program comes with ABSOLUTELY NO WARRANTY; for details type `show w'. 657 | This is free software, and you are welcome to redistribute it 658 | under certain conditions; type `show c' for details. 659 | 660 | The hypothetical commands `show w' and `show c' should show the appropriate 661 | parts of the General Public License. Of course, your program's commands 662 | might be different; for a GUI interface, you would use an "about box". 663 | 664 | You should also get your employer (if you work as a programmer) or school, 665 | if any, to sign a "copyright disclaimer" for the program, if necessary. 666 | For more information on this, and how to apply and follow the GNU GPL, see 667 | . 668 | 669 | The GNU General Public License does not permit incorporating your program 670 | into proprietary programs. If your program is a subroutine library, you 671 | may consider it more useful to permit linking proprietary applications with 672 | the library. If this is what you want to do, use the GNU Lesser General 673 | Public License instead of this License. But first, please read 674 | . 675 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Task Manager 2 | 一个简单的 Android 应用,为一些没有后台管理页功能的设备提供清理后台的能力,如`WearOS 2` 3 | 4 | 下载:https://bke.lanzoub.com/b02lstn5je 5 | 6 | 密码:30433 7 | 8 | ![shot](https://img.picui.cn/free/2024/08/29/66d08fbdc75f8.png)![shot](https://img.picui.cn/free/2024/08/29/66d08fbd99f04.png)![shot](https://img.picui.cn/free/2024/08/29/66d08fbdc8d08.png) 9 | 10 | ### 使用 11 | `TaskManager`需要[`Shizuku`](https://github.com/RikkaApps/Shizuku)激活。对于安卓手表等小屏设备,我推荐使用[`Shizuku-wear`](https://github.com/java30433/Shizuku-wear)。激活方式不再赘述 12 | 13 | 在最近应用列表中,**单击**可以打开当前应用,**长按**可以查看应用信息,**从右往左滑**可以强行停止APP 14 | 15 | 由于方法的限制,最近应用列表中显示的并非打开过的APP,而是所有在后台中运行的APP进程,划卡强停后,所有于此APP相关的进程都会被杀死(类似于在设置中使用“强行停止”)。因此代码中预先隐藏了一部分重要的系统进程、Shizuku 和 TaskManager 本身。某些系统进程在强行停止后会自动重启,这是不可避免的,您可以长按后将其从任务列表中隐藏。 16 | 17 | ### 技术细节 18 | 使用了`Jetpack Compose`作为UI框架 19 | 20 | `lib-navigator`是我编写的 compose 导航库 21 | 22 | `lib-protostore`是我编写的基于`kotlinx-serialization-protobuf`实现的数据持久化库 23 | 24 | 所有代码均以 GPLv3 协议开源。 25 | 26 | ### 更新日志 27 | - 2024-08-29 v1.1.0 28 | - 添加了在列表中隐藏应用的功能 29 | - 在 Github 开源 30 | - 2024-08-28 v1.0.0 31 | - 第一个版本 -------------------------------------------------------------------------------- /app/.gitignore: -------------------------------------------------------------------------------- 1 | /build 2 | /signing.gradle.kts 3 | /release -------------------------------------------------------------------------------- /app/build.gradle.kts: -------------------------------------------------------------------------------- 1 | apply("./signing.gradle.kts") 2 | plugins { 3 | alias(libs.plugins.android.application) 4 | alias(libs.plugins.jetbrains.kotlin.android) 5 | alias(libs.plugins.compose.compiler) 6 | alias(libs.plugins.kotlinx.serialization) 7 | } 8 | 9 | android { 10 | namespace = "wat.app.taskmanager" 11 | compileSdk = 34 12 | 13 | applicationVariants.configureEach { 14 | outputs.configureEach { 15 | (this as com.android.build.gradle.internal.api.BaseVariantOutputImpl).outputFileName = 16 | "TaskManager-${defaultConfig.versionName}.apk" 17 | } 18 | } 19 | signingConfigs { 20 | create("release") { 21 | @Suppress("UNCHECKED_CAST") 22 | fun ext(key: String) = rootProject.extra[key] as T 23 | storeFile = ext("storeFile") 24 | storePassword = ext("storePassword") 25 | keyAlias = ext("keyAlias") 26 | keyPassword = ext("keyPassword") 27 | } 28 | } 29 | defaultConfig { 30 | applicationId = "wat.app.taskmanager" 31 | minSdk = 24 32 | targetSdk = 35 33 | versionCode = 2 34 | versionName = "1.1.0" 35 | 36 | testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner" 37 | ndk { 38 | abiFilters.addAll(setOf("armeabi","x86","armeabi-v7a","x86_64","arm64-v8a")) 39 | } 40 | } 41 | buildFeatures { 42 | buildConfig = true 43 | compose = true 44 | } 45 | buildTypes { 46 | release { 47 | isMinifyEnabled = true 48 | proguardFiles( 49 | getDefaultProguardFile("proguard-android-optimize.txt"), 50 | "proguard-rules.pro" 51 | ) 52 | signingConfig = signingConfigs["release"] 53 | } 54 | debug { 55 | isMinifyEnabled = false 56 | signingConfig = signingConfigs["release"] 57 | } 58 | } 59 | compileOptions { 60 | sourceCompatibility = JavaVersion.VERSION_1_8 61 | targetCompatibility = JavaVersion.VERSION_1_8 62 | } 63 | kotlinOptions { 64 | jvmTarget = "1.8" 65 | } 66 | composeOptions { 67 | kotlinCompilerExtensionVersion = "1.5.1" 68 | } 69 | } 70 | 71 | dependencies { 72 | implementation(project(":lib-components")) 73 | implementation(project(":lib-navigator")) 74 | implementation(project(":lib-protostore")) 75 | implementation(libs.shizuku.api) 76 | implementation(libs.shizuku.provider) 77 | 78 | implementation(libs.androidx.activity.compose) 79 | implementation(platform(libs.androidx.compose.bom)) 80 | implementation(libs.androidx.compose.ui) 81 | implementation(libs.androidx.compose.foundation) 82 | implementation(libs.androidx.compose.ui.graphics) 83 | } -------------------------------------------------------------------------------- /app/proguard-rules.pro: -------------------------------------------------------------------------------- 1 | -keepattributes SourceFile,LineNumberTable 2 | -ignorewarnings 3 | -optimizations !method/inlining/* 4 | -keepnames class wat.** { *; } 5 | -keepnames class bakuen.** { *; } 6 | -keep class * implements android.BinderInterface { *; } -------------------------------------------------------------------------------- /app/src/androidTest/java/wat/app/taskmanager/ExampleInstrumentedTest.kt: -------------------------------------------------------------------------------- 1 | package wat.app.taskmanager 2 | 3 | import androidx.test.platform.app.InstrumentationRegistry 4 | import androidx.test.ext.junit.runners.AndroidJUnit4 5 | 6 | import org.junit.Test 7 | import org.junit.runner.RunWith 8 | 9 | import org.junit.Assert.* 10 | 11 | /** 12 | * Instrumented test, which will execute on an Android device. 13 | * 14 | * See [testing documentation](http://d.android.com/tools/testing). 15 | */ 16 | @RunWith(AndroidJUnit4::class) 17 | class ExampleInstrumentedTest { 18 | @Test 19 | fun useAppContext() { 20 | // Context of the app under test. 21 | val appContext = InstrumentationRegistry.getInstrumentation().targetContext 22 | assertEquals("wat.app.taskmanager", appContext.packageName) 23 | } 24 | } -------------------------------------------------------------------------------- /app/src/main/AndroidManifest.xml: -------------------------------------------------------------------------------- 1 | 2 | 4 | 5 | 12 | 15 | 17 | 18 | 19 | 20 | 21 | 22 | 29 | 30 | 31 | -------------------------------------------------------------------------------- /app/src/main/java/android/BinderInterface.java: -------------------------------------------------------------------------------- 1 | package android; 2 | 3 | /** 4 | * 这个类只是用来在 proguard 里作标记的,避免 android 包下面的 binder 被 r8 混淆掉 5 | */ 6 | public interface BinderInterface { 7 | } -------------------------------------------------------------------------------- /app/src/main/java/android/app/IActivityManager.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (C) 2016 The Android Open Source Project 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | package android.app; 18 | 19 | import android.BinderInterface; 20 | import android.os.Binder; 21 | import android.os.IBinder; 22 | 23 | import androidx.annotation.Keep; 24 | 25 | import java.util.List; 26 | 27 | public interface IActivityManager extends BinderInterface { 28 | List getRunningAppProcesses(); 29 | void forceStopPackage(String packageName, int userId); 30 | 31 | abstract class Stub extends Binder implements IActivityManager { 32 | public static IActivityManager asInterface(IBinder obj) { 33 | throw new UnsupportedOperationException(); 34 | } 35 | } 36 | 37 | } -------------------------------------------------------------------------------- /app/src/main/java/wat/app/taskmanager/MainActivity.kt: -------------------------------------------------------------------------------- 1 | package wat.app.taskmanager 2 | 3 | import android.content.Context 4 | import android.os.Bundle 5 | import androidx.activity.ComponentActivity 6 | import androidx.activity.compose.setContent 7 | import androidx.compose.ui.graphics.Color 8 | import bakuen.lib.navigator.NavHost 9 | import bakuen.wear.components.AutoSize 10 | import bakuen.wear.components.Surface 11 | import bakuen.wear.components.rememberViewModel 12 | import wat.app.taskmanager.screens.main.MainScreen 13 | import wat.app.taskmanager.screens.main.MainViewModel 14 | import wat.app.taskmanager.shizuku.ShizukuTools 15 | import wat.app.taskmanager.utils.Update 16 | 17 | lateinit var appContext: Context 18 | private set 19 | class MainActivity : ComponentActivity() { 20 | 21 | override fun onCreate(savedInstanceState: Bundle?) { 22 | super.onCreate(savedInstanceState) 23 | appContext = applicationContext 24 | Update.check(this) 25 | ShizukuTools.requestPermission() 26 | setContent { 27 | AutoSize(designWidth = 192f) { 28 | NavHost(initScreen = { MainScreen(rememberViewModel { MainViewModel() }) }) { 29 | Surface(color = Color.Black, fillMaxSize = true) { 30 | it() 31 | } 32 | } 33 | } 34 | } 35 | } 36 | 37 | override fun onPause() { 38 | super.onPause() 39 | finish() 40 | } 41 | } -------------------------------------------------------------------------------- /app/src/main/java/wat/app/taskmanager/prefs/Settings.kt: -------------------------------------------------------------------------------- 1 | package wat.app.taskmanager.prefs 2 | 3 | import bakuen.lib.protostore.ProtoStore 4 | import kotlinx.serialization.ExperimentalSerializationApi 5 | import kotlinx.serialization.Serializable 6 | import kotlinx.serialization.protobuf.ProtoNumber 7 | 8 | @Serializable 9 | data class Settings @OptIn(ExperimentalSerializationApi::class) constructor( 10 | @ProtoNumber(1) 11 | val hidePackages: Set = setOf() 12 | ) : ProtoStore -------------------------------------------------------------------------------- /app/src/main/java/wat/app/taskmanager/screens/about/AboutScreen.kt: -------------------------------------------------------------------------------- 1 | package wat.app.taskmanager.screens.about 2 | 3 | import androidx.compose.foundation.clickable 4 | import androidx.compose.runtime.Composable 5 | import androidx.compose.ui.Modifier 6 | import bakuen.lib.navigator.Navigator 7 | import bakuen.wear.components.SurfaceButton 8 | import bakuen.wear.components.Text 9 | import wat.app.taskmanager.BuildConfig 10 | import wat.app.taskmanager.screens.hides.HideAppListScreen 11 | import wat.app.taskmanager.utils.ScreenColumn 12 | 13 | @Composable 14 | fun AboutScreen() { 15 | ScreenColumn(center = true) { 16 | Text(text = "任务管理器 TaskManager") 17 | Text(text = "版本:${BuildConfig.VERSION_NAME} (${BuildConfig.VERSION_CODE})") 18 | Text(text = "By java30433") 19 | Text(text = "交流Q群: 1002991206") 20 | Text(text = "以 GPLv3 协议开源") 21 | SurfaceButton(modifier = Modifier.clickable { Navigator.forward { HideAppListScreen() } }) { 22 | Text(text = "所有被隐藏应用的列表") 23 | } 24 | } 25 | } -------------------------------------------------------------------------------- /app/src/main/java/wat/app/taskmanager/screens/detail/AppDetailScreen.kt: -------------------------------------------------------------------------------- 1 | package wat.app.taskmanager.screens.detail 2 | 3 | import androidx.compose.foundation.Image 4 | import androidx.compose.foundation.clickable 5 | import androidx.compose.foundation.layout.Column 6 | import androidx.compose.foundation.layout.fillMaxWidth 7 | import androidx.compose.runtime.Composable 8 | import androidx.compose.ui.Modifier 9 | import androidx.compose.ui.focus.focusModifier 10 | import bakuen.lib.navigator.Navigator 11 | import bakuen.lib.protostore.setStore 12 | import bakuen.wear.components.Space 13 | import bakuen.wear.components.SurfaceButton 14 | import bakuen.wear.components.Text 15 | import wat.app.taskmanager.prefs.Settings 16 | import wat.app.taskmanager.utils.Const 17 | import wat.app.taskmanager.utils.ScreenColumn 18 | import wat.app.taskmanager.screens.main.AppInfo 19 | 20 | @Composable 21 | fun AppDetailScreen(info: AppInfo) { 22 | ScreenColumn { 23 | Space(size = Const.scrPaddingTop) 24 | Image(bitmap = info.iconBitmap, contentDescription = null) 25 | Text(text = info.label) 26 | Column( 27 | modifier = Modifier.fillMaxWidth() 28 | ) { 29 | Text(text = "应用包名:${info.packageName}") 30 | } 31 | SurfaceButton(modifier = Modifier.clickable { 32 | setStore { it.copy(hidePackages = it.hidePackages.plus(info.packageName)) } 33 | Navigator.navigateBack() 34 | }) { 35 | Text(text = "在列表中隐藏此应用") 36 | } 37 | } 38 | } -------------------------------------------------------------------------------- /app/src/main/java/wat/app/taskmanager/screens/hides/HideAppListScreen.kt: -------------------------------------------------------------------------------- 1 | package wat.app.taskmanager.screens.hides 2 | 3 | import androidx.compose.runtime.Composable 4 | import androidx.compose.runtime.LaunchedEffect 5 | import androidx.compose.runtime.getValue 6 | import androidx.compose.runtime.mutableStateOf 7 | import androidx.compose.runtime.remember 8 | import androidx.compose.runtime.setValue 9 | import androidx.compose.ui.platform.LocalContext 10 | import androidx.compose.ui.unit.dp 11 | import bakuen.lib.navigator.SwipeToDismiss 12 | import bakuen.lib.protostore.getStore 13 | import bakuen.lib.protostore.rememberStore 14 | import bakuen.lib.protostore.setStore 15 | import bakuen.wear.components.Text 16 | import wat.app.taskmanager.prefs.Settings 17 | import wat.app.taskmanager.screens.main.AppCell 18 | import wat.app.taskmanager.screens.main.AppInfo 19 | import wat.app.taskmanager.utils.ScreenColumn 20 | import wat.app.taskmanager.utils.Title 21 | 22 | @Composable 23 | fun HideAppListScreen() { 24 | val pm = LocalContext.current.packageManager 25 | var settings by rememberStore() 26 | var state by remember { mutableStateOf(State()) } 27 | LaunchedEffect(settings.hidePackages.size) { 28 | state = state.copy(hideList = settings.hidePackages.map { 29 | val info = pm.getApplicationInfo(it, 0) 30 | AppInfo( 31 | packageName = it, 32 | icon = info.loadIcon(pm), 33 | label = info.loadLabel(pm).toString() 34 | ) 35 | }) 36 | } 37 | HideAppListScreenUI(state = state, dispatch = { 38 | when (it) { 39 | is Event.Remove -> settings = 40 | settings.copy(hidePackages = settings.hidePackages.minus(it.packageName)) 41 | } 42 | }) 43 | } 44 | 45 | private data class State( 46 | val hideList: List = listOf() 47 | ) 48 | 49 | private sealed class Event { 50 | class Remove(val packageName: String) : Event() 51 | } 52 | 53 | @Composable 54 | private fun HideAppListScreenUI(state: State, dispatch: (Event) -> Unit) { 55 | ScreenColumn { 56 | Title { 57 | Text(text = "被隐藏的应用") 58 | } 59 | state.hideList.let { 60 | if (it.isEmpty()) Text(text = "没有被隐藏的应用!") 61 | else it.forEach { 62 | SwipeToDismiss( 63 | velocity = 10.dp, 64 | reverse = true, 65 | onDismiss = { dispatch(Event.Remove(it.packageName)) } 66 | ) { 67 | AppCell(info = it) 68 | } 69 | } 70 | } 71 | } 72 | } -------------------------------------------------------------------------------- /app/src/main/java/wat/app/taskmanager/screens/main/AppCell.kt: -------------------------------------------------------------------------------- 1 | package wat.app.taskmanager.screens.main 2 | 3 | import androidx.compose.foundation.ExperimentalFoundationApi 4 | import androidx.compose.foundation.Image 5 | import androidx.compose.foundation.background 6 | import androidx.compose.foundation.combinedClickable 7 | import androidx.compose.foundation.layout.Arrangement 8 | import androidx.compose.foundation.layout.Row 9 | import androidx.compose.foundation.layout.fillMaxWidth 10 | import androidx.compose.foundation.layout.padding 11 | import androidx.compose.foundation.layout.size 12 | import androidx.compose.runtime.Composable 13 | import androidx.compose.ui.Alignment 14 | import androidx.compose.ui.Modifier 15 | import androidx.compose.ui.graphics.asImageBitmap 16 | import androidx.compose.ui.unit.dp 17 | import androidx.core.graphics.drawable.toBitmap 18 | import bakuen.wear.components.Shapes 19 | import bakuen.wear.components.Text 20 | import bakuen.wear.components.Theme 21 | 22 | 23 | @OptIn(ExperimentalFoundationApi::class) 24 | @Composable 25 | fun AppCell( 26 | info: AppInfo, 27 | onLongClick: (() -> Unit)? = null, 28 | onClick: () -> Unit = {} 29 | ) { 30 | Row( 31 | modifier = Modifier 32 | .fillMaxWidth() 33 | .background(color = Theme.color.surfaceContainer, shape = Shapes.Cell) 34 | .combinedClickable( 35 | onClick = onClick, 36 | onLongClick = onLongClick 37 | ) 38 | .padding(horizontal = 12.dp, vertical = 6.dp), 39 | verticalAlignment = Alignment.CenterVertically, 40 | horizontalArrangement = Arrangement.spacedBy(4.dp) 41 | ) { 42 | Image( 43 | modifier = Modifier.size(28.dp), 44 | bitmap = info.iconBitmap, 45 | contentDescription = null 46 | ) 47 | Text(text = info.label) 48 | } 49 | } -------------------------------------------------------------------------------- /app/src/main/java/wat/app/taskmanager/screens/main/AppInfo.kt: -------------------------------------------------------------------------------- 1 | package wat.app.taskmanager.screens.main 2 | 3 | import android.graphics.drawable.Drawable 4 | import androidx.compose.ui.graphics.asImageBitmap 5 | import androidx.core.graphics.drawable.toBitmap 6 | 7 | data class AppInfo( 8 | val packageName: String, 9 | val label: String, 10 | val icon: Drawable 11 | ) { 12 | val iconBitmap by lazy { icon.toBitmap().asImageBitmap() } 13 | } -------------------------------------------------------------------------------- /app/src/main/java/wat/app/taskmanager/screens/main/InfoText.kt: -------------------------------------------------------------------------------- 1 | package wat.app.taskmanager.screens.main 2 | 3 | import android.content.BroadcastReceiver 4 | import android.content.Context 5 | import android.content.Intent 6 | import android.content.IntentFilter 7 | import android.content.res.Resources 8 | import android.os.BatteryManager 9 | import androidx.compose.foundation.clickable 10 | import androidx.compose.runtime.Composable 11 | import androidx.compose.runtime.DisposableEffect 12 | import androidx.compose.runtime.getValue 13 | import androidx.compose.runtime.produceState 14 | import androidx.compose.ui.Modifier 15 | import androidx.compose.ui.platform.LocalContext 16 | import bakuen.wear.components.Text 17 | import kotlinx.coroutines.delay 18 | import java.text.SimpleDateFormat 19 | import java.util.Date 20 | import kotlin.math.roundToInt 21 | 22 | 23 | @Composable 24 | fun InfoText(onClick: ()->Unit = {}) { 25 | val time by produceState(initialValue = "") { 26 | do { 27 | value = 28 | SimpleDateFormat("HH:mm", Resources.getSystem().configuration.locales[0]).format( 29 | Date() 30 | ) 31 | delay(1_000) 32 | } while (true) 33 | } 34 | val context = LocalContext.current 35 | var batteryReceiver: BroadcastReceiver? = null //TODO 不优雅 36 | val battery by produceState(initialValue = 0) { 37 | batteryReceiver = object : BroadcastReceiver() { 38 | override fun onReceive(p0: Context?, intent: Intent) { 39 | val level = intent.getIntExtra(BatteryManager.EXTRA_LEVEL, -1) 40 | val scale = intent.getIntExtra(BatteryManager.EXTRA_SCALE, -1) 41 | value = (level * 100 / scale.toFloat()).roundToInt() 42 | } 43 | } 44 | context.registerReceiver(batteryReceiver, IntentFilter(Intent.ACTION_BATTERY_CHANGED)) 45 | } 46 | DisposableEffect(Unit) { 47 | onDispose { 48 | context.unregisterReceiver(batteryReceiver) 49 | } 50 | } 51 | Text(modifier = Modifier.clickable(onClick = onClick), text = "$time $battery%") 52 | } 53 | -------------------------------------------------------------------------------- /app/src/main/java/wat/app/taskmanager/screens/main/MainEvent.kt: -------------------------------------------------------------------------------- 1 | package wat.app.taskmanager.screens.main 2 | 3 | sealed class MainEvent { 4 | class Open(val packageName: String) : MainEvent() 5 | class Kill(val packageName: String) : MainEvent() 6 | } -------------------------------------------------------------------------------- /app/src/main/java/wat/app/taskmanager/screens/main/MainScreen.kt: -------------------------------------------------------------------------------- 1 | package wat.app.taskmanager.screens.main 2 | 3 | import androidx.compose.foundation.layout.fillMaxSize 4 | import androidx.compose.foundation.layout.wrapContentSize 5 | import androidx.compose.runtime.Composable 6 | import androidx.compose.runtime.LaunchedEffect 7 | import androidx.compose.runtime.key 8 | import androidx.compose.ui.Modifier 9 | import androidx.compose.ui.unit.dp 10 | import bakuen.lib.navigator.Navigator 11 | import bakuen.lib.protostore.rememberStore 12 | import bakuen.wear.components.Space 13 | import bakuen.wear.components.SwipeToDismiss 14 | import bakuen.wear.components.Text 15 | import wat.app.taskmanager.prefs.Settings 16 | import wat.app.taskmanager.utils.ScreenColumn 17 | import wat.app.taskmanager.shizuku.ShizukuState 18 | import wat.app.taskmanager.screens.about.AboutScreen 19 | import wat.app.taskmanager.screens.detail.AppDetailScreen 20 | import wat.app.taskmanager.utils.Title 21 | 22 | @Composable 23 | fun MainScreen(model: MainViewModel) { 24 | LaunchedEffect(rememberStore().value.hidePackages.size) { 25 | model.updateAppList() 26 | } 27 | MainScreenUI(state = model.state, dispatch = model::dispatch) 28 | } 29 | 30 | @Composable 31 | private fun MainScreenUI(state: MainState, dispatch: (MainEvent)->Unit) { 32 | if (state.shizuku == ShizukuState.GRANTED) { 33 | ScreenColumn { 34 | Title { 35 | InfoText(onClick = { 36 | Navigator.forward { AboutScreen() } 37 | }) 38 | } 39 | if (state.runningAppProgress.isEmpty()) { 40 | Text(text = "当前没有运行中的应用") 41 | } else state.runningAppProgress.forEach { 42 | key(it.packageName) { 43 | SwipeToDismiss( 44 | velocity = 16.dp, 45 | reverse = true, 46 | onDismiss = { dispatch(MainEvent.Kill(it.packageName)) }) { 47 | AppCell( 48 | info = it, 49 | onClick = { dispatch(MainEvent.Open(it.packageName)) }, 50 | onLongClick = { Navigator.forward { AppDetailScreen(info = it) } } 51 | ) 52 | } 53 | } 54 | } 55 | } 56 | } else { 57 | Text( 58 | modifier = Modifier 59 | .fillMaxSize() 60 | .wrapContentSize(), 61 | text = if (state.shizuku == ShizukuState.NOT_GRANTED) "请授予 Shizuku 权限!" 62 | else "Shizuku 服务不可用!" 63 | ) 64 | } 65 | } -------------------------------------------------------------------------------- /app/src/main/java/wat/app/taskmanager/screens/main/MainState.kt: -------------------------------------------------------------------------------- 1 | package wat.app.taskmanager.screens.main 2 | 3 | import wat.app.taskmanager.shizuku.ShizukuState 4 | 5 | data class MainState( 6 | val shizuku: ShizukuState, 7 | val runningAppProgress: List = listOf() 8 | ) -------------------------------------------------------------------------------- /app/src/main/java/wat/app/taskmanager/screens/main/MainViewModel.kt: -------------------------------------------------------------------------------- 1 | package wat.app.taskmanager.screens.main 2 | 3 | import androidx.compose.runtime.getValue 4 | import androidx.compose.runtime.mutableStateOf 5 | import androidx.compose.runtime.setValue 6 | import bakuen.lib.protostore.getStore 7 | import bakuen.wear.components.SimpleViewModel 8 | import wat.app.taskmanager.utils.Const 9 | import wat.app.taskmanager.utils.PackageUtils.killApp 10 | import wat.app.taskmanager.utils.PackageUtils.launchApp 11 | import wat.app.taskmanager.shizuku.ShizukuAPI 12 | import wat.app.taskmanager.shizuku.ShizukuTools 13 | import wat.app.taskmanager.appContext 14 | import wat.app.taskmanager.prefs.Settings 15 | 16 | class MainViewModel : SimpleViewModel() { 17 | 18 | var state by mutableStateOf(MainState(shizuku = ShizukuTools.state)) 19 | private set 20 | 21 | fun updateAppList() { 22 | ShizukuTools.requireBinder { 23 | val pm = appContext.packageManager 24 | state = state.copy( 25 | runningAppProgress = ShizukuAPI.ACTIVITY_MANAGER.runningAppProcesses 26 | .filter { 27 | !(Const.ignoreApps.contains(it.processName) || 28 | killedApps.contains(it.processName) || 29 | getStore().hidePackages.contains(it.processName)) 30 | } 31 | .mapNotNull { 32 | runCatching { 33 | val info = pm.getApplicationInfo(it.processName, 0) 34 | AppInfo( 35 | packageName = it.processName, 36 | icon = info.loadIcon(pm), 37 | label = info.loadLabel(pm).toString() 38 | ) 39 | }.getOrNull() 40 | } 41 | ) 42 | } 43 | } 44 | 45 | private val killedApps = mutableSetOf() 46 | override fun dispatch(event: MainEvent) { 47 | when (event) { 48 | is MainEvent.Open -> appContext.launchApp(event.packageName) 49 | is MainEvent.Kill -> { 50 | appContext.killApp(event.packageName) 51 | killedApps.add(event.packageName) 52 | updateAppList() 53 | } 54 | } 55 | } 56 | } -------------------------------------------------------------------------------- /app/src/main/java/wat/app/taskmanager/shizuku/ShizukuAPI.kt: -------------------------------------------------------------------------------- 1 | package wat.app.taskmanager.shizuku 2 | 3 | import android.app.IActivityManager 4 | import android.content.Context 5 | import rikka.shizuku.ShizukuBinderWrapper 6 | import rikka.shizuku.SystemServiceHelper 7 | 8 | object ShizukuAPI { 9 | val ACTIVITY_MANAGER: IActivityManager by lazy { 10 | IActivityManager.Stub.asInterface(ShizukuBinderWrapper( 11 | SystemServiceHelper.getSystemService(Context.ACTIVITY_SERVICE) 12 | )) 13 | } 14 | } -------------------------------------------------------------------------------- /app/src/main/java/wat/app/taskmanager/shizuku/ShizukuState.kt: -------------------------------------------------------------------------------- 1 | package wat.app.taskmanager.shizuku 2 | 3 | enum class ShizukuState { 4 | GRANTED, NOT_GRANTED, UNAVAILABLE 5 | } -------------------------------------------------------------------------------- /app/src/main/java/wat/app/taskmanager/shizuku/ShizukuTools.kt: -------------------------------------------------------------------------------- 1 | package wat.app.taskmanager.shizuku 2 | 3 | import android.content.pm.PackageManager 4 | import androidx.compose.runtime.getValue 5 | import androidx.compose.runtime.mutableStateOf 6 | import androidx.compose.runtime.setValue 7 | import rikka.shizuku.Shizuku 8 | 9 | object ShizukuTools { 10 | private fun getShizukuState() = if (running) if (granted) ShizukuState.GRANTED else ShizukuState.NOT_GRANTED 11 | else ShizukuState.UNAVAILABLE 12 | var state by mutableStateOf(getShizukuState()) 13 | private set 14 | init { 15 | Shizuku.addBinderReceivedListener { 16 | state = getShizukuState() 17 | } 18 | Shizuku.addBinderDeadListener { 19 | state = getShizukuState() 20 | } 21 | } 22 | val running get() = Shizuku.pingBinder() 23 | 24 | fun requireBinder(block: ()->Unit) { 25 | if (running && granted) block() 26 | else Shizuku.addBinderReceivedListener(block) 27 | } 28 | 29 | val granted get() = running && Shizuku.checkSelfPermission() == PackageManager.PERMISSION_GRANTED 30 | fun requestPermission(callback: () -> Unit = {}) { 31 | state = getShizukuState() 32 | if (!running) return 33 | if (!granted) { 34 | Shizuku.addRequestPermissionResultListener { _, _ -> 35 | callback() 36 | } 37 | Shizuku.requestPermission(30433) 38 | } else callback() 39 | } 40 | } -------------------------------------------------------------------------------- /app/src/main/java/wat/app/taskmanager/utils/Composables.kt: -------------------------------------------------------------------------------- 1 | package wat.app.taskmanager.utils 2 | 3 | import androidx.compose.foundation.background 4 | import androidx.compose.foundation.layout.Column 5 | import androidx.compose.foundation.layout.ColumnScope 6 | import androidx.compose.foundation.layout.fillMaxSize 7 | import androidx.compose.foundation.layout.padding 8 | import androidx.compose.foundation.layout.wrapContentSize 9 | import androidx.compose.foundation.rememberScrollState 10 | import androidx.compose.runtime.Composable 11 | import androidx.compose.ui.Alignment 12 | import androidx.compose.ui.Modifier 13 | import androidx.compose.ui.graphics.Color 14 | import androidx.compose.ui.unit.dp 15 | import bakuen.lib.navigator.Navigator 16 | import bakuen.wear.components.Space 17 | import bakuen.wear.components.calculate 18 | import bakuen.wear.components.condition 19 | import bakuen.wear.components.wear.verticalRotaryScroll 20 | import wat.app.taskmanager.screens.about.AboutScreen 21 | import wat.app.taskmanager.screens.main.InfoText 22 | 23 | @Composable 24 | inline fun ScreenColumn(center: Boolean = false, bg: Color? = null, content: @Composable ColumnScope.()->Unit) { 25 | Column( 26 | modifier = Modifier 27 | .fillMaxSize() 28 | .calculate { if (bg!=null) Modifier.background(color = bg) else null } 29 | .padding(horizontal = Const.scrPaddingHor) 30 | .condition(center, Modifier.wrapContentSize(Alignment.Center)) 31 | .verticalRotaryScroll(rememberScrollState()), 32 | horizontalAlignment = Alignment.CenterHorizontally, 33 | verticalArrangement = Const.colArrangement, 34 | content = content 35 | ) 36 | } 37 | 38 | @Composable 39 | inline fun ColumnScope.Title(content: @Composable ColumnScope.()->Unit) { 40 | Space(size = Const.scrPaddingTop) 41 | content() 42 | Space(size = 8.dp) 43 | } -------------------------------------------------------------------------------- /app/src/main/java/wat/app/taskmanager/utils/Const.kt: -------------------------------------------------------------------------------- 1 | package wat.app.taskmanager.utils 2 | 3 | import androidx.compose.foundation.background 4 | import androidx.compose.foundation.layout.Arrangement 5 | import androidx.compose.foundation.layout.Column 6 | import androidx.compose.foundation.layout.ColumnScope 7 | import androidx.compose.foundation.layout.fillMaxSize 8 | import androidx.compose.foundation.layout.padding 9 | import androidx.compose.foundation.layout.wrapContentSize 10 | import androidx.compose.foundation.rememberScrollState 11 | import androidx.compose.runtime.Composable 12 | import androidx.compose.ui.Alignment 13 | import androidx.compose.ui.Modifier 14 | import androidx.compose.ui.graphics.Color 15 | import androidx.compose.ui.unit.dp 16 | import bakuen.wear.components.Space 17 | import bakuen.wear.components.calculate 18 | import bakuen.wear.components.condition 19 | import bakuen.wear.components.wear.verticalRotaryScroll 20 | import wat.app.taskmanager.BuildConfig 21 | 22 | object Const { 23 | val ignoreApps = setOf( 24 | BuildConfig.APPLICATION_ID, 25 | "moe.shizuku.privileged.api", 26 | "com.google.android.wearable.app", 27 | "com.google.android.gms", 28 | "com.android.connectivity.metrics", 29 | "com.android.se", 30 | "com.android.phone", 31 | "com.android.providers.calendar", 32 | "com.google.android.ext.services", 33 | "com.google.android.deskclock", 34 | "con.google.android.apps.handwriting.ime", 35 | "system" 36 | ) 37 | val scrPaddingHor = 9.dp 38 | val scrPaddingTop = 10.dp 39 | val colArrangement = Arrangement.spacedBy(2.dp) 40 | } -------------------------------------------------------------------------------- /app/src/main/java/wat/app/taskmanager/utils/PackageUtils.kt: -------------------------------------------------------------------------------- 1 | package wat.app.taskmanager.utils 2 | 3 | import android.annotation.SuppressLint 4 | import android.content.ComponentName 5 | import android.content.Context 6 | import android.content.Intent 7 | import android.content.pm.PackageManager 8 | import android.text.TextUtils 9 | import wat.app.taskmanager.shizuku.ShizukuAPI 10 | 11 | 12 | object PackageUtils { 13 | fun Context.killApp(packageName: String) { 14 | ShizukuAPI.ACTIVITY_MANAGER.forceStopPackage(packageName, 0) 15 | } 16 | 17 | fun Context.launchApp(packageName: String) { 18 | getAppOpenIntentByPackageName(this, packageName)?.let { 19 | startActivity(it) 20 | } 21 | } 22 | @SuppressLint("QueryPermissionsNeeded") 23 | fun getAppOpenIntentByPackageName(context: Context, packageName: String): Intent? { 24 | var mainAct: String? = null 25 | val pkgMag = context.packageManager 26 | val intent = Intent(Intent.ACTION_MAIN) 27 | intent.addCategory(Intent.CATEGORY_LAUNCHER) 28 | intent.setFlags(Intent.FLAG_ACTIVITY_RESET_TASK_IF_NEEDED or Intent.FLAG_ACTIVITY_NEW_TASK) 29 | val list = pkgMag.queryIntentActivities(intent, PackageManager.GET_ACTIVITIES) 30 | for (i in list.indices) { 31 | val info = list[i] 32 | if (info.activityInfo.packageName == packageName) { 33 | mainAct = info.activityInfo.name 34 | break 35 | } 36 | } 37 | if (TextUtils.isEmpty(mainAct)) { 38 | return null 39 | } 40 | intent.setComponent(ComponentName(packageName, mainAct!!)) 41 | return intent 42 | } 43 | } -------------------------------------------------------------------------------- /app/src/main/java/wat/app/taskmanager/utils/Update.kt: -------------------------------------------------------------------------------- 1 | package wat.app.taskmanager.utils 2 | 3 | import android.app.Activity 4 | import android.os.Handler 5 | import android.os.Looper 6 | import android.widget.Toast 7 | import wat.app.taskmanager.BuildConfig 8 | import java.io.BufferedReader 9 | import java.io.InputStreamReader 10 | import java.net.HttpURLConnection 11 | import java.net.URL 12 | 13 | object Update { 14 | fun check(activity: Activity) { 15 | Thread { 16 | try { 17 | val connection = URL("https://pastebin.com/raw/FjTceQ7F") 18 | .openConnection() as HttpURLConnection 19 | connection.requestMethod = "GET" 20 | if (connection.responseCode == HttpURLConnection.HTTP_OK) { 21 | val reader = 22 | BufferedReader(InputStreamReader(connection.inputStream)) 23 | val response = StringBuilder() 24 | var line: String? 25 | while ((reader.readLine().also { line = it }) != null) { 26 | response.append(line) 27 | } 28 | reader.close() 29 | val info = response.toString().split("@") 30 | val newVersion = info[0].toInt() 31 | if (newVersion > BuildConfig.VERSION_CODE) { 32 | Handler(Looper.getMainLooper()).post { 33 | Toast.makeText( 34 | activity, 35 | info[1].replace("\\n", "\n"), 36 | Toast.LENGTH_LONG 37 | ).show() 38 | } 39 | } 40 | } 41 | connection.disconnect() 42 | } catch (e: Exception) { 43 | e.printStackTrace() 44 | } 45 | }.start() 46 | } 47 | } -------------------------------------------------------------------------------- /app/src/main/res/drawable/ic_launcher_foreground.xml: -------------------------------------------------------------------------------- 1 | 7 | 11 | 14 | 15 | 16 | -------------------------------------------------------------------------------- /app/src/main/res/mipmap-anydpi-v26/ic_launcher.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | -------------------------------------------------------------------------------- /app/src/main/res/mipmap-anydpi-v26/ic_launcher_round.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | -------------------------------------------------------------------------------- /app/src/main/res/mipmap-hdpi/ic_launcher.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/java30433/TaskManager/1bca47ad3cba310723929a231a5e7cb4d4dc9434/app/src/main/res/mipmap-hdpi/ic_launcher.webp -------------------------------------------------------------------------------- /app/src/main/res/mipmap-hdpi/ic_launcher_round.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/java30433/TaskManager/1bca47ad3cba310723929a231a5e7cb4d4dc9434/app/src/main/res/mipmap-hdpi/ic_launcher_round.webp -------------------------------------------------------------------------------- /app/src/main/res/mipmap-mdpi/ic_launcher.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/java30433/TaskManager/1bca47ad3cba310723929a231a5e7cb4d4dc9434/app/src/main/res/mipmap-mdpi/ic_launcher.webp -------------------------------------------------------------------------------- /app/src/main/res/mipmap-mdpi/ic_launcher_round.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/java30433/TaskManager/1bca47ad3cba310723929a231a5e7cb4d4dc9434/app/src/main/res/mipmap-mdpi/ic_launcher_round.webp -------------------------------------------------------------------------------- /app/src/main/res/mipmap-xhdpi/ic_launcher.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/java30433/TaskManager/1bca47ad3cba310723929a231a5e7cb4d4dc9434/app/src/main/res/mipmap-xhdpi/ic_launcher.webp -------------------------------------------------------------------------------- /app/src/main/res/mipmap-xhdpi/ic_launcher_round.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/java30433/TaskManager/1bca47ad3cba310723929a231a5e7cb4d4dc9434/app/src/main/res/mipmap-xhdpi/ic_launcher_round.webp -------------------------------------------------------------------------------- /app/src/main/res/mipmap-xxhdpi/ic_launcher.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/java30433/TaskManager/1bca47ad3cba310723929a231a5e7cb4d4dc9434/app/src/main/res/mipmap-xxhdpi/ic_launcher.webp -------------------------------------------------------------------------------- /app/src/main/res/mipmap-xxhdpi/ic_launcher_round.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/java30433/TaskManager/1bca47ad3cba310723929a231a5e7cb4d4dc9434/app/src/main/res/mipmap-xxhdpi/ic_launcher_round.webp -------------------------------------------------------------------------------- /app/src/main/res/mipmap-xxxhdpi/ic_launcher.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/java30433/TaskManager/1bca47ad3cba310723929a231a5e7cb4d4dc9434/app/src/main/res/mipmap-xxxhdpi/ic_launcher.webp -------------------------------------------------------------------------------- /app/src/main/res/mipmap-xxxhdpi/ic_launcher_round.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/java30433/TaskManager/1bca47ad3cba310723929a231a5e7cb4d4dc9434/app/src/main/res/mipmap-xxxhdpi/ic_launcher_round.webp -------------------------------------------------------------------------------- /app/src/main/res/values/ic_launcher_background.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | #3DDCB7 4 | -------------------------------------------------------------------------------- /app/src/main/res/values/theme.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 6 | -------------------------------------------------------------------------------- /app/src/test/java/wat/app/taskmanager/ExampleUnitTest.kt: -------------------------------------------------------------------------------- 1 | package wat.app.taskmanager 2 | 3 | import org.junit.Test 4 | 5 | import org.junit.Assert.* 6 | 7 | /** 8 | * Example local unit test, which will execute on the development machine (host). 9 | * 10 | * See [testing documentation](http://d.android.com/tools/testing). 11 | */ 12 | class ExampleUnitTest { 13 | @Test 14 | fun addition_isCorrect() { 15 | assertEquals(4, 2 + 2) 16 | } 17 | } -------------------------------------------------------------------------------- /build.gradle.kts: -------------------------------------------------------------------------------- 1 | // Top-level build file where you can add configuration options common to all sub-projects/modules. 2 | plugins { 3 | alias(libs.plugins.android.application) apply false 4 | alias(libs.plugins.android.library) apply false 5 | alias(libs.plugins.jetbrains.kotlin.android) apply false 6 | alias(libs.plugins.compose.compiler) apply false 7 | alias(libs.plugins.kotlinx.serialization) apply false 8 | } -------------------------------------------------------------------------------- /gradle.properties: -------------------------------------------------------------------------------- 1 | # Project-wide Gradle settings. 2 | # IDE (e.g. Android Studio) users: 3 | # Gradle settings configured through the IDE *will override* 4 | # any settings specified in this file. 5 | # For more details on how to configure your build environment visit 6 | # http://www.gradle.org/docs/current/userguide/build_environment.html 7 | # Specifies the JVM arguments used for the daemon process. 8 | # The setting is particularly useful for tweaking memory settings. 9 | org.gradle.jvmargs=-Xmx2048m -Dfile.encoding=UTF-8 10 | # When configured, Gradle will run in incubating parallel mode. 11 | # This option should only be used with decoupled projects. For more details, visit 12 | # https://developer.android.com/r/tools/gradle-multi-project-decoupled-projects 13 | # org.gradle.parallel=true 14 | # AndroidX package structure to make it clearer which packages are bundled with the 15 | # Android operating system, and which are packaged with your app's APK 16 | # https://developer.android.com/topic/libraries/support-library/androidx-rn 17 | android.useAndroidX=true 18 | # Kotlin code style for this project: "official" or "obsolete": 19 | kotlin.code.style=official 20 | # Enables namespacing of each library's R class so that its R class includes only the 21 | # resources declared in the library itself and none from the library's dependencies, 22 | # thereby reducing the size of the R class for that library 23 | android.nonTransitiveRClass=true -------------------------------------------------------------------------------- /gradle/libs.versions.toml: -------------------------------------------------------------------------------- 1 | [versions] 2 | agp = "8.5.1" 3 | shizuku-api = "13.1.5" 4 | kotlin = "2.0.0" 5 | activityCompose = "1.9.1" 6 | composeBom = "2024.08.00" 7 | m3color = "2024.5" 8 | wearComposeFoundation = "1.4.0-rc01" 9 | kotlinxCoroutines = "1.9.0-RC.2" 10 | kotlinxSerialization = "1.6.3" 11 | 12 | [libraries] 13 | m3color = { module = "com.github.Kyant0:m3color", version.ref = "m3color" } 14 | androidx-wear-compose-foundation = { module = "androidx.wear.compose:compose-foundation", version.ref = "wearComposeFoundation" } 15 | androidx-compose-animation = { group = "androidx.compose.animation", name = "animation" } 16 | androidx-activity-compose = { group = "androidx.activity", name = "activity-compose", version.ref = "activityCompose" } 17 | androidx-compose-bom = { group = "androidx.compose", name = "compose-bom", version.ref = "composeBom" } 18 | androidx-compose-ui = { group = "androidx.compose.ui", name = "ui" } 19 | androidx-compose-foundation = { group = "androidx.compose.foundation", name = "foundation", version = "1.7.0-rc01" } 20 | androidx-compose-ui-graphics = { group = "androidx.compose.ui", name = "ui-graphics" } 21 | shizuku-api = { module = "dev.rikka.shizuku:api", version.ref = "shizuku-api" } 22 | shizuku-provider = { module = "dev.rikka.shizuku:provider", version.ref = "shizuku-api" } 23 | kotlinx-serialization-protobuf = { module = "org.jetbrains.kotlinx:kotlinx-serialization-protobuf", version.ref = "kotlinxSerialization" } 24 | kotlinx-coroutines-core = { module = "org.jetbrains.kotlinx:kotlinx-coroutines-core", version.ref = "kotlinxCoroutines" } 25 | 26 | [plugins] 27 | android-library = { id = "com.android.library", version.ref = "agp" } 28 | android-application = { id = "com.android.application", version.ref = "agp" } 29 | jetbrains-kotlin-android = { id = "org.jetbrains.kotlin.android", version.ref = "kotlin" } 30 | compose-compiler = { id = "org.jetbrains.kotlin.plugin.compose", version.ref = "kotlin" } 31 | kotlinx-serialization = { id = "org.jetbrains.kotlin.plugin.serialization", version.ref = "kotlin" } 32 | 33 | -------------------------------------------------------------------------------- /gradle/wrapper/gradle-wrapper.jar: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/java30433/TaskManager/1bca47ad3cba310723929a231a5e7cb4d4dc9434/gradle/wrapper/gradle-wrapper.jar -------------------------------------------------------------------------------- /gradle/wrapper/gradle-wrapper.properties: -------------------------------------------------------------------------------- 1 | #Sun Aug 25 21:39:07 CST 2024 2 | distributionBase=GRADLE_USER_HOME 3 | distributionPath=wrapper/dists 4 | distributionUrl=https\://services.gradle.org/distributions/gradle-8.7-bin.zip 5 | zipStoreBase=GRADLE_USER_HOME 6 | zipStorePath=wrapper/dists 7 | -------------------------------------------------------------------------------- /gradlew: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env sh 2 | 3 | # 4 | # Copyright 2015 the original author or authors. 5 | # 6 | # Licensed under the Apache License, Version 2.0 (the "License"); 7 | # you may not use this file except in compliance with the License. 8 | # You may obtain a copy of the License at 9 | # 10 | # https://www.apache.org/licenses/LICENSE-2.0 11 | # 12 | # Unless required by applicable law or agreed to in writing, software 13 | # distributed under the License is distributed on an "AS IS" BASIS, 14 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 15 | # See the License for the specific language governing permissions and 16 | # limitations under the License. 17 | # 18 | 19 | ############################################################################## 20 | ## 21 | ## Gradle start up script for UN*X 22 | ## 23 | ############################################################################## 24 | 25 | # Attempt to set APP_HOME 26 | # Resolve links: $0 may be a link 27 | PRG="$0" 28 | # Need this for relative symlinks. 29 | while [ -h "$PRG" ] ; do 30 | ls=`ls -ld "$PRG"` 31 | link=`expr "$ls" : '.*-> \(.*\)$'` 32 | if expr "$link" : '/.*' > /dev/null; then 33 | PRG="$link" 34 | else 35 | PRG=`dirname "$PRG"`"/$link" 36 | fi 37 | done 38 | SAVED="`pwd`" 39 | cd "`dirname \"$PRG\"`/" >/dev/null 40 | APP_HOME="`pwd -P`" 41 | cd "$SAVED" >/dev/null 42 | 43 | APP_NAME="Gradle" 44 | APP_BASE_NAME=`basename "$0"` 45 | 46 | # Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. 47 | DEFAULT_JVM_OPTS='"-Xmx64m" "-Xms64m"' 48 | 49 | # Use the maximum available, or set MAX_FD != -1 to use that value. 50 | MAX_FD="maximum" 51 | 52 | warn () { 53 | echo "$*" 54 | } 55 | 56 | die () { 57 | echo 58 | echo "$*" 59 | echo 60 | exit 1 61 | } 62 | 63 | # OS specific support (must be 'true' or 'false'). 64 | cygwin=false 65 | msys=false 66 | darwin=false 67 | nonstop=false 68 | case "`uname`" in 69 | CYGWIN* ) 70 | cygwin=true 71 | ;; 72 | Darwin* ) 73 | darwin=true 74 | ;; 75 | MINGW* ) 76 | msys=true 77 | ;; 78 | NONSTOP* ) 79 | nonstop=true 80 | ;; 81 | esac 82 | 83 | CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar 84 | 85 | 86 | # Determine the Java command to use to start the JVM. 87 | if [ -n "$JAVA_HOME" ] ; then 88 | if [ -x "$JAVA_HOME/jre/sh/java" ] ; then 89 | # IBM's JDK on AIX uses strange locations for the executables 90 | JAVACMD="$JAVA_HOME/jre/sh/java" 91 | else 92 | JAVACMD="$JAVA_HOME/bin/java" 93 | fi 94 | if [ ! -x "$JAVACMD" ] ; then 95 | die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME 96 | 97 | Please set the JAVA_HOME variable in your environment to match the 98 | location of your Java installation." 99 | fi 100 | else 101 | JAVACMD="java" 102 | which java >/dev/null 2>&1 || die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. 103 | 104 | Please set the JAVA_HOME variable in your environment to match the 105 | location of your Java installation." 106 | fi 107 | 108 | # Increase the maximum file descriptors if we can. 109 | if [ "$cygwin" = "false" -a "$darwin" = "false" -a "$nonstop" = "false" ] ; then 110 | MAX_FD_LIMIT=`ulimit -H -n` 111 | if [ $? -eq 0 ] ; then 112 | if [ "$MAX_FD" = "maximum" -o "$MAX_FD" = "max" ] ; then 113 | MAX_FD="$MAX_FD_LIMIT" 114 | fi 115 | ulimit -n $MAX_FD 116 | if [ $? -ne 0 ] ; then 117 | warn "Could not set maximum file descriptor limit: $MAX_FD" 118 | fi 119 | else 120 | warn "Could not query maximum file descriptor limit: $MAX_FD_LIMIT" 121 | fi 122 | fi 123 | 124 | # For Darwin, add options to specify how the application appears in the dock 125 | if $darwin; then 126 | GRADLE_OPTS="$GRADLE_OPTS \"-Xdock:name=$APP_NAME\" \"-Xdock:icon=$APP_HOME/media/gradle.icns\"" 127 | fi 128 | 129 | # For Cygwin or MSYS, switch paths to Windows format before running java 130 | if [ "$cygwin" = "true" -o "$msys" = "true" ] ; then 131 | APP_HOME=`cygpath --path --mixed "$APP_HOME"` 132 | CLASSPATH=`cygpath --path --mixed "$CLASSPATH"` 133 | 134 | JAVACMD=`cygpath --unix "$JAVACMD"` 135 | 136 | # We build the pattern for arguments to be converted via cygpath 137 | ROOTDIRSRAW=`find -L / -maxdepth 1 -mindepth 1 -type d 2>/dev/null` 138 | SEP="" 139 | for dir in $ROOTDIRSRAW ; do 140 | ROOTDIRS="$ROOTDIRS$SEP$dir" 141 | SEP="|" 142 | done 143 | OURCYGPATTERN="(^($ROOTDIRS))" 144 | # Add a user-defined pattern to the cygpath arguments 145 | if [ "$GRADLE_CYGPATTERN" != "" ] ; then 146 | OURCYGPATTERN="$OURCYGPATTERN|($GRADLE_CYGPATTERN)" 147 | fi 148 | # Now convert the arguments - kludge to limit ourselves to /bin/sh 149 | i=0 150 | for arg in "$@" ; do 151 | CHECK=`echo "$arg"|egrep -c "$OURCYGPATTERN" -` 152 | CHECK2=`echo "$arg"|egrep -c "^-"` ### Determine if an option 153 | 154 | if [ $CHECK -ne 0 ] && [ $CHECK2 -eq 0 ] ; then ### Added a condition 155 | eval `echo args$i`=`cygpath --path --ignore --mixed "$arg"` 156 | else 157 | eval `echo args$i`="\"$arg\"" 158 | fi 159 | i=`expr $i + 1` 160 | done 161 | case $i in 162 | 0) set -- ;; 163 | 1) set -- "$args0" ;; 164 | 2) set -- "$args0" "$args1" ;; 165 | 3) set -- "$args0" "$args1" "$args2" ;; 166 | 4) set -- "$args0" "$args1" "$args2" "$args3" ;; 167 | 5) set -- "$args0" "$args1" "$args2" "$args3" "$args4" ;; 168 | 6) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" ;; 169 | 7) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" ;; 170 | 8) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" ;; 171 | 9) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" "$args8" ;; 172 | esac 173 | fi 174 | 175 | # Escape application args 176 | save () { 177 | for i do printf %s\\n "$i" | sed "s/'/'\\\\''/g;1s/^/'/;\$s/\$/' \\\\/" ; done 178 | echo " " 179 | } 180 | APP_ARGS=`save "$@"` 181 | 182 | # Collect all arguments for the java command, following the shell quoting and substitution rules 183 | eval set -- $DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS "\"-Dorg.gradle.appname=$APP_BASE_NAME\"" -classpath "\"$CLASSPATH\"" org.gradle.wrapper.GradleWrapperMain "$APP_ARGS" 184 | 185 | exec "$JAVACMD" "$@" 186 | -------------------------------------------------------------------------------- /gradlew.bat: -------------------------------------------------------------------------------- 1 | @rem 2 | @rem Copyright 2015 the original author or authors. 3 | @rem 4 | @rem Licensed under the Apache License, Version 2.0 (the "License"); 5 | @rem you may not use this file except in compliance with the License. 6 | @rem You may obtain a copy of the License at 7 | @rem 8 | @rem https://www.apache.org/licenses/LICENSE-2.0 9 | @rem 10 | @rem Unless required by applicable law or agreed to in writing, software 11 | @rem distributed under the License is distributed on an "AS IS" BASIS, 12 | @rem WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | @rem See the License for the specific language governing permissions and 14 | @rem limitations under the License. 15 | @rem 16 | 17 | @if "%DEBUG%" == "" @echo off 18 | @rem ########################################################################## 19 | @rem 20 | @rem Gradle startup script for Windows 21 | @rem 22 | @rem ########################################################################## 23 | 24 | @rem Set local scope for the variables with windows NT shell 25 | if "%OS%"=="Windows_NT" setlocal 26 | 27 | set DIRNAME=%~dp0 28 | if "%DIRNAME%" == "" set DIRNAME=. 29 | set APP_BASE_NAME=%~n0 30 | set APP_HOME=%DIRNAME% 31 | 32 | @rem Resolve any "." and ".." in APP_HOME to make it shorter. 33 | for %%i in ("%APP_HOME%") do set APP_HOME=%%~fi 34 | 35 | @rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. 36 | set DEFAULT_JVM_OPTS="-Xmx64m" "-Xms64m" 37 | 38 | @rem Find java.exe 39 | if defined JAVA_HOME goto findJavaFromJavaHome 40 | 41 | set JAVA_EXE=java.exe 42 | %JAVA_EXE% -version >NUL 2>&1 43 | if "%ERRORLEVEL%" == "0" goto execute 44 | 45 | echo. 46 | echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. 47 | echo. 48 | echo Please set the JAVA_HOME variable in your environment to match the 49 | echo location of your Java installation. 50 | 51 | goto fail 52 | 53 | :findJavaFromJavaHome 54 | set JAVA_HOME=%JAVA_HOME:"=% 55 | set JAVA_EXE=%JAVA_HOME%/bin/java.exe 56 | 57 | if exist "%JAVA_EXE%" goto execute 58 | 59 | echo. 60 | echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% 61 | echo. 62 | echo Please set the JAVA_HOME variable in your environment to match the 63 | echo location of your Java installation. 64 | 65 | goto fail 66 | 67 | :execute 68 | @rem Setup the command line 69 | 70 | set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar 71 | 72 | 73 | @rem Execute Gradle 74 | "%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %* 75 | 76 | :end 77 | @rem End local scope for the variables with windows NT shell 78 | if "%ERRORLEVEL%"=="0" goto mainEnd 79 | 80 | :fail 81 | rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of 82 | rem the _cmd.exe /c_ return code! 83 | if not "" == "%GRADLE_EXIT_CONSOLE%" exit 1 84 | exit /b 1 85 | 86 | :mainEnd 87 | if "%OS%"=="Windows_NT" endlocal 88 | 89 | :omega 90 | -------------------------------------------------------------------------------- /lib-components/.gitignore: -------------------------------------------------------------------------------- 1 | /build -------------------------------------------------------------------------------- /lib-components/build.gradle.kts: -------------------------------------------------------------------------------- 1 | plugins { 2 | alias(libs.plugins.android.library) 3 | alias(libs.plugins.jetbrains.kotlin.android) 4 | alias(libs.plugins.compose.compiler) 5 | } 6 | 7 | android { 8 | namespace = "bakuen.wear.components" 9 | compileSdk = 34 10 | 11 | defaultConfig { 12 | minSdk = 21 13 | consumerProguardFiles("consumer-rules.pro") 14 | } 15 | 16 | buildTypes { 17 | release { 18 | isMinifyEnabled = false 19 | proguardFiles( 20 | getDefaultProguardFile("proguard-android-optimize.txt"), 21 | "proguard-rules.pro" 22 | ) 23 | } 24 | } 25 | compileOptions { 26 | sourceCompatibility = JavaVersion.VERSION_1_8 27 | targetCompatibility = JavaVersion.VERSION_1_8 28 | } 29 | kotlinOptions { 30 | jvmTarget = "1.8" 31 | } 32 | } 33 | 34 | dependencies { 35 | implementation(libs.m3color) 36 | implementation(platform(libs.androidx.compose.bom)) 37 | implementation(libs.androidx.compose.ui) 38 | implementation(libs.androidx.compose.foundation) 39 | implementation(libs.androidx.compose.animation) 40 | 41 | implementation(libs.androidx.wear.compose.foundation) 42 | } -------------------------------------------------------------------------------- /lib-components/consumer-rules.pro: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/java30433/TaskManager/1bca47ad3cba310723929a231a5e7cb4d4dc9434/lib-components/consumer-rules.pro -------------------------------------------------------------------------------- /lib-components/proguard-rules.pro: -------------------------------------------------------------------------------- 1 | # Add project specific ProGuard rules here. 2 | # You can control the set of applied configuration files using the 3 | # proguardFiles setting in build.gradle. 4 | # 5 | # For more details, see 6 | # http://developer.android.com/guide/developing/tools/proguard.html 7 | 8 | # If your project uses WebView with JS, uncomment the following 9 | # and specify the fully qualified class name to the JavaScript interface 10 | # class: 11 | #-keepclassmembers class fqcn.of.javascript.interface.for.webview { 12 | # public *; 13 | #} 14 | 15 | # Uncomment this to preserve the line number information for 16 | # debugging stack traces. 17 | #-keepattributes SourceFile,LineNumberTable 18 | 19 | # If you keep the line number information, uncomment this to 20 | # hide the original source file name. 21 | #-renamesourcefileattribute SourceFile -------------------------------------------------------------------------------- /lib-components/src/main/AndroidManifest.xml: -------------------------------------------------------------------------------- 1 | 2 | 4 | 5 | -------------------------------------------------------------------------------- /lib-components/src/main/java/bakuen/wear/components/AutoSize.kt: -------------------------------------------------------------------------------- 1 | package bakuen.wear.components 2 | 3 | import androidx.compose.runtime.Composable 4 | import androidx.compose.runtime.CompositionLocalProvider 5 | import androidx.compose.ui.platform.LocalContext 6 | import androidx.compose.ui.platform.LocalDensity 7 | import androidx.compose.ui.unit.Density 8 | 9 | @Composable 10 | fun AutoSize(designWidth: Float, content: @Composable ()->Unit) { 11 | val fontScale = LocalDensity.current.fontScale 12 | val displayMetrics = LocalContext.current.resources.displayMetrics 13 | val widthPixels = displayMetrics.widthPixels 14 | CompositionLocalProvider( 15 | LocalDensity provides Density( 16 | density = widthPixels / designWidth, 17 | fontScale = fontScale 18 | ), 19 | content = content 20 | ) 21 | } -------------------------------------------------------------------------------- /lib-components/src/main/java/bakuen/wear/components/Background.kt: -------------------------------------------------------------------------------- 1 | package bakuen.wear.components 2 | 3 | import androidx.compose.foundation.background 4 | import androidx.compose.foundation.layout.Box 5 | import androidx.compose.foundation.layout.BoxScope 6 | import androidx.compose.foundation.layout.fillMaxSize 7 | import androidx.compose.runtime.Composable 8 | import androidx.compose.ui.Alignment 9 | import androidx.compose.ui.Modifier 10 | import androidx.compose.ui.graphics.Color 11 | 12 | @Composable 13 | inline fun Surface( 14 | color: Color = Theme.color.surface, 15 | fillMaxSize: Boolean = false, 16 | contentAlignment: Alignment = Alignment.TopStart, 17 | content: @Composable BoxScope.() -> Unit 18 | ) { 19 | Box( 20 | modifier = Modifier 21 | .background(color = color) 22 | .cleanClick() 23 | .condition(fillMaxSize, Modifier.fillMaxSize()) 24 | , content = content, contentAlignment = contentAlignment 25 | ) 26 | } -------------------------------------------------------------------------------- /lib-components/src/main/java/bakuen/wear/components/Button.kt: -------------------------------------------------------------------------------- 1 | package bakuen.wear.components 2 | 3 | import androidx.compose.foundation.ExperimentalFoundationApi 4 | import androidx.compose.foundation.background 5 | import androidx.compose.foundation.clickable 6 | import androidx.compose.foundation.layout.Arrangement 7 | import androidx.compose.foundation.layout.Box 8 | import androidx.compose.foundation.layout.PaddingValues 9 | import androidx.compose.foundation.layout.Row 10 | import androidx.compose.foundation.layout.RowScope 11 | import androidx.compose.foundation.layout.fillMaxWidth 12 | import androidx.compose.foundation.layout.padding 13 | import androidx.compose.foundation.shape.CutCornerShape 14 | import androidx.compose.foundation.shape.RoundedCornerShape 15 | import androidx.compose.runtime.Composable 16 | import androidx.compose.runtime.CompositionLocalProvider 17 | import androidx.compose.ui.Alignment 18 | import androidx.compose.ui.Modifier 19 | import androidx.compose.ui.graphics.Color 20 | import androidx.compose.ui.text.TextStyle 21 | import androidx.compose.ui.unit.dp 22 | 23 | @Composable 24 | fun SurfaceButton( 25 | modifier: Modifier = Modifier, 26 | content: @Composable RowScope.() -> Unit, 27 | ) { 28 | FilledButton( 29 | modifier = modifier, 30 | content = content, 31 | color = Theme.color.surfaceContainer, 32 | textColor = Theme.color.onSurface 33 | ) 34 | } 35 | 36 | @Composable 37 | fun TextButton( 38 | modifier: Modifier = Modifier, 39 | padding: PaddingValues = PaddingValues(vertical = 4.dp, horizontal = 6.dp), 40 | onClick: () -> Unit, 41 | content: @Composable RowScope.() -> Unit, 42 | ) { 43 | Box( 44 | modifier = Modifier 45 | .clickable(onClick = onClick) 46 | .padding(padding) 47 | ) { 48 | CompositionLocalProvider(LocalTextColor provides Theme.color.primary) { 49 | Row( 50 | modifier = modifier, 51 | verticalAlignment = Alignment.CenterVertically, 52 | horizontalArrangement = Arrangement.Center, 53 | content = content 54 | ) 55 | } 56 | } 57 | } 58 | 59 | @Composable 60 | private fun FilledButton( 61 | modifier: Modifier = Modifier, 62 | content: @Composable RowScope.() -> Unit, 63 | color: Color, 64 | textColor: Color, 65 | ) { 66 | Box( 67 | modifier = Modifier 68 | .fillMaxWidth() 69 | .background(color = color, shape = RoundedCornerShape(100)) 70 | .padding(vertical = 12.dp, horizontal = 16.dp), 71 | contentAlignment = Alignment.Center 72 | ) { 73 | CompositionLocalProvider(LocalTextColor provides textColor) { 74 | Row( 75 | modifier = modifier, 76 | verticalAlignment = Alignment.CenterVertically, 77 | horizontalArrangement = Arrangement.Center, 78 | content = content 79 | ) 80 | } 81 | } 82 | } -------------------------------------------------------------------------------- /lib-components/src/main/java/bakuen/wear/components/Density.kt: -------------------------------------------------------------------------------- 1 | package bakuen.wear.components 2 | 3 | import android.content.res.Resources 4 | import androidx.compose.runtime.Composable 5 | import androidx.compose.ui.platform.LocalDensity 6 | 7 | @Composable 8 | fun Float.pxToDp() = with(LocalDensity.current) { 9 | this@pxToDp.toDp() 10 | } 11 | 12 | object DensityData { 13 | val screenHeight = Resources.getSystem().displayMetrics.heightPixels 14 | val screenWidth = Resources.getSystem().displayMetrics.widthPixels 15 | } -------------------------------------------------------------------------------- /lib-components/src/main/java/bakuen/wear/components/Icon.kt: -------------------------------------------------------------------------------- 1 | package bakuen.wear.components 2 | 3 | import androidx.annotation.DrawableRes 4 | import androidx.compose.foundation.Image 5 | import androidx.compose.foundation.clickable 6 | import androidx.compose.foundation.layout.padding 7 | import androidx.compose.foundation.layout.size 8 | import androidx.compose.runtime.Composable 9 | import androidx.compose.ui.Modifier 10 | import androidx.compose.ui.graphics.Color 11 | import androidx.compose.ui.graphics.ColorFilter 12 | import androidx.compose.ui.graphics.painter.Painter 13 | import androidx.compose.ui.res.painterResource 14 | import androidx.compose.ui.unit.Dp 15 | 16 | @Composable 17 | fun Icon(modifier: Modifier, @DrawableRes res: Int, color: Color = LocalTextColor.current) { 18 | Image( 19 | modifier = modifier, 20 | painter = painterResource(id = res), 21 | contentDescription = null, 22 | colorFilter = ColorFilter.tint(color) 23 | ) 24 | } -------------------------------------------------------------------------------- /lib-components/src/main/java/bakuen/wear/components/Modifier.kt: -------------------------------------------------------------------------------- 1 | package bakuen.wear.components 2 | 3 | import androidx.compose.foundation.clickable 4 | import androidx.compose.foundation.interaction.MutableInteractionSource 5 | import androidx.compose.foundation.layout.size 6 | import androidx.compose.runtime.Composable 7 | import androidx.compose.runtime.remember 8 | import androidx.compose.ui.Modifier 9 | import androidx.compose.ui.platform.LocalContext 10 | import androidx.compose.ui.platform.LocalDensity 11 | import kotlin.math.sqrt 12 | 13 | fun Modifier.calculate(block: ()->Modifier?) = 14 | block()?.let { then(it) } ?: then(this) 15 | 16 | fun Modifier.condition(condition: Boolean, ifTrue: Modifier, ifFalse: Modifier = this) = 17 | if (condition) then(ifTrue) else then(ifFalse) 18 | 19 | @Composable 20 | fun Modifier.cleanClick(onClick: (() -> Unit) = {}) = then( 21 | Modifier.clickable( 22 | interactionSource = remember { MutableInteractionSource() }, 23 | indication = null, 24 | onClick = onClick 25 | ) 26 | ) 27 | 28 | @Composable 29 | fun Modifier.inscribedBoxSize() = 30 | then(Modifier.size((LocalContext.current.resources.displayMetrics.widthPixels / sqrt(2f)).pxToDp())) 31 | 32 | internal fun Modifier.nullableClick(onClick: (() -> Unit)?): Modifier { 33 | return then(Modifier.clickable(onClick = onClick ?: return this)) 34 | } -------------------------------------------------------------------------------- /lib-components/src/main/java/bakuen/wear/components/Shapes.kt: -------------------------------------------------------------------------------- 1 | package bakuen.wear.components 2 | 3 | import androidx.compose.foundation.shape.RoundedCornerShape 4 | 5 | object Shapes { 6 | val Cell = RoundedCornerShape(100) 7 | } -------------------------------------------------------------------------------- /lib-components/src/main/java/bakuen/wear/components/SimpleViewModel.kt: -------------------------------------------------------------------------------- 1 | package bakuen.wear.components 2 | 3 | import androidx.compose.runtime.Composable 4 | import androidx.compose.runtime.remember 5 | import kotlinx.coroutines.MainScope 6 | 7 | @Composable 8 | fun > rememberViewModel(factory: ()->VM) = remember(factory) 9 | 10 | abstract class SimpleViewModel { 11 | val scope = MainScope() 12 | abstract fun dispatch(event: T) 13 | } -------------------------------------------------------------------------------- /lib-components/src/main/java/bakuen/wear/components/Space.kt: -------------------------------------------------------------------------------- 1 | package bakuen.wear.components 2 | 3 | import androidx.compose.foundation.layout.ColumnScope 4 | import androidx.compose.foundation.layout.RowScope 5 | import androidx.compose.foundation.layout.Spacer 6 | import androidx.compose.foundation.layout.height 7 | import androidx.compose.foundation.layout.width 8 | import androidx.compose.runtime.Composable 9 | import androidx.compose.ui.Modifier 10 | import androidx.compose.ui.unit.Dp 11 | 12 | @Composable 13 | fun ColumnScope.Space(size: Dp) { 14 | Spacer(modifier = Modifier.height(size)) 15 | } 16 | 17 | @Composable 18 | fun RowScope.Space(size: Dp) { 19 | Spacer(modifier = Modifier.width(size)) 20 | } -------------------------------------------------------------------------------- /lib-components/src/main/java/bakuen/wear/components/SwipeToDismiss.kt: -------------------------------------------------------------------------------- 1 | package bakuen.wear.components 2 | 3 | import android.content.res.Resources 4 | import androidx.compose.animation.core.tween 5 | import androidx.compose.animation.rememberSplineBasedDecay 6 | import androidx.compose.foundation.ExperimentalFoundationApi 7 | import androidx.compose.foundation.gestures.AnchoredDraggableState 8 | import androidx.compose.foundation.gestures.DraggableAnchors 9 | import androidx.compose.foundation.gestures.Orientation 10 | import androidx.compose.foundation.gestures.anchoredDraggable 11 | import androidx.compose.foundation.layout.Box 12 | import androidx.compose.foundation.layout.BoxScope 13 | import androidx.compose.foundation.layout.offset 14 | import androidx.compose.runtime.Composable 15 | import androidx.compose.runtime.remember 16 | import androidx.compose.ui.Modifier 17 | import androidx.compose.ui.platform.LocalDensity 18 | import androidx.compose.ui.unit.Dp 19 | import androidx.compose.ui.unit.IntOffset 20 | 21 | private enum class DragAnchors { 22 | Normal, 23 | Dismiss, 24 | } 25 | 26 | @OptIn(ExperimentalFoundationApi::class) 27 | @Composable 28 | fun SwipeToDismiss( 29 | modifier: Modifier = Modifier, 30 | velocity: Dp, 31 | onDismiss: ()->Unit, 32 | reverse: Boolean = false, 33 | content: @Composable BoxScope.()->Unit 34 | ) { 35 | val reverseSymbol = if (reverse) -1 else 1 36 | val maxOffset = reverseSymbol * Resources.getSystem().displayMetrics.widthPixels.toFloat() 37 | val density = LocalDensity.current 38 | val decay = rememberSplineBasedDecay() 39 | val state = remember { 40 | AnchoredDraggableState( 41 | initialValue = DragAnchors.Normal, 42 | positionalThreshold = { totalDistance -> 43 | totalDistance * 0.5f 44 | }, 45 | velocityThreshold = { 46 | with(density) { 47 | velocity.toPx() 48 | } 49 | }, 50 | snapAnimationSpec = tween(), 51 | decayAnimationSpec = decay, 52 | anchors = DraggableAnchors { 53 | DragAnchors.Normal at 0f 54 | DragAnchors.Dismiss at maxOffset 55 | } 56 | ) 57 | } 58 | Box( 59 | modifier = modifier 60 | .offset { 61 | val x = state.requireOffset() 62 | if (x == maxOffset) onDismiss() 63 | IntOffset(x = x.toInt(), y = 0) 64 | } 65 | .anchoredDraggable( 66 | state = state, 67 | orientation = Orientation.Horizontal, 68 | ), 69 | content = content 70 | ) 71 | } -------------------------------------------------------------------------------- /lib-components/src/main/java/bakuen/wear/components/Text.kt: -------------------------------------------------------------------------------- 1 | package bakuen.wear.components 2 | 3 | import androidx.compose.foundation.text.BasicText 4 | import androidx.compose.foundation.text.input.TextFieldLineLimits 5 | import androidx.compose.runtime.Composable 6 | import androidx.compose.runtime.staticCompositionLocalOf 7 | import androidx.compose.ui.Modifier 8 | import androidx.compose.ui.graphics.Color 9 | import androidx.compose.ui.text.TextStyle 10 | 11 | val LocalTextColor = staticCompositionLocalOf { Theme.color.onSurface } 12 | 13 | @Composable 14 | fun Text( 15 | modifier: Modifier = Modifier, 16 | text: String, 17 | style: TextStyle = Theme.typo.body, 18 | color: Color = LocalTextColor.current, 19 | lineLimits: TextFieldLineLimits = TextFieldLineLimits.Default, 20 | ) { 21 | val minLines = 22 | if (lineLimits is TextFieldLineLimits.MultiLine) lineLimits.minHeightInLines else 1 23 | val maxLines = 24 | if (lineLimits is TextFieldLineLimits.MultiLine) lineLimits.maxHeightInLines else 1 25 | BasicText( 26 | modifier = modifier, text = text, style = style.copy( 27 | color = color 28 | ), minLines = minLines, maxLines = maxLines, 29 | ) 30 | } -------------------------------------------------------------------------------- /lib-components/src/main/java/bakuen/wear/components/TextField.kt: -------------------------------------------------------------------------------- 1 | package bakuen.wear.components 2 | 3 | import androidx.compose.foundation.ExperimentalFoundationApi 4 | import androidx.compose.foundation.background 5 | import androidx.compose.foundation.layout.Box 6 | import androidx.compose.foundation.text.BasicTextField 7 | import androidx.compose.foundation.text.KeyboardOptions 8 | import androidx.compose.foundation.text.input.TextFieldLineLimits 9 | import androidx.compose.foundation.text.input.TextFieldState 10 | import androidx.compose.runtime.Composable 11 | import androidx.compose.ui.Modifier 12 | import androidx.compose.ui.text.TextStyle 13 | 14 | @Composable 15 | fun TextField( 16 | modifier: Modifier = Modifier, 17 | state: TextFieldState, 18 | textStyle: TextStyle = Theme.typo.body, 19 | keyboardOptions: KeyboardOptions = KeyboardOptions.Default, 20 | lineLimits: TextFieldLineLimits = TextFieldLineLimits.Default, 21 | hint: String = "" 22 | ) { 23 | BasicTextField( 24 | modifier = modifier, 25 | state = state, 26 | textStyle = textStyle, 27 | keyboardOptions = keyboardOptions, 28 | lineLimits = lineLimits, 29 | decorator = { innerInputField -> 30 | if (state.text.isEmpty()) Text( 31 | text = hint, 32 | color = Theme.color.onSurfaceVariant, 33 | style = textStyle, 34 | lineLimits = TextFieldLineLimits.SingleLine 35 | ) 36 | innerInputField() 37 | } 38 | ) 39 | } -------------------------------------------------------------------------------- /lib-components/src/main/java/bakuen/wear/components/Theme.kt: -------------------------------------------------------------------------------- 1 | package bakuen.wear.components 2 | 3 | import androidx.compose.ui.graphics.Color 4 | import androidx.compose.ui.graphics.toArgb 5 | import androidx.compose.ui.text.TextStyle 6 | import androidx.compose.ui.text.font.FontWeight 7 | import androidx.compose.ui.unit.sp 8 | import com.kyant.m3color.hct.Hct 9 | import com.kyant.m3color.scheme.SchemeTonalSpot 10 | 11 | object Theme { 12 | var color = ColorScheme.fromPrimary(keyColor = Color(0xFF_769CDF)) 13 | val typo = Typo( 14 | body = TextStyle.Default.copy( 15 | color = color.onSurface, 16 | fontWeight = FontWeight.Normal, 17 | fontSize = 11.sp 18 | ) 19 | ) 20 | 21 | } 22 | 23 | data class ColorScheme( 24 | val primary: Color, 25 | val onPrimary: Color, 26 | val primaryContainer: Color, 27 | val onPrimaryContainer: Color, 28 | val surface: Color, 29 | val onSurface: Color, 30 | val surfaceContainer: Color, 31 | val onSurfaceVariant: Color, 32 | val outline: Color, 33 | ) { 34 | companion object { 35 | fun fromPrimary( 36 | keyColor: Color, 37 | isDark: Boolean = true, 38 | pureDark: Boolean = true, 39 | contrastLevel: Double = 0.0 40 | ): ColorScheme { 41 | val hct = Hct.fromInt(keyColor.toArgb()) 42 | val scheme = SchemeTonalSpot(hct, isDark, contrastLevel) 43 | val surface = 44 | if (pureDark) if (isDark) 0xFF000000.toInt() else 0xFFFFFFFF.toInt() else scheme.surface 45 | return ColorScheme( 46 | primary = Color(scheme.primary), 47 | onPrimary = Color(scheme.onPrimary), 48 | primaryContainer = Color(scheme.primaryContainer), 49 | onPrimaryContainer = Color(scheme.onPrimaryContainer), 50 | surface = Color(surface), 51 | surfaceContainer = Color(scheme.surfaceContainer), 52 | onSurface = Color(scheme.onSurface), 53 | onSurfaceVariant = Color(scheme.outline), 54 | outline = Color(scheme.outline) 55 | ) 56 | } 57 | } 58 | } 59 | 60 | data class Typo( 61 | val body: TextStyle, 62 | val bodySmall: TextStyle = body.copy( 63 | fontSize = body.fontSize.times(0.9f) 64 | ), 65 | val title: TextStyle = body.copy( 66 | fontWeight = FontWeight.Bold 67 | ), 68 | val titleSmall: TextStyle = title.copy( 69 | fontSize = title.fontSize.times(0.9f) 70 | ) 71 | ) -------------------------------------------------------------------------------- /lib-components/src/main/java/bakuen/wear/components/material3/ExperimentalMaterial3Api.kt: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2021 The Android Open Source Project 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | package bakuen.wear.components.material3 18 | 19 | @RequiresOptIn( 20 | "This material API is experimental and is likely to change or to be removed in" + " the future." 21 | ) 22 | @Retention(AnnotationRetention.BINARY) 23 | annotation class ExperimentalMaterial3Api 24 | -------------------------------------------------------------------------------- /lib-components/src/main/java/bakuen/wear/components/material3/ProgressIndicator.kt: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2022 The Android Open Source Project 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | package bakuen.wear.components.material3 18 | 19 | import androidx.compose.animation.core.LinearEasing 20 | import androidx.compose.animation.core.Spring 21 | import androidx.compose.animation.core.SpringSpec 22 | import androidx.compose.animation.core.animateFloat 23 | import androidx.compose.animation.core.infiniteRepeatable 24 | import androidx.compose.animation.core.keyframes 25 | import androidx.compose.animation.core.rememberInfiniteTransition 26 | import androidx.compose.animation.core.tween 27 | import androidx.compose.foundation.Canvas 28 | import androidx.compose.foundation.layout.size 29 | import androidx.compose.foundation.progressSemantics 30 | import bakuen.wear.components.material3.ProgressIndicatorDefaults.drawStopIndicator 31 | import bakuen.wear.components.material3.internal.IncreaseVerticalSemanticsBounds 32 | import androidx.compose.material3.tokens.CircularProgressIndicatorTokens 33 | import bakuen.wear.components.material3.tokens.LinearProgressIndicatorTokens 34 | import bakuen.wear.components.material3.tokens.MotionTokens 35 | import bakuen.wear.components.material3.tokens.ProgressIndicatorTokens 36 | import androidx.compose.runtime.Composable 37 | import androidx.compose.ui.Modifier 38 | import androidx.compose.ui.geometry.Offset 39 | import androidx.compose.ui.geometry.Size 40 | import androidx.compose.ui.graphics.Color 41 | import androidx.compose.ui.graphics.StrokeCap 42 | import androidx.compose.ui.graphics.drawscope.DrawScope 43 | import androidx.compose.ui.graphics.drawscope.Stroke 44 | import androidx.compose.ui.graphics.drawscope.rotate 45 | import androidx.compose.ui.platform.LocalDensity 46 | import androidx.compose.ui.semantics.ProgressBarRangeInfo 47 | import androidx.compose.ui.semantics.progressBarRangeInfo 48 | import androidx.compose.ui.semantics.semantics 49 | import androidx.compose.ui.unit.Dp 50 | import androidx.compose.ui.unit.LayoutDirection 51 | import androidx.compose.ui.unit.dp 52 | import kotlin.math.PI 53 | import kotlin.math.abs 54 | import kotlin.math.max 55 | import kotlin.math.min 56 | 57 | /** 58 | * Determinate Material Design linear progress indicator. 60 | * 61 | * Progress indicators express an unspecified wait time or display the duration of a process. 62 | * 63 | * ![Linear progress indicator 64 | * image](https://firebasestorage.googleapis.com/v0/b/design-spec/o/projects%2Fgoogle-material-3%2Fimages%2Flqdiyyvh-1P-progress-indicator-configurations.png?alt=media) 65 | * 66 | * By default there is no animation between [progress] values. You can use 67 | * [ProgressIndicatorDefaults.ProgressAnimationSpec] as the default recommended [AnimationSpec] when 68 | * animating progress, such as in the following example: 69 | * 70 | * @sample androidx.compose.material3.samples.LinearProgressIndicatorSample 71 | * @param progress the progress of this progress indicator, where 0.0 represents no progress and 1.0 72 | * represents full progress. Values outside of this range are coerced into the range. 73 | * @param modifier the [Modifier] to be applied to this progress indicator 74 | * @param color color of this progress indicator 75 | * @param trackColor color of the track behind the indicator, visible when the progress has not 76 | * reached the area of the overall indicator yet 77 | * @param strokeCap stroke cap to use for the ends of this progress indicator 78 | */ 79 | @Deprecated( 80 | message = 81 | "Use the overload that takes `gapSize` and `drawStopIndicator`, see " + 82 | "`LegacyLinearProgressIndicatorSample` on how to restore the previous behavior", 83 | replaceWith = 84 | ReplaceWith( 85 | "LinearProgressIndicator(progress, modifier, color, trackColor, strokeCap, " + 86 | "gapSize, drawStopIndicator)" 87 | ), 88 | level = DeprecationLevel.HIDDEN 89 | ) 90 | @OptIn(ExperimentalMaterial3Api::class) 91 | @Composable 92 | fun LinearProgressIndicator( 93 | progress: () -> Float, 94 | modifier: Modifier = Modifier, 95 | color: Color = ProgressIndicatorDefaults.linearColor, 96 | trackColor: Color = ProgressIndicatorDefaults.linearTrackColor, 97 | strokeCap: StrokeCap = ProgressIndicatorDefaults.LinearStrokeCap, 98 | ) { 99 | LinearProgressIndicator( 100 | progress, 101 | modifier, 102 | color, 103 | trackColor, 104 | strokeCap, 105 | gapSize = ProgressIndicatorDefaults.LinearIndicatorTrackGapSize 106 | ) 107 | } 108 | 109 | /** 110 | * Determinate Material Design linear progress indicator. 112 | * 113 | * Progress indicators express an unspecified wait time or display the duration of a process. 114 | * 115 | * ![Linear progress indicator 116 | * image](https://firebasestorage.googleapis.com/v0/b/design-spec/o/projects%2Fgoogle-material-3%2Fimages%2Flqdiyyvh-1P-progress-indicator-configurations.png?alt=media) 117 | * 118 | * By default there is no animation between [progress] values. You can use 119 | * [ProgressIndicatorDefaults.ProgressAnimationSpec] as the default recommended [AnimationSpec] when 120 | * animating progress, such as in the following example: 121 | * 122 | * @sample androidx.compose.material3.samples.LinearProgressIndicatorSample 123 | * @param progress the progress of this progress indicator, where 0.0 represents no progress and 1.0 124 | * represents full progress. Values outside of this range are coerced into the range. 125 | * @param modifier the [Modifier] to be applied to this progress indicator 126 | * @param color color of this progress indicator 127 | * @param trackColor color of the track behind the indicator, visible when the progress has not 128 | * reached the area of the overall indicator yet 129 | * @param strokeCap stroke cap to use for the ends of this progress indicator 130 | * @param gapSize size of the gap between the progress indicator and the track 131 | * @param drawStopIndicator lambda that will be called to draw the stop indicator 132 | */ 133 | @OptIn(ExperimentalMaterial3Api::class) 134 | @Composable 135 | fun LinearProgressIndicator( 136 | progress: () -> Float, 137 | modifier: Modifier = Modifier, 138 | color: Color = ProgressIndicatorDefaults.linearColor, 139 | trackColor: Color = ProgressIndicatorDefaults.linearTrackColor, 140 | strokeCap: StrokeCap = ProgressIndicatorDefaults.LinearStrokeCap, 141 | gapSize: Dp = ProgressIndicatorDefaults.LinearIndicatorTrackGapSize, 142 | drawStopIndicator: DrawScope.() -> Unit = { 143 | drawStopIndicator( 144 | drawScope = this, 145 | stopSize = ProgressIndicatorDefaults.LinearTrackStopIndicatorSize, 146 | color = color, 147 | strokeCap = strokeCap 148 | ) 149 | }, 150 | ) { 151 | val coercedProgress = { progress().coerceIn(0f, 1f) } 152 | Canvas( 153 | modifier 154 | .then(IncreaseVerticalSemanticsBounds) 155 | .semantics(mergeDescendants = true) { 156 | progressBarRangeInfo = ProgressBarRangeInfo(coercedProgress(), 0f..1f) 157 | } 158 | .size(LinearIndicatorWidth, LinearIndicatorHeight) 159 | ) { 160 | val strokeWidth = size.height 161 | val adjustedGapSize = 162 | if (strokeCap == StrokeCap.Butt || size.height > size.width) { 163 | gapSize 164 | } else { 165 | gapSize + strokeWidth.toDp() 166 | } 167 | val gapSizeFraction = adjustedGapSize / size.width.toDp() 168 | val currentCoercedProgress = coercedProgress() 169 | 170 | // track 171 | val trackStartFraction = 172 | currentCoercedProgress + min(currentCoercedProgress, gapSizeFraction) 173 | if (trackStartFraction <= 1f) { 174 | drawLinearIndicator(trackStartFraction, 1f, trackColor, strokeWidth, strokeCap) 175 | } 176 | // indicator 177 | drawLinearIndicator(0f, currentCoercedProgress, color, strokeWidth, strokeCap) 178 | // stop 179 | drawStopIndicator(this) 180 | } 181 | } 182 | 183 | /** 184 | * Indeterminate Material Design linear progress indicator. 186 | * 187 | * Progress indicators express an unspecified wait time or display the duration of a process. 188 | * 189 | * ![Linear progress indicator 190 | * image](https://firebasestorage.googleapis.com/v0/b/design-spec/o/projects%2Fgoogle-material-3%2Fimages%2Flqdiyyvh-1P-progress-indicator-configurations.png?alt=media) 191 | * 192 | * @sample androidx.compose.material3.samples.IndeterminateLinearProgressIndicatorSample 193 | * @param modifier the [Modifier] to be applied to this progress indicator 194 | * @param color color of this progress indicator 195 | * @param trackColor color of the track behind the indicator, visible when the progress has not 196 | * reached the area of the overall indicator yet 197 | * @param strokeCap stroke cap to use for the ends of this progress indicator 198 | */ 199 | @Deprecated( 200 | message = 201 | "Use the overload that takes `gapSize`, see `" + 202 | "LegacyIndeterminateLinearProgressIndicatorSample` on how to restore the previous behavior", 203 | replaceWith = 204 | ReplaceWith("LinearProgressIndicator(modifier, color, trackColor, strokeCap, gapSize)"), 205 | level = DeprecationLevel.HIDDEN 206 | ) 207 | @OptIn(ExperimentalMaterial3Api::class) 208 | @Composable 209 | fun LinearProgressIndicator( 210 | modifier: Modifier = Modifier, 211 | color: Color = ProgressIndicatorDefaults.linearColor, 212 | trackColor: Color = ProgressIndicatorDefaults.linearTrackColor, 213 | strokeCap: StrokeCap = ProgressIndicatorDefaults.LinearStrokeCap, 214 | ) { 215 | LinearProgressIndicator( 216 | modifier, 217 | color, 218 | trackColor, 219 | strokeCap, 220 | gapSize = ProgressIndicatorDefaults.LinearIndicatorTrackGapSize, 221 | ) 222 | } 223 | 224 | /** 225 | * Indeterminate Material Design linear progress indicator. 227 | * 228 | * Progress indicators express an unspecified wait time or display the duration of a process. 229 | * 230 | * ![Linear progress indicator 231 | * image](https://firebasestorage.googleapis.com/v0/b/design-spec/o/projects%2Fgoogle-material-3%2Fimages%2Flqdiyyvh-1P-progress-indicator-configurations.png?alt=media) 232 | * 233 | * @sample androidx.compose.material3.samples.IndeterminateLinearProgressIndicatorSample 234 | * @param modifier the [Modifier] to be applied to this progress indicator 235 | * @param color color of this progress indicator 236 | * @param trackColor color of the track behind the indicator, visible when the progress has not 237 | * reached the area of the overall indicator yet 238 | * @param strokeCap stroke cap to use for the ends of this progress indicator 239 | * @param gapSize size of the gap between the progress indicator and the track 240 | */ 241 | @OptIn(ExperimentalMaterial3Api::class) 242 | @Composable 243 | fun LinearProgressIndicator( 244 | modifier: Modifier = Modifier, 245 | color: Color = ProgressIndicatorDefaults.linearColor, 246 | trackColor: Color = ProgressIndicatorDefaults.linearTrackColor, 247 | strokeCap: StrokeCap = ProgressIndicatorDefaults.LinearStrokeCap, 248 | gapSize: Dp = ProgressIndicatorDefaults.LinearIndicatorTrackGapSize, 249 | ) { 250 | val infiniteTransition = rememberInfiniteTransition() 251 | val firstLineHead = 252 | infiniteTransition.animateFloat( 253 | initialValue = 0f, 254 | targetValue = 1f, 255 | animationSpec = linearIndeterminateFirstLineHeadAnimationSpec 256 | ) 257 | val firstLineTail = 258 | infiniteTransition.animateFloat( 259 | initialValue = 0f, 260 | targetValue = 1f, 261 | animationSpec = linearIndeterminateFirstLineTailAnimationSpec 262 | ) 263 | val secondLineHead = 264 | infiniteTransition.animateFloat( 265 | initialValue = 0f, 266 | targetValue = 1f, 267 | animationSpec = linearIndeterminateSecondLineHeadAnimationSpec 268 | ) 269 | val secondLineTail = 270 | infiniteTransition.animateFloat( 271 | initialValue = 0f, 272 | targetValue = 1f, 273 | animationSpec = linearIndeterminateSecondLineTailAnimationSpec 274 | ) 275 | Canvas( 276 | modifier 277 | .then(IncreaseVerticalSemanticsBounds) 278 | .progressSemantics() 279 | .size(LinearIndicatorWidth, LinearIndicatorHeight) 280 | ) { 281 | val strokeWidth = size.height 282 | val adjustedGapSize = 283 | if (strokeCap == StrokeCap.Butt || size.height > size.width) { 284 | gapSize 285 | } else { 286 | gapSize + strokeWidth.toDp() 287 | } 288 | val gapSizeFraction = adjustedGapSize / size.width.toDp() 289 | 290 | // Track before line 1 291 | if (firstLineHead.value < 1f - gapSizeFraction) { 292 | val start = if (firstLineHead.value > 0) firstLineHead.value + gapSizeFraction else 0f 293 | drawLinearIndicator(start, 1f, trackColor, strokeWidth, strokeCap) 294 | } 295 | 296 | // Line 1 297 | if (firstLineHead.value - firstLineTail.value > 0) { 298 | drawLinearIndicator( 299 | firstLineHead.value, 300 | firstLineTail.value, 301 | color, 302 | strokeWidth, 303 | strokeCap, 304 | ) 305 | } 306 | 307 | // Track between line 1 and line 2 308 | if (firstLineTail.value > gapSizeFraction) { 309 | val start = if (secondLineHead.value > 0) secondLineHead.value + gapSizeFraction else 0f 310 | val end = if (firstLineTail.value < 1f) firstLineTail.value - gapSizeFraction else 1f 311 | drawLinearIndicator(start, end, trackColor, strokeWidth, strokeCap) 312 | } 313 | 314 | // Line 2 315 | if (secondLineHead.value - secondLineTail.value > 0) { 316 | drawLinearIndicator( 317 | secondLineHead.value, 318 | secondLineTail.value, 319 | color, 320 | strokeWidth, 321 | strokeCap, 322 | ) 323 | } 324 | 325 | // Track after line 2 326 | if (secondLineTail.value > gapSizeFraction) { 327 | val end = if (secondLineTail.value < 1) secondLineTail.value - gapSizeFraction else 1f 328 | drawLinearIndicator(0f, end, trackColor, strokeWidth, strokeCap) 329 | } 330 | } 331 | } 332 | 333 | @Deprecated( 334 | message = "Use the overload that takes `progress` as a lambda", 335 | replaceWith = 336 | ReplaceWith( 337 | "LinearProgressIndicator(\n" + 338 | "progress = { progress },\n" + 339 | "modifier = modifier,\n" + 340 | "color = color,\n" + 341 | "trackColor = trackColor,\n" + 342 | "strokeCap = strokeCap,\n" + 343 | ")" 344 | ) 345 | ) 346 | @Composable 347 | fun LinearProgressIndicator( 348 | progress: Float, 349 | modifier: Modifier = Modifier, 350 | color: Color = ProgressIndicatorDefaults.linearColor, 351 | trackColor: Color = ProgressIndicatorDefaults.linearTrackColor, 352 | strokeCap: StrokeCap = ProgressIndicatorDefaults.LinearStrokeCap, 353 | ) = 354 | LinearProgressIndicator( 355 | progress = { progress }, 356 | modifier = modifier, 357 | color = color, 358 | trackColor = trackColor, 359 | strokeCap = strokeCap, 360 | ) 361 | 362 | @Suppress("DEPRECATION") 363 | @Deprecated("Maintained for binary compatibility", level = DeprecationLevel.HIDDEN) 364 | @Composable 365 | fun LinearProgressIndicator( 366 | progress: Float, 367 | modifier: Modifier = Modifier, 368 | color: Color = ProgressIndicatorDefaults.linearColor, 369 | trackColor: Color = ProgressIndicatorDefaults.linearTrackColor, 370 | ) = 371 | LinearProgressIndicator( 372 | progress, 373 | modifier, 374 | color, 375 | trackColor, 376 | strokeCap = ProgressIndicatorDefaults.LinearStrokeCap, 377 | ) 378 | 379 | @Deprecated("Maintained for binary compatibility", level = DeprecationLevel.HIDDEN) 380 | @Composable 381 | fun LinearProgressIndicator( 382 | modifier: Modifier = Modifier, 383 | color: Color = ProgressIndicatorDefaults.linearColor, 384 | trackColor: Color = ProgressIndicatorDefaults.linearTrackColor, 385 | ) = 386 | LinearProgressIndicator( 387 | modifier, 388 | color, 389 | trackColor, 390 | strokeCap = ProgressIndicatorDefaults.LinearStrokeCap, 391 | ) 392 | 393 | private fun DrawScope.drawLinearIndicator( 394 | startFraction: Float, 395 | endFraction: Float, 396 | color: Color, 397 | strokeWidth: Float, 398 | strokeCap: StrokeCap, 399 | ) { 400 | val width = size.width 401 | val height = size.height 402 | // Start drawing from the vertical center of the stroke 403 | val yOffset = height / 2 404 | 405 | val isLtr = layoutDirection == LayoutDirection.Ltr 406 | val barStart = (if (isLtr) startFraction else 1f - endFraction) * width 407 | val barEnd = (if (isLtr) endFraction else 1f - startFraction) * width 408 | 409 | // if there isn't enough space to draw the stroke caps, fall back to StrokeCap.Butt 410 | if (strokeCap == StrokeCap.Butt || height > width) { 411 | // Progress line 412 | drawLine(color, Offset(barStart, yOffset), Offset(barEnd, yOffset), strokeWidth) 413 | } else { 414 | // need to adjust barStart and barEnd for the stroke caps 415 | val strokeCapOffset = strokeWidth / 2 416 | val coerceRange = strokeCapOffset..(width - strokeCapOffset) 417 | val adjustedBarStart = barStart.coerceIn(coerceRange) 418 | val adjustedBarEnd = barEnd.coerceIn(coerceRange) 419 | 420 | if (abs(endFraction - startFraction) > 0) { 421 | // Progress line 422 | drawLine( 423 | color, 424 | Offset(adjustedBarStart, yOffset), 425 | Offset(adjustedBarEnd, yOffset), 426 | strokeWidth, 427 | strokeCap, 428 | ) 429 | } 430 | } 431 | } 432 | 433 | /** 434 | * Determinate Material Design circular progress indicator. 436 | * 437 | * Progress indicators express an unspecified wait time or display the duration of a process. 438 | * 439 | * ![Circular progress indicator 440 | * image](https://firebasestorage.googleapis.com/v0/b/design-spec/o/projects%2Fgoogle-material-3%2Fimages%2Flqdiyyvh-1P-progress-indicator-configurations.png?alt=media) 441 | * 442 | * By default there is no animation between [progress] values. You can use 443 | * [ProgressIndicatorDefaults.ProgressAnimationSpec] as the default recommended [AnimationSpec] when 444 | * animating progress, such as in the following example: 445 | * 446 | * @sample androidx.compose.material3.samples.CircularProgressIndicatorSample 447 | * @param progress the progress of this progress indicator, where 0.0 represents no progress and 1.0 448 | * represents full progress. Values outside of this range are coerced into the range. 449 | * @param modifier the [Modifier] to be applied to this progress indicator 450 | * @param color color of this progress indicator 451 | * @param strokeWidth stroke width of this progress indicator 452 | * @param trackColor color of the track behind the indicator, visible when the progress has not 453 | * reached the area of the overall indicator yet 454 | * @param strokeCap stroke cap to use for the ends of this progress indicator 455 | */ 456 | @Deprecated( 457 | message = 458 | "Use the overload that takes `gapSize`, see " + 459 | "`LegacyCircularProgressIndicatorSample` on how to restore the previous behavior", 460 | replaceWith = 461 | ReplaceWith( 462 | "CircularProgressIndicator(progress, modifier, color, strokeWidth, trackColor, " + 463 | "strokeCap, gapSize)" 464 | ), 465 | level = DeprecationLevel.HIDDEN 466 | ) 467 | @OptIn(ExperimentalMaterial3Api::class) 468 | @Composable 469 | fun CircularProgressIndicator( 470 | progress: () -> Float, 471 | modifier: Modifier = Modifier, 472 | color: Color = ProgressIndicatorDefaults.circularColor, 473 | strokeWidth: Dp = ProgressIndicatorDefaults.CircularStrokeWidth, 474 | trackColor: Color = ProgressIndicatorDefaults.circularDeterminateTrackColor, 475 | strokeCap: StrokeCap = ProgressIndicatorDefaults.CircularDeterminateStrokeCap, 476 | ) { 477 | CircularProgressIndicator( 478 | progress, 479 | modifier, 480 | color, 481 | strokeWidth, 482 | trackColor, 483 | strokeCap, 484 | gapSize = ProgressIndicatorDefaults.CircularIndicatorTrackGapSize 485 | ) 486 | } 487 | 488 | /** 489 | * Determinate Material Design circular progress indicator. 491 | * 492 | * Progress indicators express an unspecified wait time or display the duration of a process. 493 | * 494 | * ![Circular progress indicator 495 | * image](https://firebasestorage.googleapis.com/v0/b/design-spec/o/projects%2Fgoogle-material-3%2Fimages%2Flqdiyyvh-1P-progress-indicator-configurations.png?alt=media) 496 | * 497 | * By default there is no animation between [progress] values. You can use 498 | * [ProgressIndicatorDefaults.ProgressAnimationSpec] as the default recommended [AnimationSpec] when 499 | * animating progress, such as in the following example: 500 | * 501 | * @sample androidx.compose.material3.samples.CircularProgressIndicatorSample 502 | * @param progress the progress of this progress indicator, where 0.0 represents no progress and 1.0 503 | * represents full progress. Values outside of this range are coerced into the range. 504 | * @param modifier the [Modifier] to be applied to this progress indicator 505 | * @param color color of this progress indicator 506 | * @param strokeWidth stroke width of this progress indicator 507 | * @param trackColor color of the track behind the indicator, visible when the progress has not 508 | * reached the area of the overall indicator yet 509 | * @param strokeCap stroke cap to use for the ends of this progress indicator 510 | * @param gapSize size of the gap between the progress indicator and the track 511 | */ 512 | @OptIn(ExperimentalMaterial3Api::class) 513 | @Composable 514 | fun CircularProgressIndicator( 515 | progress: () -> Float, 516 | modifier: Modifier = Modifier, 517 | color: Color = ProgressIndicatorDefaults.circularColor, 518 | strokeWidth: Dp = ProgressIndicatorDefaults.CircularStrokeWidth, 519 | trackColor: Color = ProgressIndicatorDefaults.circularDeterminateTrackColor, 520 | strokeCap: StrokeCap = ProgressIndicatorDefaults.CircularDeterminateStrokeCap, 521 | gapSize: Dp = ProgressIndicatorDefaults.CircularIndicatorTrackGapSize, 522 | ) { 523 | val coercedProgress = { progress().coerceIn(0f, 1f) } 524 | val stroke = with(LocalDensity.current) { Stroke(width = strokeWidth.toPx(), cap = strokeCap) } 525 | Canvas( 526 | modifier 527 | .semantics(mergeDescendants = true) { 528 | progressBarRangeInfo = ProgressBarRangeInfo(coercedProgress(), 0f..1f) 529 | } 530 | .size(CircularIndicatorDiameter) 531 | ) { 532 | // Start at 12 o'clock 533 | val startAngle = 270f 534 | val sweep = coercedProgress() * 360f 535 | val adjustedGapSize = 536 | if (strokeCap == StrokeCap.Butt || size.height > size.width) { 537 | gapSize 538 | } else { 539 | gapSize + strokeWidth 540 | } 541 | val gapSizeSweep = (adjustedGapSize.value / (PI * size.width.toDp().value).toFloat()) * 360f 542 | 543 | drawCircularIndicator( 544 | startAngle + sweep + min(sweep, gapSizeSweep), 545 | 360f - sweep - min(sweep, gapSizeSweep) * 2, 546 | trackColor, 547 | stroke 548 | ) 549 | drawDeterminateCircularIndicator(startAngle, sweep, color, stroke) 550 | } 551 | } 552 | 553 | /** 554 | * Indeterminate Material Design circular progress indicator. 556 | * 557 | * Progress indicators express an unspecified wait time or display the duration of a process. 558 | * 559 | * ![Circular progress indicator 560 | * image](https://firebasestorage.googleapis.com/v0/b/design-spec/o/projects%2Fgoogle-material-3%2Fimages%2Flqdiyyvh-1P-progress-indicator-configurations.png?alt=media) 561 | * 562 | * @sample androidx.compose.material3.samples.IndeterminateCircularProgressIndicatorSample 563 | * @param modifier the [Modifier] to be applied to this progress indicator 564 | * @param color color of this progress indicator 565 | * @param strokeWidth stroke width of this progress indicator 566 | * @param trackColor color of the track behind the indicator, visible when the progress has not 567 | * reached the area of the overall indicator yet 568 | * @param strokeCap stroke cap to use for the ends of this progress indicator 569 | */ 570 | @Deprecated( 571 | message = "Use the overload that takes `gapSize`", 572 | replaceWith = 573 | ReplaceWith( 574 | "CircularProgressIndicator(modifier, color, strokeWidth, trackColor, strokeCap, " + 575 | "gapSize)" 576 | ), 577 | level = DeprecationLevel.HIDDEN 578 | ) 579 | @OptIn(ExperimentalMaterial3Api::class) 580 | @Composable 581 | fun CircularProgressIndicator( 582 | modifier: Modifier = Modifier, 583 | color: Color = ProgressIndicatorDefaults.circularColor, 584 | strokeWidth: Dp = ProgressIndicatorDefaults.CircularStrokeWidth, 585 | trackColor: Color = ProgressIndicatorDefaults.circularIndeterminateTrackColor, 586 | strokeCap: StrokeCap = ProgressIndicatorDefaults.CircularIndeterminateStrokeCap, 587 | ) = 588 | CircularProgressIndicator( 589 | modifier = modifier, 590 | color = color, 591 | strokeWidth = strokeWidth, 592 | trackColor = trackColor, 593 | strokeCap = strokeCap, 594 | gapSize = ProgressIndicatorDefaults.CircularIndicatorTrackGapSize 595 | ) 596 | 597 | /** 598 | * Indeterminate Material Design circular progress indicator. 600 | * 601 | * Progress indicators express an unspecified wait time or display the duration of a process. 602 | * 603 | * ![Circular progress indicator 604 | * image](https://firebasestorage.googleapis.com/v0/b/design-spec/o/projects%2Fgoogle-material-3%2Fimages%2Flqdiyyvh-1P-progress-indicator-configurations.png?alt=media) 605 | * 606 | * @sample androidx.compose.material3.samples.IndeterminateCircularProgressIndicatorSample 607 | * @param modifier the [Modifier] to be applied to this progress indicator 608 | * @param color color of this progress indicator 609 | * @param strokeWidth stroke width of this progress indicator 610 | * @param trackColor color of the track behind the indicator, visible when the progress has not 611 | * reached the area of the overall indicator yet 612 | * @param strokeCap stroke cap to use for the ends of this progress indicator 613 | * @param gapSize size of the gap between the progress indicator and the track 614 | */ 615 | @OptIn(ExperimentalMaterial3Api::class) 616 | @Composable 617 | fun CircularProgressIndicator( 618 | modifier: Modifier = Modifier, 619 | color: Color = ProgressIndicatorDefaults.circularColor, 620 | strokeWidth: Dp = ProgressIndicatorDefaults.CircularStrokeWidth, 621 | trackColor: Color = ProgressIndicatorDefaults.circularIndeterminateTrackColor, 622 | strokeCap: StrokeCap = ProgressIndicatorDefaults.CircularIndeterminateStrokeCap, 623 | gapSize: Dp = ProgressIndicatorDefaults.CircularIndicatorTrackGapSize 624 | ) { 625 | val stroke = with(LocalDensity.current) { Stroke(width = strokeWidth.toPx(), cap = strokeCap) } 626 | 627 | val infiniteTransition = rememberInfiniteTransition() 628 | // A global rotation that does a 360 degrees rotation in 6 seconds. 629 | val globalRotation = 630 | infiniteTransition.animateFloat( 631 | initialValue = 0f, 632 | targetValue = CircularGlobalRotationDegreesTarget, 633 | animationSpec = circularIndeterminateGlobalRotationAnimationSpec 634 | ) 635 | 636 | // An additional rotation that moves by 90 degrees in 500ms and then rest for 1 second. 637 | val additionalRotation = 638 | infiniteTransition.animateFloat( 639 | initialValue = 0f, 640 | targetValue = CircularAdditionalRotationDegreesTarget, 641 | animationSpec = circularIndeterminateRotationAnimationSpec 642 | ) 643 | 644 | // Indicator progress animation that will be changing the progress up and down as the indicator 645 | // rotates. 646 | val progressAnimation = 647 | infiniteTransition.animateFloat( 648 | initialValue = CircularIndeterminateMinProgress, 649 | targetValue = CircularIndeterminateMaxProgress, 650 | animationSpec = circularIndeterminateProgressAnimationSpec 651 | ) 652 | 653 | Canvas(modifier.progressSemantics().size(CircularIndicatorDiameter)) { 654 | val sweep = progressAnimation.value * 360f 655 | val adjustedGapSize = 656 | if (strokeCap == StrokeCap.Butt || size.height > size.width) { 657 | gapSize 658 | } else { 659 | gapSize + strokeWidth 660 | } 661 | val gapSizeSweep = (adjustedGapSize.value / (PI * size.width.toDp().value).toFloat()) * 360f 662 | 663 | rotate(globalRotation.value + additionalRotation.value) { 664 | drawCircularIndicator( 665 | sweep + min(sweep, gapSizeSweep), 666 | 360f - sweep - min(sweep, gapSizeSweep) * 2, 667 | trackColor, 668 | stroke 669 | ) 670 | drawDeterminateCircularIndicator(startAngle = 0f, sweep, color, stroke) 671 | } 672 | } 673 | } 674 | 675 | @Suppress("DEPRECATION") 676 | @Deprecated( 677 | message = "Use the overload that takes `progress` as a lambda", 678 | replaceWith = 679 | ReplaceWith( 680 | "CircularProgressIndicator(\n" + 681 | "progress = { progress },\n" + 682 | "modifier = modifier,\n" + 683 | "color = color,\n" + 684 | "strokeWidth = strokeWidth,\n" + 685 | "trackColor = trackColor,\n" + 686 | "strokeCap = strokeCap,\n" + 687 | ")" 688 | ) 689 | ) 690 | @Composable 691 | fun CircularProgressIndicator( 692 | progress: Float, 693 | modifier: Modifier = Modifier, 694 | color: Color = ProgressIndicatorDefaults.circularColor, 695 | strokeWidth: Dp = ProgressIndicatorDefaults.CircularStrokeWidth, 696 | trackColor: Color = ProgressIndicatorDefaults.circularTrackColor, 697 | strokeCap: StrokeCap = ProgressIndicatorDefaults.CircularDeterminateStrokeCap, 698 | ) = 699 | CircularProgressIndicator( 700 | progress = { progress }, 701 | modifier = modifier, 702 | color = color, 703 | strokeWidth = strokeWidth, 704 | trackColor = trackColor, 705 | strokeCap = strokeCap, 706 | ) 707 | 708 | @Suppress("DEPRECATION") 709 | @Deprecated("Maintained for binary compatibility", level = DeprecationLevel.HIDDEN) 710 | @Composable 711 | fun CircularProgressIndicator( 712 | progress: Float, 713 | modifier: Modifier = Modifier, 714 | color: Color = ProgressIndicatorDefaults.circularColor, 715 | strokeWidth: Dp = ProgressIndicatorDefaults.CircularStrokeWidth 716 | ) = 717 | CircularProgressIndicator( 718 | progress, 719 | modifier, 720 | color, 721 | strokeWidth, 722 | trackColor = ProgressIndicatorDefaults.circularTrackColor, 723 | strokeCap = ProgressIndicatorDefaults.CircularDeterminateStrokeCap, 724 | ) 725 | 726 | @Suppress("DEPRECATION") 727 | @Deprecated("Maintained for binary compatibility", level = DeprecationLevel.HIDDEN) 728 | @Composable 729 | fun CircularProgressIndicator( 730 | modifier: Modifier = Modifier, 731 | color: Color = ProgressIndicatorDefaults.circularColor, 732 | strokeWidth: Dp = ProgressIndicatorDefaults.CircularStrokeWidth 733 | ) = 734 | CircularProgressIndicator( 735 | modifier, 736 | color, 737 | strokeWidth, 738 | trackColor = ProgressIndicatorDefaults.circularTrackColor, 739 | strokeCap = ProgressIndicatorDefaults.CircularIndeterminateStrokeCap, 740 | ) 741 | 742 | private fun DrawScope.drawCircularIndicator( 743 | startAngle: Float, 744 | sweep: Float, 745 | color: Color, 746 | stroke: Stroke 747 | ) { 748 | // To draw this circle we need a rect with edges that line up with the midpoint of the stroke. 749 | // To do this we need to remove half the stroke width from the total diameter for both sides. 750 | val diameterOffset = stroke.width / 2 751 | val arcDimen = size.width - 2 * diameterOffset 752 | drawArc( 753 | color = color, 754 | startAngle = startAngle, 755 | sweepAngle = sweep, 756 | useCenter = false, 757 | topLeft = Offset(diameterOffset, diameterOffset), 758 | size = Size(arcDimen, arcDimen), 759 | style = stroke 760 | ) 761 | } 762 | 763 | private fun DrawScope.drawCircularIndicatorTrack(color: Color, stroke: Stroke) = 764 | drawCircularIndicator(0f, 360f, color, stroke) 765 | 766 | private fun DrawScope.drawDeterminateCircularIndicator( 767 | startAngle: Float, 768 | sweep: Float, 769 | color: Color, 770 | stroke: Stroke 771 | ) = drawCircularIndicator(startAngle, sweep, color, stroke) 772 | 773 | private fun DrawScope.drawIndeterminateCircularIndicator( 774 | startAngle: Float, 775 | strokeWidth: Dp, 776 | sweep: Float, 777 | color: Color, 778 | stroke: Stroke 779 | ) { 780 | val strokeCapOffset = 781 | if (stroke.cap == StrokeCap.Butt) { 782 | 0f 783 | } else { 784 | // Length of arc is angle * radius 785 | // Angle (radians) is length / radius 786 | // The length should be the same as the stroke width for calculating the min angle 787 | (180.0 / PI).toFloat() * (strokeWidth / (CircularIndicatorDiameter / 2)) / 2f 788 | } 789 | 790 | // Adding a stroke cap draws half the stroke width behind the start point, so we want to 791 | // move it forward by that amount so the arc visually appears in the correct place 792 | val adjustedStartAngle = startAngle + strokeCapOffset 793 | 794 | // When the start and end angles are in the same place, we still want to draw a small sweep, so 795 | // the stroke caps get added on both ends and we draw the correct minimum length arc 796 | val adjustedSweep = max(sweep, 0.1f) 797 | 798 | drawCircularIndicator(adjustedStartAngle, adjustedSweep, color, stroke) 799 | } 800 | 801 | /** 802 | * Contains the default values used for [LinearProgressIndicator] and [CircularProgressIndicator]. 803 | */ 804 | object ProgressIndicatorDefaults { 805 | /** Default color for a linear progress indicator. */ 806 | val linearColor: Color 807 | @Composable get() = ProgressIndicatorTokens.ActiveIndicatorColor 808 | 809 | /** Default color for a circular progress indicator. */ 810 | val circularColor: Color 811 | @Composable get() = ProgressIndicatorTokens.ActiveIndicatorColor 812 | 813 | /** Default track color for a linear progress indicator. */ 814 | val linearTrackColor: Color 815 | @Composable get() = ProgressIndicatorTokens.TrackColor 816 | 817 | /** Default track color for a circular progress indicator. */ 818 | @Deprecated( 819 | "Renamed to circularDeterminateTrackColor or circularIndeterminateTrackColor", 820 | ReplaceWith("ProgressIndicatorDefaults.circularIndeterminateTrackColor"), 821 | DeprecationLevel.WARNING 822 | ) 823 | val circularTrackColor: Color 824 | @Composable get() = Color.Transparent 825 | 826 | /** Default track color for a circular determinate progress indicator. */ 827 | val circularDeterminateTrackColor: Color 828 | @Composable get() = ProgressIndicatorTokens.TrackColor 829 | 830 | /** Default track color for a circular indeterminate progress indicator. */ 831 | val circularIndeterminateTrackColor: Color 832 | @Composable get() = Color.Transparent 833 | 834 | /** Default stroke width for a circular progress indicator. */ 835 | val CircularStrokeWidth: Dp = CircularProgressIndicatorTokens.TrackThickness 836 | 837 | /** Default stroke cap for a linear progress indicator. */ 838 | val LinearStrokeCap: StrokeCap = StrokeCap.Round 839 | 840 | /** Default stroke cap for a determinate circular progress indicator. */ 841 | val CircularDeterminateStrokeCap: StrokeCap = StrokeCap.Round 842 | 843 | /** Default stroke cap for an indeterminate circular progress indicator. */ 844 | val CircularIndeterminateStrokeCap: StrokeCap = StrokeCap.Round 845 | 846 | /** Default track stop indicator size for a linear progress indicator. */ 847 | @Suppress("OPT_IN_MARKER_ON_WRONG_TARGET") 848 | @get:ExperimentalMaterial3Api 849 | @ExperimentalMaterial3Api 850 | val LinearTrackStopIndicatorSize: Dp = LinearProgressIndicatorTokens.StopSize 851 | 852 | /** Default indicator track gap size for a linear progress indicator. */ 853 | @Suppress("OPT_IN_MARKER_ON_WRONG_TARGET") 854 | @get:ExperimentalMaterial3Api 855 | @ExperimentalMaterial3Api 856 | val LinearIndicatorTrackGapSize: Dp = LinearProgressIndicatorTokens.TrackActiveSpace 857 | 858 | /** Default indicator track gap size for a circular progress indicator. */ 859 | @Suppress("OPT_IN_MARKER_ON_WRONG_TARGET") 860 | @get:ExperimentalMaterial3Api 861 | @ExperimentalMaterial3Api 862 | val CircularIndicatorTrackGapSize: Dp = CircularProgressIndicatorTokens.TrackActiveSpace 863 | 864 | /** 865 | * The default [AnimationSpec] that should be used when animating between progress in a 866 | * determinate progress indicator. 867 | */ 868 | val ProgressAnimationSpec = 869 | SpringSpec( 870 | dampingRatio = Spring.DampingRatioNoBouncy, 871 | stiffness = Spring.StiffnessVeryLow, 872 | // The default threshold is 0.01, or 1% of the overall progress range, which is quite 873 | // large and noticeable. We purposefully choose a smaller threshold. 874 | visibilityThreshold = 1 / 1000f 875 | ) 876 | 877 | /** 878 | * Draws the stop indicator at the end of the track. 879 | * 880 | * @param drawScope the [DrawScope] 881 | * @param stopSize size of this stop indicator, it cannot be bigger than the track's height 882 | * @param color color of this stop indicator 883 | * @param strokeCap stroke cap to use for the ends of this stop indicator 884 | */ 885 | fun drawStopIndicator( 886 | drawScope: DrawScope, 887 | stopSize: Dp, 888 | color: Color, 889 | strokeCap: StrokeCap, 890 | ) { 891 | with(drawScope) { 892 | val adjustedStopSize = 893 | min(stopSize.toPx(), size.height) // Stop can't be bigger than track 894 | val stopOffset = (size.height - adjustedStopSize) / 2 // Offset from end 895 | if (strokeCap == StrokeCap.Round) { 896 | drawCircle( 897 | color = color, 898 | radius = adjustedStopSize / 2f, 899 | center = 900 | Offset( 901 | x = size.width - (adjustedStopSize / 2f) - stopOffset, 902 | y = size.height / 2f 903 | ) 904 | ) 905 | } else { 906 | drawRect( 907 | color = color, 908 | topLeft = 909 | Offset( 910 | x = size.width - adjustedStopSize - stopOffset, 911 | y = (size.height - adjustedStopSize) / 2f 912 | ), 913 | size = Size(width = adjustedStopSize, height = adjustedStopSize) 914 | ) 915 | } 916 | } 917 | } 918 | } 919 | 920 | /** A global animation spec for indeterminate circular progress indicator. */ 921 | internal val circularIndeterminateGlobalRotationAnimationSpec 922 | get() = 923 | infiniteRepeatable( 924 | animation = tween(CircularAnimationProgressDuration, easing = LinearEasing) 925 | ) 926 | 927 | /** 928 | * An animation spec for indeterminate circular progress indicators that infinitely rotates a 360 929 | * degrees. 930 | */ 931 | internal val circularIndeterminateRotationAnimationSpec 932 | get() = 933 | infiniteRepeatable( 934 | animation = 935 | keyframes { 936 | durationMillis = CircularAnimationProgressDuration // 6000ms 937 | 90f at 938 | CircularAnimationAdditionalRotationDuration using 939 | MotionTokens.EasingEmphasizedDecelerateCubicBezier // 300ms 940 | 90f at CircularAnimationAdditionalRotationDelay // hold till 1500ms 941 | 180f at 942 | CircularAnimationAdditionalRotationDuration + 943 | CircularAnimationAdditionalRotationDelay // 1800ms 944 | 180f at CircularAnimationAdditionalRotationDelay * 2 // hold till 3000ms 945 | 270f at 946 | CircularAnimationAdditionalRotationDuration + 947 | CircularAnimationAdditionalRotationDelay * 2 // 3300ms 948 | 270f at CircularAnimationAdditionalRotationDelay * 3 // hold till 4500ms 949 | 360f at 950 | CircularAnimationAdditionalRotationDuration + 951 | CircularAnimationAdditionalRotationDelay * 3 // 4800ms 952 | 360f at CircularAnimationProgressDuration // hold till 6000ms 953 | } 954 | ) 955 | 956 | /** An animation spec for indeterminate circular progress indicators progress motion. */ 957 | internal val circularIndeterminateProgressAnimationSpec 958 | get() = 959 | infiniteRepeatable( 960 | animation = 961 | keyframes { 962 | durationMillis = CircularAnimationProgressDuration // 6000ms 963 | CircularIndeterminateMaxProgress at 964 | CircularAnimationProgressDuration / 2 using 965 | CircularProgressEasing // 3000ms 966 | CircularIndeterminateMinProgress at CircularAnimationProgressDuration 967 | } 968 | ) 969 | 970 | /** An animation spec for indeterminate linear progress indicator first line head position. */ 971 | internal val linearIndeterminateFirstLineHeadAnimationSpec 972 | get() = 973 | infiniteRepeatable( 974 | animation = 975 | keyframes { 976 | durationMillis = LinearAnimationDuration 977 | 0f at FirstLineHeadDelay using LinearIndeterminateProgressEasing 978 | 1f at FirstLineHeadDuration + FirstLineHeadDelay 979 | } 980 | ) 981 | 982 | /** An animation spec for indeterminate linear progress indicator first line tail position. */ 983 | internal val linearIndeterminateFirstLineTailAnimationSpec 984 | get() = 985 | infiniteRepeatable( 986 | animation = 987 | keyframes { 988 | durationMillis = LinearAnimationDuration 989 | 0f at FirstLineTailDelay using LinearIndeterminateProgressEasing 990 | 1f at FirstLineTailDuration + FirstLineTailDelay 991 | } 992 | ) 993 | 994 | /** An animation spec for indeterminate linear progress indicator second line head position. */ 995 | internal val linearIndeterminateSecondLineHeadAnimationSpec 996 | get() = 997 | infiniteRepeatable( 998 | animation = 999 | keyframes { 1000 | durationMillis = LinearAnimationDuration 1001 | 0f at SecondLineHeadDelay using LinearIndeterminateProgressEasing 1002 | 1f at SecondLineHeadDuration + SecondLineHeadDelay 1003 | } 1004 | ) 1005 | 1006 | /** An animation spec for indeterminate linear progress indicator second line tail position. */ 1007 | internal val linearIndeterminateSecondLineTailAnimationSpec 1008 | get() = 1009 | infiniteRepeatable( 1010 | animation = 1011 | keyframes { 1012 | durationMillis = LinearAnimationDuration 1013 | 0f at SecondLineTailDelay using LinearIndeterminateProgressEasing 1014 | 1f at SecondLineTailDuration + SecondLineTailDelay 1015 | } 1016 | ) 1017 | // LinearProgressIndicator Material specs 1018 | 1019 | // Width is given in the spec but not defined as a token. 1020 | /*@VisibleForTesting*/ 1021 | internal val LinearIndicatorWidth = 240.dp 1022 | 1023 | /*@VisibleForTesting*/ 1024 | internal val LinearIndicatorHeight = LinearProgressIndicatorTokens.Height 1025 | 1026 | // CircularProgressIndicator Material specs 1027 | // Diameter of the indicator circle 1028 | /*@VisibleForTesting*/ 1029 | internal val CircularIndicatorDiameter = CircularProgressIndicatorTokens.Size 1030 | 1031 | // Indeterminate linear indicator transition specs 1032 | 1033 | // Total duration for one linear cycle 1034 | internal const val LinearAnimationDuration = 1750 1035 | 1036 | // Duration of the head and tail animations for both lines 1037 | internal const val FirstLineHeadDuration = 1000 1038 | internal const val FirstLineTailDuration = 1000 1039 | internal const val SecondLineHeadDuration = 850 1040 | internal const val SecondLineTailDuration = 850 1041 | 1042 | // Delay before the start of the head and tail animations for both lines 1043 | internal const val FirstLineHeadDelay = 0 1044 | internal const val FirstLineTailDelay = 250 1045 | internal const val SecondLineHeadDelay = 650 1046 | internal const val SecondLineTailDelay = 900 1047 | 1048 | internal val LinearIndeterminateProgressEasing = MotionTokens.EasingEmphasizedAccelerateCubicBezier 1049 | 1050 | // Indeterminate circular indicator transition specs 1051 | 1052 | // The indeterminate circular indicator easing constants for its motion 1053 | internal val CircularProgressEasing = MotionTokens.EasingStandardCubicBezier 1054 | internal const val CircularIndeterminateMinProgress = 0.1f 1055 | internal const val CircularIndeterminateMaxProgress = 0.87f 1056 | 1057 | internal const val CircularAnimationProgressDuration = 6000 1058 | internal const val CircularAnimationAdditionalRotationDelay = 1500 1059 | internal const val CircularAnimationAdditionalRotationDuration = 300 1060 | internal const val CircularAdditionalRotationDegreesTarget = 360f 1061 | internal const val CircularGlobalRotationDegreesTarget = 1080f 1062 | -------------------------------------------------------------------------------- /lib-components/src/main/java/bakuen/wear/components/material3/internal/AccessibilityUtil.kt: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2024 The Android Open Source Project 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | package bakuen.wear.components.material3.internal 18 | 19 | import androidx.annotation.VisibleForTesting 20 | import androidx.compose.foundation.layout.padding 21 | import androidx.compose.ui.Modifier 22 | import androidx.compose.ui.layout.layout 23 | import androidx.compose.ui.semantics.semantics 24 | import androidx.compose.ui.unit.Dp 25 | import androidx.compose.ui.unit.dp 26 | import androidx.compose.ui.unit.offset 27 | 28 | @VisibleForTesting internal val HorizontalSemanticsBoundsPadding: Dp = 10.dp 29 | @VisibleForTesting internal val VerticalSemanticsBoundsPadding: Dp = 10.dp 30 | 31 | /** 32 | * Increases the semantics bounds horizontally by [HorizontalSemanticsBoundsPadding] in order to 33 | * meet the TalkBack box minimum size while preserving the visual appearance. 34 | */ 35 | internal val IncreaseHorizontalSemanticsBounds: Modifier = 36 | Modifier.layout { measurable, constraints -> 37 | val paddingPx = HorizontalSemanticsBoundsPadding.roundToPx() 38 | // We need to add horizontal padding to the semantics bounds in order to meet 39 | // screenreader green box minimum size, but we also want to 40 | // preserve a visual appearance and layout size below that minimum 41 | // in order to maintain backwards compatibility. This custom 42 | // layout effectively implements "negative padding". 43 | val newConstraint = constraints.offset(paddingPx * 2, 0) 44 | val placeable = measurable.measure(newConstraint) 45 | 46 | // But when actually placing the placeable, create the layout without additional 47 | // space. Place the placeable where it would've been without any extra padding. 48 | val height = placeable.height 49 | val width = placeable.width - paddingPx * 2 50 | layout(width, height) { placeable.place(-paddingPx, 0) } 51 | } 52 | .semantics(mergeDescendants = true) {} 53 | .padding(horizontal = HorizontalSemanticsBoundsPadding) 54 | 55 | /** 56 | * Increases the semantics bounds vertically by [VerticalSemanticsBoundsPadding] in order to meet 57 | * the TalkBack box minimum size while preserving the visual appearance. 58 | */ 59 | internal val IncreaseVerticalSemanticsBounds: Modifier = 60 | Modifier.layout { measurable, constraints -> 61 | val paddingPx = VerticalSemanticsBoundsPadding.roundToPx() 62 | // We need to add vertical padding to the semantics bounds in order to meet 63 | // screenreader green box minimum size, but we also want to 64 | // preserve a visual appearance and layout size below that minimum 65 | // in order to maintain backwards compatibility. This custom 66 | // layout effectively implements "negative padding". 67 | val newConstraint = constraints.offset(0, paddingPx * 2) 68 | val placeable = measurable.measure(newConstraint) 69 | 70 | // But when actually placing the placeable, create the layout without additional 71 | // space. Place the placeable where it would've been without any extra padding. 72 | val height = placeable.height - paddingPx * 2 73 | val width = placeable.width 74 | layout(width, height) { placeable.place(0, -paddingPx) } 75 | } 76 | .semantics(mergeDescendants = true) {} 77 | .padding(vertical = VerticalSemanticsBoundsPadding) 78 | -------------------------------------------------------------------------------- /lib-components/src/main/java/bakuen/wear/components/material3/tokens/CircularProgressIndicatorTokens.kt: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2024 The Android Open Source Project 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | // VERSION: v0_7_0 17 | // GENERATED CODE - DO NOT MODIFY BY HAND 18 | 19 | package androidx.compose.material3.tokens 20 | 21 | import androidx.compose.ui.unit.dp 22 | 23 | internal object CircularProgressIndicatorTokens { 24 | val ActiveThickness = 4.0.dp 25 | val ActiveWaveAmplitude = 1.6.dp 26 | val ActiveWaveWavelength = 15.0.dp 27 | val Size = 40.0.dp 28 | val TrackActiveSpace = 4.0.dp 29 | val TrackThickness = 4.0.dp 30 | val WaveSize = 48.0.dp 31 | } 32 | -------------------------------------------------------------------------------- /lib-components/src/main/java/bakuen/wear/components/material3/tokens/ColorSchemeKeyTokens.kt: -------------------------------------------------------------------------------- 1 | package bakuen.wear.components.material3.tokens 2 | 3 | import bakuen.wear.components.Theme 4 | 5 | object ColorSchemeKeyTokens { 6 | val Primary get() = Theme.color.primary 7 | val SecondaryContainer get() = Theme.color.onSurfaceVariant 8 | } 9 | -------------------------------------------------------------------------------- /lib-components/src/main/java/bakuen/wear/components/material3/tokens/LinearProgressIndicatorTokens.kt: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2024 The Android Open Source Project 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | // VERSION: v0_7_0 17 | // GENERATED CODE - DO NOT MODIFY BY HAND 18 | 19 | package bakuen.wear.components.material3.tokens 20 | 21 | import androidx.compose.ui.unit.dp 22 | 23 | internal object LinearProgressIndicatorTokens { 24 | val ActiveThickness = 4.0.dp 25 | val ActiveWaveAmplitude = 3.0.dp 26 | val ActiveWaveWavelength = 40.0.dp 27 | val Height = 4.0.dp 28 | val IndeterminateActiveWaveWavelength = 20.0.dp 29 | val StopSize = 4.0.dp 30 | val StopTrailingSpace = 0.0.dp 31 | val TrackActiveSpace = 4.0.dp 32 | val TrackThickness = 4.0.dp 33 | val WaveHeight = 10.0.dp 34 | } 35 | -------------------------------------------------------------------------------- /lib-components/src/main/java/bakuen/wear/components/material3/tokens/MotionTokens.kt: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2022 The Android Open Source Project 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | // VERSION: v0_103 17 | // GENERATED CODE - DO NOT MODIFY BY HAND 18 | 19 | package bakuen.wear.components.material3.tokens 20 | 21 | import androidx.compose.animation.core.CubicBezierEasing 22 | 23 | internal object MotionTokens { 24 | const val DurationExtraLong1 = 700.0 25 | const val DurationExtraLong2 = 800.0 26 | const val DurationExtraLong3 = 900.0 27 | const val DurationExtraLong4 = 1000.0 28 | const val DurationLong1 = 450.0 29 | const val DurationLong2 = 500.0 30 | const val DurationLong3 = 550.0 31 | const val DurationLong4 = 600.0 32 | const val DurationMedium1 = 250.0 33 | const val DurationMedium2 = 300.0 34 | const val DurationMedium3 = 350.0 35 | const val DurationMedium4 = 400.0 36 | const val DurationShort1 = 50.0 37 | const val DurationShort2 = 100.0 38 | const val DurationShort3 = 150.0 39 | const val DurationShort4 = 200.0 40 | val EasingEmphasizedCubicBezier = CubicBezierEasing(0.2f, 0.0f, 0.0f, 1.0f) 41 | val EasingEmphasizedAccelerateCubicBezier = CubicBezierEasing(0.3f, 0.0f, 0.8f, 0.15f) 42 | val EasingEmphasizedDecelerateCubicBezier = CubicBezierEasing(0.05f, 0.7f, 0.1f, 1.0f) 43 | val EasingLegacyCubicBezier = CubicBezierEasing(0.4f, 0.0f, 0.2f, 1.0f) 44 | val EasingLegacyAccelerateCubicBezier = CubicBezierEasing(0.4f, 0.0f, 1.0f, 1.0f) 45 | val EasingLegacyDecelerateCubicBezier = CubicBezierEasing(0.0f, 0.0f, 0.2f, 1.0f) 46 | val EasingLinearCubicBezier = CubicBezierEasing(0.0f, 0.0f, 1.0f, 1.0f) 47 | val EasingStandardCubicBezier = CubicBezierEasing(0.2f, 0.0f, 0.0f, 1.0f) 48 | val EasingStandardAccelerateCubicBezier = CubicBezierEasing(0.3f, 0.0f, 1.0f, 1.0f) 49 | val EasingStandardDecelerateCubicBezier = CubicBezierEasing(0.0f, 0.0f, 0.0f, 1.0f) 50 | } 51 | -------------------------------------------------------------------------------- /lib-components/src/main/java/bakuen/wear/components/material3/tokens/ProgressIndicatorTokens.kt: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2024 The Android Open Source Project 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | // VERSION: v0_4_0 17 | // GENERATED CODE - DO NOT MODIFY BY HAND 18 | 19 | package bakuen.wear.components.material3.tokens 20 | 21 | internal object ProgressIndicatorTokens { 22 | val ActiveIndicatorColor = ColorSchemeKeyTokens.Primary 23 | val ActiveShape = ShapeKeyTokens.CornerFull 24 | val StopColor = ColorSchemeKeyTokens.Primary 25 | val StopShape = ShapeKeyTokens.CornerFull 26 | val TrackColor = ColorSchemeKeyTokens.SecondaryContainer 27 | val TrackShape = ShapeKeyTokens.CornerFull 28 | } 29 | -------------------------------------------------------------------------------- /lib-components/src/main/java/bakuen/wear/components/material3/tokens/ShapeKeyTokens.kt: -------------------------------------------------------------------------------- 1 | package bakuen.wear.components.material3.tokens 2 | 3 | import androidx.compose.foundation.shape.RoundedCornerShape 4 | 5 | object ShapeKeyTokens { 6 | val CornerFull = RoundedCornerShape(100) 7 | } 8 | -------------------------------------------------------------------------------- /lib-components/src/main/java/bakuen/wear/components/wear/Modifier.kt: -------------------------------------------------------------------------------- 1 | package bakuen.wear.components.wear 2 | 3 | import androidx.compose.foundation.ScrollState 4 | import androidx.compose.foundation.focusable 5 | import androidx.compose.foundation.gestures.animateScrollBy 6 | import androidx.compose.foundation.gestures.scrollBy 7 | import androidx.compose.foundation.rememberScrollState 8 | import androidx.compose.foundation.verticalScroll 9 | import androidx.compose.runtime.Composable 10 | import androidx.compose.runtime.rememberCoroutineScope 11 | import androidx.compose.ui.Modifier 12 | import androidx.compose.ui.focus.focusRequester 13 | import androidx.compose.ui.input.rotary.onRotaryScrollEvent 14 | import androidx.wear.compose.foundation.ExperimentalWearFoundationApi 15 | import androidx.wear.compose.foundation.rememberActiveFocusRequester 16 | import kotlinx.coroutines.launch 17 | 18 | @OptIn(ExperimentalWearFoundationApi::class) 19 | @Composable 20 | fun Modifier.verticalRotaryScroll(state: ScrollState): Modifier { 21 | val coroutineScope = rememberCoroutineScope() 22 | return then(Modifier 23 | .verticalScroll(state) 24 | .onRotaryScrollEvent { 25 | coroutineScope.launch { 26 | state.scrollBy(it.verticalScrollPixels) 27 | state.animateScrollBy(0f) 28 | } 29 | true 30 | } 31 | .focusRequester(rememberActiveFocusRequester()) 32 | .focusable()) 33 | } -------------------------------------------------------------------------------- /lib-navigator/.gitignore: -------------------------------------------------------------------------------- 1 | /build -------------------------------------------------------------------------------- /lib-navigator/build.gradle.kts: -------------------------------------------------------------------------------- 1 | plugins { 2 | alias(libs.plugins.android.library) 3 | alias(libs.plugins.jetbrains.kotlin.android) 4 | alias(libs.plugins.compose.compiler) 5 | } 6 | 7 | android { 8 | namespace = "bakuen.lib.navigator" 9 | compileSdk = 34 10 | 11 | defaultConfig { 12 | minSdk = 21 13 | consumerProguardFiles("consumer-rules.pro") 14 | } 15 | 16 | buildTypes { 17 | release { 18 | isMinifyEnabled = false 19 | proguardFiles( 20 | getDefaultProguardFile("proguard-android-optimize.txt"), 21 | "proguard-rules.pro" 22 | ) 23 | } 24 | } 25 | compileOptions { 26 | sourceCompatibility = JavaVersion.VERSION_1_8 27 | targetCompatibility = JavaVersion.VERSION_1_8 28 | } 29 | kotlinOptions { 30 | jvmTarget = "1.8" 31 | } 32 | } 33 | 34 | dependencies { 35 | implementation(platform(libs.androidx.compose.bom)) 36 | implementation(libs.androidx.activity.compose) 37 | implementation(libs.androidx.compose.ui) 38 | implementation(libs.androidx.compose.animation) 39 | implementation(libs.androidx.compose.foundation) 40 | } -------------------------------------------------------------------------------- /lib-navigator/consumer-rules.pro: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/java30433/TaskManager/1bca47ad3cba310723929a231a5e7cb4d4dc9434/lib-navigator/consumer-rules.pro -------------------------------------------------------------------------------- /lib-navigator/proguard-rules.pro: -------------------------------------------------------------------------------- 1 | # Add project specific ProGuard rules here. 2 | # You can control the set of applied configuration files using the 3 | # proguardFiles setting in build.gradle. 4 | # 5 | # For more details, see 6 | # http://developer.android.com/guide/developing/tools/proguard.html 7 | 8 | # If your project uses WebView with JS, uncomment the following 9 | # and specify the fully qualified class name to the JavaScript interface 10 | # class: 11 | #-keepclassmembers class fqcn.of.javascript.interface.for.webview { 12 | # public *; 13 | #} 14 | 15 | # Uncomment this to preserve the line number information for 16 | # debugging stack traces. 17 | #-keepattributes SourceFile,LineNumberTable 18 | 19 | # If you keep the line number information, uncomment this to 20 | # hide the original source file name. 21 | #-renamesourcefileattribute SourceFile -------------------------------------------------------------------------------- /lib-navigator/src/main/AndroidManifest.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /lib-navigator/src/main/java/bakuen/lib/navigator/ContentNode.kt: -------------------------------------------------------------------------------- 1 | package bakuen.lib.navigator 2 | 3 | import androidx.compose.runtime.Composable 4 | import androidx.compose.runtime.getValue 5 | import androidx.compose.runtime.mutableStateOf 6 | import androidx.compose.runtime.setValue 7 | 8 | open class ContentNode internal constructor( 9 | internal val content: @Composable () -> Unit, 10 | internal val key: Any? = null, 11 | ) { 12 | var enableSwipeBack by mutableStateOf(true) 13 | } -------------------------------------------------------------------------------- /lib-navigator/src/main/java/bakuen/lib/navigator/Dialog.kt: -------------------------------------------------------------------------------- 1 | package bakuen.lib.navigator 2 | 3 | import android.content.res.Resources 4 | import androidx.compose.animation.AnimatedVisibility 5 | import androidx.compose.animation.EnterTransition 6 | import androidx.compose.animation.fadeIn 7 | import androidx.compose.animation.slideInVertically 8 | import androidx.compose.animation.slideOutHorizontally 9 | import androidx.compose.runtime.Composable 10 | import androidx.compose.runtime.CompositionLocalProvider 11 | import androidx.compose.runtime.LaunchedEffect 12 | import androidx.compose.runtime.mutableStateListOf 13 | import androidx.compose.runtime.staticCompositionLocalOf 14 | import androidx.compose.ui.Modifier 15 | import androidx.compose.ui.unit.dp 16 | 17 | class DialogNode( 18 | content: @Composable () -> Unit, 19 | key: Any? = null 20 | ) : ContentNode(content, key) { 21 | fun dismiss() { 22 | DialogMan.dialogs.remove(this) 23 | } 24 | } 25 | 26 | val LocalDialog = staticCompositionLocalOf { error("") } 27 | 28 | object DialogMan { 29 | internal val dialogs = mutableStateListOf() 30 | 31 | fun push(key: Any? = null, content: @Composable () -> Unit) { 32 | dialogs.add(DialogNode(content = { 33 | val dialog = LocalDialog.current 34 | SwipeToDismiss(velocity = 16.dp, enabled = dialog.enableSwipeBack, onDismiss = { dialog.dismiss() }) { 35 | content() 36 | } 37 | }, key = key)) 38 | } 39 | 40 | @Composable 41 | internal fun Dialogs() { 42 | dialogs.forEach { node -> 43 | CompositionLocalProvider( 44 | LocalDialog provides node, 45 | content = node.content 46 | ) 47 | } 48 | } 49 | } -------------------------------------------------------------------------------- /lib-navigator/src/main/java/bakuen/lib/navigator/EventChannel.kt: -------------------------------------------------------------------------------- 1 | package bakuen.lib.navigator 2 | 3 | import androidx.compose.runtime.Composable 4 | import androidx.compose.runtime.collectAsState 5 | import kotlinx.coroutines.channels.Channel 6 | import kotlinx.coroutines.flow.receiveAsFlow 7 | import kotlinx.coroutines.runBlocking 8 | 9 | class EventChannel { 10 | private val channel = Channel(capacity = Channel.BUFFERED) 11 | private val flow = channel.receiveAsFlow() 12 | fun emit(value: T) { 13 | runBlocking { 14 | channel.send(value) 15 | } 16 | } 17 | @Composable 18 | fun consumeAsState() = flow.collectAsState(initial = null) 19 | } -------------------------------------------------------------------------------- /lib-navigator/src/main/java/bakuen/lib/navigator/NavigationEvent.kt: -------------------------------------------------------------------------------- 1 | package bakuen.lib.navigator 2 | 3 | sealed class NavigationEvent { 4 | data object Backward : NavigationEvent() 5 | class Forward(val next: ContentNode) : NavigationEvent() 6 | class Replace(val target: ContentNode) : NavigationEvent() 7 | } -------------------------------------------------------------------------------- /lib-navigator/src/main/java/bakuen/lib/navigator/NavigationWrapper.kt: -------------------------------------------------------------------------------- 1 | package bakuen.lib.navigator 2 | 3 | import androidx.compose.animation.core.Animatable 4 | import androidx.compose.animation.core.FloatExponentialDecaySpec 5 | import androidx.compose.foundation.gestures.Orientation 6 | import androidx.compose.foundation.gestures.draggable 7 | import androidx.compose.foundation.gestures.rememberDraggableState 8 | import androidx.compose.foundation.layout.Box 9 | import androidx.compose.foundation.layout.BoxWithConstraints 10 | import androidx.compose.foundation.layout.fillMaxSize 11 | import androidx.compose.foundation.layout.offset 12 | import androidx.compose.runtime.Composable 13 | import androidx.compose.runtime.CompositionLocalProvider 14 | import androidx.compose.runtime.LaunchedEffect 15 | import androidx.compose.runtime.remember 16 | import androidx.compose.runtime.rememberCoroutineScope 17 | import androidx.compose.ui.Modifier 18 | import androidx.compose.ui.unit.IntOffset 19 | import kotlinx.coroutines.launch 20 | import kotlinx.coroutines.runBlocking 21 | 22 | @Composable 23 | internal fun NavigationWrapper(event: NavigationEvent?) { 24 | BoxWithConstraints( 25 | modifier = Modifier.fillMaxSize() 26 | ) { 27 | val maxOffset = constraints.maxWidth.toFloat() 28 | val swipeOffset = remember { Animatable(0f) } 29 | LaunchedEffect(event) { 30 | launch { 31 | when (event) { 32 | is NavigationEvent.Backward -> { 33 | swipeOffset.animateTo(maxOffset) 34 | Navigator.nodeBackward() 35 | swipeOffset.snapTo(0f) 36 | } 37 | 38 | is NavigationEvent.Forward -> { 39 | Navigator.nodeForward(event.next) 40 | swipeOffset.snapTo(maxOffset) 41 | swipeOffset.animateTo(0f) 42 | } 43 | 44 | is NavigationEvent.Replace -> { 45 | Navigator.nodeForward(event.target) 46 | swipeOffset.snapTo(maxOffset) 47 | swipeOffset.animateTo(0f) 48 | screens.removeAt(screens.lastIndex - 1) 49 | } 50 | 51 | else -> {} 52 | } 53 | } 54 | } 55 | val scope = rememberCoroutineScope() 56 | Box( 57 | modifier = Modifier 58 | .fillMaxSize() 59 | .run { 60 | if (screens.last().enableSwipeBack) { 61 | draggable( 62 | orientation = Orientation.Horizontal, 63 | state = rememberDraggableState { 64 | scope.launch { 65 | swipeOffset.snapTo((swipeOffset.value + it).coerceAtLeast(0f)) 66 | } 67 | }, 68 | onDragStopped = { velocity -> 69 | val targetValue = 70 | if (FloatExponentialDecaySpec().getTargetValue( 71 | swipeOffset.value, velocity 72 | ) > maxOffset / 2 73 | ) maxOffset else 0f 74 | swipeOffset.animateTo(targetValue) 75 | if (targetValue != 0f) { 76 | Navigator.nodeBackward() 77 | swipeOffset.snapTo(0f) 78 | } 79 | } 80 | ) 81 | } else this 82 | } 83 | ) { 84 | screens.forEachIndexed { index, screen -> 85 | stateHolder.SaveableStateProvider(key = screen.hashCode()) { 86 | if (index >= screens.size - 2) { 87 | Box(Modifier.run { 88 | when (index) { 89 | screens.size - 2 -> { 90 | this.offset { 91 | IntOffset( 92 | x = ((-maxOffset + swipeOffset.value * 1f) * 0.3f).toInt(), 93 | y = 0 94 | ) 95 | } 96 | } 97 | 98 | screens.size - 1 -> { 99 | this.offset { 100 | IntOffset( 101 | x = swipeOffset.value.toInt(), 102 | y = 0 103 | ) 104 | } 105 | } 106 | 107 | else -> this 108 | } 109 | }) { 110 | CompositionLocalProvider(LocalScreen provides screen) { 111 | screenRoot(screen.content) 112 | } 113 | } 114 | } 115 | } 116 | } 117 | } 118 | } 119 | } -------------------------------------------------------------------------------- /lib-navigator/src/main/java/bakuen/lib/navigator/Navigator.kt: -------------------------------------------------------------------------------- 1 | package bakuen.lib.navigator 2 | 3 | import android.annotation.SuppressLint 4 | import android.app.Activity 5 | import androidx.activity.compose.BackHandler 6 | import androidx.activity.compose.LocalOnBackPressedDispatcherOwner 7 | import androidx.compose.runtime.Composable 8 | import androidx.compose.runtime.LaunchedEffect 9 | import androidx.compose.runtime.getValue 10 | import androidx.compose.runtime.mutableStateListOf 11 | import androidx.compose.runtime.mutableStateOf 12 | import androidx.compose.runtime.saveable.SaveableStateHolder 13 | import androidx.compose.runtime.saveable.rememberSaveable 14 | import androidx.compose.runtime.saveable.rememberSaveableStateHolder 15 | import androidx.compose.runtime.setValue 16 | import androidx.compose.runtime.staticCompositionLocalOf 17 | import androidx.compose.ui.platform.LocalContext 18 | import bakuen.lib.navigator.Navigator.ReduceRecompose 19 | import bakuen.lib.navigator.Navigator.event 20 | 21 | val LocalScreen = staticCompositionLocalOf { error("") } 22 | internal lateinit var stateHolder: SaveableStateHolder 23 | internal lateinit var screenRoot: @Composable (currentScreen: @Composable () -> Unit) -> Unit 24 | internal val screens = mutableStateListOf() 25 | 26 | object Navigator { 27 | 28 | internal var event = EventChannel() 29 | 30 | @SuppressLint("Range") 31 | internal fun nodeBackward() { 32 | screens.removeLastOrNull() 33 | } 34 | 35 | internal fun nodeForward(node: ContentNode) { 36 | screens.add(node) 37 | } 38 | 39 | fun navigateBack(): Boolean { 40 | if (screens.size >= 2) { 41 | event.emit(NavigationEvent.Backward) 42 | return true 43 | } else return false 44 | } 45 | 46 | fun forward(key: Any? = null, screen: @Composable () -> Unit) { 47 | if (screens.last().key?.equals(key) == true) return 48 | event.emit(NavigationEvent.Forward(ContentNode(screen, key))) 49 | } 50 | 51 | fun replaceTop(screen: @Composable () -> Unit) { 52 | event.emit(NavigationEvent.Replace(ContentNode(screen))) 53 | } 54 | 55 | @Composable 56 | internal fun ReduceRecompose(content: @Composable () -> Unit) { 57 | content() 58 | } 59 | } 60 | 61 | @Composable 62 | fun NavHost( 63 | initScreen: @Composable () -> Unit, 64 | screenWrapper: @Composable (currentScreen: @Composable () -> Unit) -> Unit 65 | ) { 66 | stateHolder = rememberSaveableStateHolder() 67 | screenRoot = screenWrapper 68 | var isInit by rememberSaveable { mutableStateOf(true) } 69 | if (isInit) { 70 | screens.clear() 71 | screens.add(ContentNode(initScreen)) 72 | isInit = false 73 | } 74 | ReduceRecompose { 75 | BackHandler(enabled = screens.size > 1) { 76 | Navigator.navigateBack() 77 | } 78 | val ac = (LocalContext.current as? Activity) 79 | LaunchedEffect(screens.size) { 80 | if (screens.size == 0) { 81 | ac?.finish() 82 | } 83 | } 84 | if (screens.isNotEmpty()) { 85 | NavigationWrapper( 86 | event = event.consumeAsState().value 87 | ) 88 | } 89 | DialogMan.Dialogs() 90 | } 91 | } -------------------------------------------------------------------------------- /lib-navigator/src/main/java/bakuen/lib/navigator/SwipeToDismiss.kt: -------------------------------------------------------------------------------- 1 | package bakuen.lib.navigator 2 | 3 | import android.content.res.Resources 4 | import androidx.compose.animation.core.tween 5 | import androidx.compose.animation.rememberSplineBasedDecay 6 | import androidx.compose.foundation.ExperimentalFoundationApi 7 | import androidx.compose.foundation.gestures.AnchoredDraggableState 8 | import androidx.compose.foundation.gestures.DraggableAnchors 9 | import androidx.compose.foundation.gestures.Orientation 10 | import androidx.compose.foundation.gestures.anchoredDraggable 11 | import androidx.compose.foundation.layout.Box 12 | import androidx.compose.foundation.layout.BoxScope 13 | import androidx.compose.foundation.layout.offset 14 | import androidx.compose.runtime.Composable 15 | import androidx.compose.runtime.remember 16 | import androidx.compose.ui.Modifier 17 | import androidx.compose.ui.platform.LocalDensity 18 | import androidx.compose.ui.unit.Dp 19 | import androidx.compose.ui.unit.IntOffset 20 | 21 | private enum class DragAnchors { 22 | Normal, 23 | Dismiss, 24 | } 25 | 26 | @OptIn(ExperimentalFoundationApi::class) 27 | @Composable 28 | fun SwipeToDismiss( 29 | modifier: Modifier = Modifier, 30 | enabled: Boolean = true, 31 | velocity: Dp, 32 | onDismiss: ()->Unit, 33 | reverse: Boolean = false, 34 | content: @Composable BoxScope.()->Unit 35 | ) { 36 | if (enabled) { 37 | val reverseSymbol = if (reverse) -1 else 1 38 | val maxOffset = reverseSymbol * Resources.getSystem().displayMetrics.widthPixels.toFloat() 39 | val density = LocalDensity.current 40 | val decay = rememberSplineBasedDecay() 41 | val state = remember { 42 | AnchoredDraggableState( 43 | initialValue = DragAnchors.Normal, 44 | positionalThreshold = { totalDistance -> 45 | totalDistance * 0.5f 46 | }, 47 | velocityThreshold = { 48 | with(density) { 49 | velocity.toPx() 50 | } 51 | }, 52 | snapAnimationSpec = tween(), 53 | decayAnimationSpec = decay, 54 | anchors = DraggableAnchors { 55 | DragAnchors.Normal at 0f 56 | DragAnchors.Dismiss at maxOffset 57 | } 58 | ) 59 | } 60 | Box( 61 | modifier = modifier 62 | .offset { 63 | val x = state.requireOffset() 64 | if (x == maxOffset) onDismiss() 65 | IntOffset(x = x.toInt(), y = 0) 66 | } 67 | .anchoredDraggable( 68 | state = state, 69 | orientation = Orientation.Horizontal, 70 | ), 71 | content = content 72 | ) 73 | } else Box(modifier = modifier, content = content) 74 | } -------------------------------------------------------------------------------- /lib-protostore/.gitignore: -------------------------------------------------------------------------------- 1 | /build -------------------------------------------------------------------------------- /lib-protostore/build.gradle.kts: -------------------------------------------------------------------------------- 1 | plugins { 2 | alias(libs.plugins.android.library) 3 | alias(libs.plugins.jetbrains.kotlin.android) 4 | alias(libs.plugins.kotlinx.serialization) 5 | alias(libs.plugins.compose.compiler) 6 | } 7 | 8 | android { 9 | namespace = "bakuen.lib.protostore" 10 | compileSdk = 34 11 | 12 | defaultConfig { 13 | minSdk = 21 14 | 15 | testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner" 16 | consumerProguardFiles("consumer-rules.pro") 17 | } 18 | 19 | buildTypes { 20 | release { 21 | isMinifyEnabled = false 22 | proguardFiles( 23 | getDefaultProguardFile("proguard-android-optimize.txt"), 24 | "proguard-rules.pro" 25 | ) 26 | } 27 | } 28 | compileOptions { 29 | sourceCompatibility = JavaVersion.VERSION_1_8 30 | targetCompatibility = JavaVersion.VERSION_1_8 31 | } 32 | kotlinOptions { 33 | jvmTarget = "1.8" 34 | } 35 | } 36 | 37 | dependencies { 38 | implementation(platform(libs.androidx.compose.bom)) 39 | implementation(libs.androidx.compose.ui) 40 | api(libs.kotlinx.serialization.protobuf) 41 | } -------------------------------------------------------------------------------- /lib-protostore/consumer-rules.pro: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/java30433/TaskManager/1bca47ad3cba310723929a231a5e7cb4d4dc9434/lib-protostore/consumer-rules.pro -------------------------------------------------------------------------------- /lib-protostore/proguard-rules.pro: -------------------------------------------------------------------------------- 1 | # Add project specific ProGuard rules here. 2 | # You can control the set of applied configuration files using the 3 | # proguardFiles setting in build.gradle. 4 | # 5 | # For more details, see 6 | # http://developer.android.com/guide/developing/tools/proguard.html 7 | 8 | # If your project uses WebView with JS, uncomment the following 9 | # and specify the fully qualified class name to the JavaScript interface 10 | # class: 11 | #-keepclassmembers class fqcn.of.javascript.interface.for.webview { 12 | # public *; 13 | #} 14 | 15 | # Uncomment this to preserve the line number information for 16 | # debugging stack traces. 17 | #-keepattributes SourceFile,LineNumberTable 18 | 19 | # If you keep the line number information, uncomment this to 20 | # hide the original source file name. 21 | #-renamesourcefileattribute SourceFile -------------------------------------------------------------------------------- /lib-protostore/src/main/AndroidManifest.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 8 | 9 | -------------------------------------------------------------------------------- /lib-protostore/src/main/java/bakuen/lib/protostore/ContextProvider.kt: -------------------------------------------------------------------------------- 1 | package bakuen.lib.protostore 2 | 3 | import android.content.ContentProvider 4 | import android.content.ContentValues 5 | import android.content.Context 6 | import android.database.Cursor 7 | import android.net.Uri 8 | 9 | lateinit var appContext: Context 10 | private set 11 | class ContextProvider : ContentProvider() { 12 | override fun onCreate(): Boolean { 13 | appContext = this.context!! 14 | return true 15 | } 16 | 17 | override fun query( 18 | p0: Uri, 19 | p1: Array?, 20 | p2: String?, 21 | p3: Array?, 22 | p4: String? 23 | ): Cursor? { 24 | return null 25 | } 26 | 27 | override fun getType(p0: Uri): String? { 28 | return null 29 | } 30 | 31 | override fun insert(p0: Uri, p1: ContentValues?): Uri? { 32 | return null 33 | } 34 | 35 | override fun delete(p0: Uri, p1: String?, p2: Array?): Int { 36 | return 0 37 | } 38 | 39 | override fun update(p0: Uri, p1: ContentValues?, p2: String?, p3: Array?): Int { 40 | return 0 41 | } 42 | } -------------------------------------------------------------------------------- /lib-protostore/src/main/java/bakuen/lib/protostore/ProtoStore.kt: -------------------------------------------------------------------------------- 1 | @file:Suppress("UNCHECKED_CAST") 2 | @file:OptIn(ExperimentalSerializationApi::class) 3 | 4 | /** 5 | * 早晚得把这个库重写了 6 | */ 7 | package bakuen.lib.protostore 8 | 9 | import android.content.Context 10 | import androidx.compose.runtime.Composable 11 | import androidx.compose.runtime.LaunchedEffect 12 | import androidx.compose.runtime.MutableState 13 | import androidx.compose.runtime.mutableStateOf 14 | import androidx.compose.runtime.remember 15 | import androidx.compose.ui.platform.LocalInspectionMode 16 | import kotlinx.serialization.ExperimentalSerializationApi 17 | import kotlinx.serialization.KSerializer 18 | import kotlinx.serialization.protobuf.ProtoBuf 19 | import java.io.File 20 | 21 | val protobuf = ProtoBuf { } 22 | class SerializerCache(val serializer: KSerializer, val name: String, var store: MutableState? = null) 23 | val serializerCache = mutableMapOf, SerializerCache<*>>() 24 | interface ProtoStore 25 | 26 | inline fun getSerializer(): SerializerCache { 27 | return serializerCache.getOrPut(T::class.java) { 28 | val companion = T::class.java.getDeclaredField("Companion").get(null) 29 | val serializer = companion.javaClass.getDeclaredMethod("serializer").invoke(companion) as KSerializer 30 | SerializerCache(serializer, serializer.descriptor.serialName) 31 | } as SerializerCache 32 | } 33 | @PublishedApi 34 | internal inline fun getStoreState(): MutableState { 35 | val cache = getSerializer() 36 | if (cache.store == null) { 37 | cache.store = mutableStateOf(protobuf.decodeFromByteArray(cache.serializer, appContext.getFile(cache.name).readBytes())) 38 | } 39 | return cache.store!! 40 | } 41 | inline fun getStore(): T = getStoreState().value 42 | inline fun writeStore(value: T) { 43 | val cache = getSerializer() 44 | if (cache.store == null) cache.store = mutableStateOf(value) 45 | else cache.store!!.value = value 46 | appContext.getFile(cache.name).writeBytes(protobuf.encodeToByteArray(cache.serializer, value)) 47 | } 48 | inline fun setStore(block: (T)->T) { 49 | writeStore(block(getStore())) 50 | } 51 | 52 | @Composable 53 | inline fun rememberStore(preview: T? = null): MutableState { 54 | if (preview != null && LocalInspectionMode.current) return remember { mutableStateOf(preview) } 55 | val state = getStoreState() 56 | LaunchedEffect(state.value) { 57 | writeStore(state.value) 58 | } 59 | return state 60 | } 61 | 62 | fun Context.getFile(name: String): File { 63 | val f = File(filesDir, name) 64 | if (!f.exists()) f.createNewFile() 65 | return f 66 | } -------------------------------------------------------------------------------- /settings.gradle.kts: -------------------------------------------------------------------------------- 1 | pluginManagement { 2 | repositories { 3 | google { 4 | content { 5 | includeGroupByRegex("com\\.android.*") 6 | includeGroupByRegex("com\\.google.*") 7 | includeGroupByRegex("androidx.*") 8 | } 9 | } 10 | mavenCentral() 11 | gradlePluginPortal() 12 | } 13 | } 14 | dependencyResolutionManagement { 15 | repositoriesMode.set(RepositoriesMode.FAIL_ON_PROJECT_REPOS) 16 | repositories { 17 | google() 18 | mavenCentral() 19 | maven("https://jitpack.io") 20 | } 21 | } 22 | 23 | rootProject.name = "TaskManager" 24 | include(":app") 25 | include(":lib-components") 26 | include(":lib-navigator") 27 | include(":lib-protostore") 28 | --------------------------------------------------------------------------------