├── .gitignore ├── CONTRIBUTORS.md ├── FEATURES.md ├── LICENSE ├── README.md ├── app ├── .gitignore ├── build.gradle.kts ├── proguard-rules.pro └── src │ └── main │ ├── AndroidManifest.xml │ ├── icon.png │ ├── java │ └── com │ │ └── corphish │ │ └── quicktools │ │ ├── TextToolsApplication.kt │ │ ├── activities │ │ ├── EvalActivity.kt │ │ ├── FindAndReplaceActivity.kt │ │ ├── MainActivity.kt │ │ ├── NoUIActivity.kt │ │ ├── OnBoardingActivity.kt │ │ ├── OptionsActivity.kt │ │ ├── RouterActivity.kt │ │ ├── SaveTextActivity.kt │ │ ├── SettingsActivity.kt │ │ ├── SimulationActivity.kt │ │ ├── TextCountActivity.kt │ │ ├── TransformActivity.kt │ │ ├── TryOutActivity.kt │ │ └── WUPActivity.kt │ │ ├── data │ │ ├── Constants.kt │ │ └── Result.kt │ │ ├── extensions │ │ └── StringExtensions.kt │ │ ├── features │ │ └── Feature.kt │ │ ├── modules │ │ └── AppModule.kt │ │ ├── repository │ │ ├── ContextMenuOptionsRepository.kt │ │ ├── ContextMenuOptionsRepositoryImpl.kt │ │ ├── SettingsRepository.kt │ │ ├── TextRepository.kt │ │ └── TextRepositoryImpl.kt │ │ ├── text │ │ ├── TextReplacementAction.kt │ │ ├── TextReplacementManager.kt │ │ └── TextTransformer.kt │ │ ├── ui │ │ ├── common │ │ │ ├── AppBars.kt │ │ │ ├── Buttons.kt │ │ │ └── ListDialog.kt │ │ └── theme │ │ │ ├── Color.kt │ │ │ ├── Theme.kt │ │ │ └── Type.kt │ │ ├── utils │ │ └── ClipboardHelper.kt │ │ └── viewmodels │ │ ├── EvalViewModel.kt │ │ ├── MainViewModel.kt │ │ ├── OnBoardingViewModel.kt │ │ ├── OptionsViewModel.kt │ │ ├── SaveTextViewModel.kt │ │ ├── SettingsViewModel.kt │ │ ├── TextCountViewModel.kt │ │ ├── TextReplacementViewModel.kt │ │ ├── TextTransformViewModel.kt │ │ └── WUPViewModel.kt │ └── res │ ├── drawable │ ├── ic_copy.xml │ ├── ic_done.xml │ ├── ic_done_all.xml │ ├── ic_find_and_replace.xml │ ├── ic_launcher_foreground.xml │ ├── ic_next.xml │ ├── ic_numbers.xml │ ├── ic_open_in_new.xml │ ├── ic_previous.xml │ ├── ic_redo.xml │ ├── ic_reset.xml │ ├── ic_save.xml │ ├── ic_text_count.xml │ ├── ic_text_transform.xml │ ├── ic_undo.xml │ └── ic_whatsapp.xml │ ├── mipmap-anydpi │ ├── 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 │ ├── resources.properties │ ├── values-es │ └── strings.xml │ ├── values-ko │ └── strings.xml │ ├── values-pt-rBR │ └── strings.xml │ ├── values │ ├── colors.xml │ ├── font_certs.xml │ ├── ic_launcher_background.xml │ ├── strings.xml │ └── themes.xml │ └── xml │ ├── backup_rules.xml │ └── data_extraction_rules.xml ├── assets ├── eval.gif ├── find_replace.gif ├── save.gif ├── single_options.png ├── text_count.gif ├── text_transform.gif ├── wup_1.gif └── wup_2.gif ├── build.gradle.kts ├── gradle.properties ├── gradle └── wrapper │ ├── gradle-wrapper.jar │ └── gradle-wrapper.properties ├── gradlew ├── gradlew.bat ├── metadata └── en-US │ ├── full_description.txt │ ├── images │ ├── icon.png │ └── phoneScreenshots │ │ ├── 1.png │ │ ├── 2.png │ │ ├── 3.png │ │ ├── 4.png │ │ ├── 5.png │ │ ├── 6.png │ │ ├── 7.png │ │ └── 8.png │ ├── short_description.txt │ └── title.txt └── settings.gradle.kts /.gitignore: -------------------------------------------------------------------------------- 1 | *.iml 2 | .gradle 3 | /local.properties 4 | .idea/ 5 | .DS_Store 6 | /build 7 | /captures 8 | .externalNativeBuild 9 | .cxx 10 | local.properties 11 | *.jks 12 | *.apk 13 | app/release/ 14 | app/singleOption/ 15 | app/multipleOptions/ -------------------------------------------------------------------------------- /CONTRIBUTORS.md: -------------------------------------------------------------------------------- 1 | # Contributors 2 | Shout out to the people who have contributed to this project. You can find the repo contributors [here](https://github.com/corphish/TextTools/graphs/contributors). 3 | 4 | ### Idea contributors 5 | People who have suggested cool ideas that have been added to the app are mentioned below. 6 | 7 | - Raffael (raffael7777@proton.me) 8 | - [boognish-rising](https://github.com/boognish-rising) 9 | - [brimwats](https://github.com/brimwats) 10 | - [AjayRahul1](https://github.com/AjayRahul1) 11 | - [Solovertical](https://github.com/Solovertical) 12 | - [sming](https://github.com/sming) 13 | - [synalice](https://github.com/synalice) 14 | 15 | ### Translators 16 | People who have suggested translations for various languages that have been added in the app are mentioned below. Big thanks to them and if you want to contribute translations, you can do it [here](https://crowdin.com/project/text-tools). 17 | 18 | - [elimirien](https://crowdin.com/profile/elimirien) 19 | - [Pvnk](https://crowdin.com/profile/pvnk) 20 | - [Marcio Andrade](https://crowdin.com/profile/marciozomb13) -------------------------------------------------------------------------------- /FEATURES.md: -------------------------------------------------------------------------------- 1 | # Features 2 | All features shown here were recorded in the `Multiple Option`. UI will slightly vary depending on the variant, but all the functionality and its UX will remain the same. 3 | 4 | ### Text unsaved numbers in WhatsApp 5 | Often we find ourselves needing to text/send file/share location to someone for temporary purposes (if you ask me, I face this a lot in situations where I go to a printing centre where they ask me to share the file that I want to print over WhatsApp, so I have to save their contact, refresh my WhatsApp contact list and then send them). Having a context menu option to directly open a WhatsApp chat from their phone number would save a lot of time and keep your phonebook clean, right? 6 | 7 | ![WhatsApp Example 1](assets/wup_1.gif) 8 | ![WhatsApp Example 2](assets/wup_2.gif) 9 | 10 | ### Evaluate mathematical expressions inline 11 | We may often find ourselves to mathematically calculate something while texting someone (probably while making plans). Having an option to perform math calculations in line without needing to open calculator could save time. 12 | 13 | ![Eval Example](assets/eval.gif) 14 | 15 | ### Transform text 16 | Transform the selected text inline. Supported transformations include changing the case, wrapping it with text and many more. 17 | 18 | ![Transform Example](assets/text_transform.gif) 19 | 20 | ### Text count 21 | Get detailed analysis about character count, word count, letter count and more for the selected text. 22 | 23 | ![Text Count Example](assets/text_count.gif) 24 | 25 | ### Save text 26 | Lets you save the selected text into a file so that it can be accessed later. 27 | 28 | ![Save text Example](assets/save.gif) 29 | 30 | ### Find & Replace 31 | Quickly find certain text and replace them with text of your choice within the selected text. 32 | 33 | ![Find and Replace Example](assets/find_replace.gif) -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Apache License 2 | Version 2.0, January 2004 3 | http://www.apache.org/licenses/ 4 | 5 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 6 | 7 | 1. Definitions. 8 | 9 | "License" shall mean the terms and conditions for use, reproduction, 10 | and distribution as defined by Sections 1 through 9 of this document. 11 | 12 | "Licensor" shall mean the copyright owner or entity authorized by 13 | the copyright owner that is granting the License. 14 | 15 | "Legal Entity" shall mean the union of the acting entity and all 16 | other entities that control, are controlled by, or are under common 17 | control with that entity. For the purposes of this definition, 18 | "control" means (i) the power, direct or indirect, to cause the 19 | direction or management of such entity, whether by contract or 20 | otherwise, or (ii) ownership of fifty percent (50%) or more of the 21 | outstanding shares, or (iii) beneficial ownership of such entity. 22 | 23 | "You" (or "Your") shall mean an individual or Legal Entity 24 | exercising permissions granted by this License. 25 | 26 | "Source" form shall mean the preferred form for making modifications, 27 | including but not limited to software source code, documentation 28 | source, and configuration files. 29 | 30 | "Object" form shall mean any form resulting from mechanical 31 | transformation or translation of a Source form, including but 32 | not limited to compiled object code, generated documentation, 33 | and conversions to other media types. 34 | 35 | "Work" shall mean the work of authorship, whether in Source or 36 | Object form, made available under the License, as indicated by a 37 | copyright notice that is included in or attached to the work 38 | (an example is provided in the Appendix below). 39 | 40 | "Derivative Works" shall mean any work, whether in Source or Object 41 | form, that is based on (or derived from) the Work and for which the 42 | editorial revisions, annotations, elaborations, or other modifications 43 | represent, as a whole, an original work of authorship. For the purposes 44 | of this License, Derivative Works shall not include works that remain 45 | separable from, or merely link (or bind by name) to the interfaces of, 46 | the Work and Derivative Works thereof. 47 | 48 | "Contribution" shall mean any work of authorship, including 49 | the original version of the Work and any modifications or additions 50 | to that Work or Derivative Works thereof, that is intentionally 51 | submitted to Licensor for inclusion in the Work by the copyright owner 52 | or by an individual or Legal Entity authorized to submit on behalf of 53 | the copyright owner. For the purposes of this definition, "submitted" 54 | means any form of electronic, verbal, or written communication sent 55 | to the Licensor or its representatives, including but not limited to 56 | communication on electronic mailing lists, source code control systems, 57 | and issue tracking systems that are managed by, or on behalf of, the 58 | Licensor for the purpose of discussing and improving the Work, but 59 | excluding communication that is conspicuously marked or otherwise 60 | designated in writing by the copyright owner as "Not a Contribution." 61 | 62 | "Contributor" shall mean Licensor and any individual or Legal Entity 63 | on behalf of whom a Contribution has been received by Licensor and 64 | subsequently incorporated within the Work. 65 | 66 | 2. Grant of Copyright License. Subject to the terms and conditions of 67 | this License, each Contributor hereby grants to You a perpetual, 68 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 69 | copyright license to reproduce, prepare Derivative Works of, 70 | publicly display, publicly perform, sublicense, and distribute the 71 | Work and such Derivative Works in Source or Object form. 72 | 73 | 3. Grant of Patent License. Subject to the terms and conditions of 74 | this License, each Contributor hereby grants to You a perpetual, 75 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 76 | (except as stated in this section) patent license to make, have made, 77 | use, offer to sell, sell, import, and otherwise transfer the Work, 78 | where such license applies only to those patent claims licensable 79 | by such Contributor that are necessarily infringed by their 80 | Contribution(s) alone or by combination of their Contribution(s) 81 | with the Work to which such Contribution(s) was submitted. If You 82 | institute patent litigation against any entity (including a 83 | cross-claim or counterclaim in a lawsuit) alleging that the Work 84 | or a Contribution incorporated within the Work constitutes direct 85 | or contributory patent infringement, then any patent licenses 86 | granted to You under this License for that Work shall terminate 87 | as of the date such litigation is filed. 88 | 89 | 4. Redistribution. You may reproduce and distribute copies of the 90 | Work or Derivative Works thereof in any medium, with or without 91 | modifications, and in Source or Object form, provided that You 92 | meet the following conditions: 93 | 94 | (a) You must give any other recipients of the Work or 95 | Derivative Works a copy of this License; and 96 | 97 | (b) You must cause any modified files to carry prominent notices 98 | stating that You changed the files; and 99 | 100 | (c) You must retain, in the Source form of any Derivative Works 101 | that You distribute, all copyright, patent, trademark, and 102 | attribution notices from the Source form of the Work, 103 | excluding those notices that do not pertain to any part of 104 | the Derivative Works; and 105 | 106 | (d) If the Work includes a "NOTICE" text file as part of its 107 | distribution, then any Derivative Works that You distribute must 108 | include a readable copy of the attribution notices contained 109 | within such NOTICE file, excluding those notices that do not 110 | pertain to any part of the Derivative Works, in at least one 111 | of the following places: within a NOTICE text file distributed 112 | as part of the Derivative Works; within the Source form or 113 | documentation, if provided along with the Derivative Works; or, 114 | within a display generated by the Derivative Works, if and 115 | wherever such third-party notices normally appear. The contents 116 | of the NOTICE file are for informational purposes only and 117 | do not modify the License. You may add Your own attribution 118 | notices within Derivative Works that You distribute, alongside 119 | or as an addendum to the NOTICE text from the Work, provided 120 | that such additional attribution notices cannot be construed 121 | as modifying the License. 122 | 123 | You may add Your own copyright statement to Your modifications and 124 | may provide additional or different license terms and conditions 125 | for use, reproduction, or distribution of Your modifications, or 126 | for any such Derivative Works as a whole, provided Your use, 127 | reproduction, and distribution of the Work otherwise complies with 128 | the conditions stated in this License. 129 | 130 | 5. Submission of Contributions. Unless You explicitly state otherwise, 131 | any Contribution intentionally submitted for inclusion in the Work 132 | by You to the Licensor shall be under the terms and conditions of 133 | this License, without any additional terms or conditions. 134 | Notwithstanding the above, nothing herein shall supersede or modify 135 | the terms of any separate license agreement you may have executed 136 | with Licensor regarding such Contributions. 137 | 138 | 6. Trademarks. This License does not grant permission to use the trade 139 | names, trademarks, service marks, or product names of the Licensor, 140 | except as required for reasonable and customary use in describing the 141 | origin of the Work and reproducing the content of the NOTICE file. 142 | 143 | 7. Disclaimer of Warranty. Unless required by applicable law or 144 | agreed to in writing, Licensor provides the Work (and each 145 | Contributor provides its Contributions) on an "AS IS" BASIS, 146 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 147 | implied, including, without limitation, any warranties or conditions 148 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A 149 | PARTICULAR PURPOSE. You are solely responsible for determining the 150 | appropriateness of using or redistributing the Work and assume any 151 | risks associated with Your exercise of permissions under this License. 152 | 153 | 8. Limitation of Liability. In no event and under no legal theory, 154 | whether in tort (including negligence), contract, or otherwise, 155 | unless required by applicable law (such as deliberate and grossly 156 | negligent acts) or agreed to in writing, shall any Contributor be 157 | liable to You for damages, including any direct, indirect, special, 158 | incidental, or consequential damages of any character arising as a 159 | result of this License or out of the use or inability to use the 160 | Work (including but not limited to damages for loss of goodwill, 161 | work stoppage, computer failure or malfunction, or any and all 162 | other commercial damages or losses), even if such Contributor 163 | has been advised of the possibility of such damages. 164 | 165 | 9. Accepting Warranty or Additional Liability. While redistributing 166 | the Work or Derivative Works thereof, You may choose to offer, 167 | and charge a fee for, acceptance of support, warranty, indemnity, 168 | or other liability obligations and/or rights consistent with this 169 | License. However, in accepting such obligations, You may act only 170 | on Your own behalf and on Your sole responsibility, not on behalf 171 | of any other Contributor, and only if You agree to indemnify, 172 | defend, and hold each Contributor harmless for any liability 173 | incurred by, or claims asserted against, such Contributor by reason 174 | of your accepting any such warranty or additional liability. 175 | 176 | END OF TERMS AND CONDITIONS 177 | 178 | APPENDIX: How to apply the Apache License to your work. 179 | 180 | To apply the Apache License to your work, attach the following 181 | boilerplate notice, with the fields enclosed by brackets "[]" 182 | replaced with your own identifying information. (Don't include 183 | the brackets!) The text should be enclosed in the appropriate 184 | comment syntax for the file format. We also recommend that a 185 | file or class name and description of purpose be included on the 186 | same "printed page" as the copyright notice for easier 187 | identification within third-party archives. 188 | 189 | Copyright [yyyy] [name of copyright owner] 190 | 191 | Licensed under the Apache License, Version 2.0 (the "License"); 192 | you may not use this file except in compliance with the License. 193 | You may obtain a copy of the License at 194 | 195 | http://www.apache.org/licenses/LICENSE-2.0 196 | 197 | Unless required by applicable law or agreed to in writing, software 198 | distributed under the License is distributed on an "AS IS" BASIS, 199 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 200 | See the License for the specific language governing permissions and 201 | limitations under the License. 202 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Text Tools 2 | 3 | ![GitHub Release](https://img.shields.io/github/v/release/corphish/TextTools) 4 | [![Github All Releases](https://img.shields.io/github/downloads/corphish/TextTools/total.svg)]() 5 | 6 | Collection of useful text related tools that can be accessed from the context menu that appears on text selection. The feature list will keep on growing. Let me know if you have any ideas. 7 | 8 | [Get it on F-Droid](https://f-droid.org/packages/com.corphish.quicktools) 11 | 12 | ### Current Features 13 | - Text unsaved numbers in WhatsApp 14 | - Evaluate mathematical expressions inline 15 | - Transform text 16 | - Text count 17 | - Save text to a file 18 | - Find & Replace in text 19 | 20 | For detailed description of each feature and their demo, please check the [features page](FEATURES.md). 21 | 22 | ### App usage 23 | The above features are provided as context menu options on selected text. Text Tools also lets the user choose how he/she wants the context menu to appear. In other words, users have total control on what appears in the context menu. 24 | 25 | - Users can choose between "Multiple options" and "Single option". 26 | - "Multiple options" will present all the options at once in the context menu. 27 | - "Single option" will present a single option (as "Text Tools") which will then reveal all the options. 28 | - If "Single option" is selected, only relevant options (depending on whether the source text is editable or non-editable) will be shown. 29 | - In all cases, users can choose which options they would want to see in the context. If a certain feature is not useful to an user, he/she can deselect it in the app and that option will not appear in the context menu (for "Multiple options") or in the dialog that presents the supported options (in case of "Single option"). 30 | 31 | ### Development methods 32 | - Developed in Jetpack compose using the latest libraries. 33 | - Use Material design for UI, with color scheme derived using Material You. 34 | - Follows MVVM architecture using ViewModels with StateFlows wherever possible. 35 | - Uses Dependency Injection provided by the Hilt library. 36 | 37 | ### Installation warning 38 | While installing the app (you have to side load), Google Play may block the installation saying it has never seen the app before. While it is true (from Google Play POV), you can proceed with installing the app. If you have doubts about the app being malicious, you are free to verify the same from the source code (it would not be open-source if the app was malicious in the first place, right?). 39 | 40 | ### Issues 41 | Issues may be observed as in the contextual menu options may not be available inside certain apps, or the functionalities may not work as expected. In such cases, kindly raise issue in this Github repo. 42 | 43 | ### Translations 44 | If you would like to contribute to this project by translating the various text shown in the app, you may do so by heading over to the [Crowdin Project](https://crowdin.com/project/text-tools). 45 | 46 | ### Support 47 | If you like my work and would like to support me, you can help to [buy me un gelato](https://www.paypal.com/paypalme/corphish). -------------------------------------------------------------------------------- /app/.gitignore: -------------------------------------------------------------------------------- 1 | /build -------------------------------------------------------------------------------- /app/build.gradle.kts: -------------------------------------------------------------------------------- 1 | plugins { 2 | id("com.android.application") 3 | id("org.jetbrains.kotlin.android") 4 | id("kotlin-kapt") 5 | id("com.google.dagger.hilt.android") 6 | id("org.jetbrains.kotlin.plugin.compose") 7 | } 8 | 9 | val variantMultipleOptions = "multipleOptions" 10 | val variantSingleOption = "singleOption" 11 | 12 | android { 13 | namespace = "com.corphish.quicktools" 14 | compileSdk = 35 15 | 16 | defaultConfig { 17 | applicationId = "com.corphish.quicktools" 18 | minSdk = 30 19 | targetSdk = 35 20 | versionCode = 22 21 | versionName = "2.1.0" 22 | 23 | testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner" 24 | vectorDrawables { 25 | useSupportLibrary = true 26 | } 27 | } 28 | 29 | buildTypes { 30 | release { 31 | isMinifyEnabled = true 32 | proguardFiles( 33 | getDefaultProguardFile("proguard-android-optimize.txt"), 34 | "proguard-rules.pro" 35 | ) 36 | } 37 | } 38 | 39 | compileOptions { 40 | sourceCompatibility = JavaVersion.VERSION_17 41 | targetCompatibility = JavaVersion.VERSION_17 42 | } 43 | kotlinOptions { 44 | jvmTarget = "17" 45 | } 46 | kotlin { 47 | jvmToolchain(17) 48 | } 49 | buildFeatures { 50 | compose = true 51 | buildConfig = true 52 | } 53 | composeOptions { 54 | kotlinCompilerExtensionVersion = "1.4.3" 55 | } 56 | packaging { 57 | resources { 58 | excludes += "/META-INF/{AL2.0,LGPL2.1}" 59 | } 60 | } 61 | 62 | dependenciesInfo { 63 | // Disables dependency metadata when building APKs. 64 | includeInApk = false 65 | // Disables dependency metadata when building Android App Bundles. 66 | includeInBundle = false 67 | } 68 | 69 | androidResources { 70 | generateLocaleConfig = true 71 | } 72 | } 73 | 74 | dependencies { 75 | // Core 76 | implementation("androidx.core:core-ktx:1.16.0") 77 | implementation("androidx.appcompat:appcompat:1.7.0") 78 | implementation("androidx.lifecycle:lifecycle-runtime-ktx:2.9.0") 79 | implementation("androidx.preference:preference-ktx:1.2.1") 80 | 81 | // Material 3 expressive 82 | implementation("androidx.compose.material3:material3-android:1.4.0-alpha15") 83 | implementation("androidx.compose.material:material-icons-extended:1.7.8") 84 | 85 | // Compose 86 | implementation(platform("androidx.compose:compose-bom:2025.05.01")) 87 | implementation("androidx.activity:activity-compose:1.10.1") 88 | implementation("androidx.compose.ui:ui") 89 | implementation("androidx.compose.ui:ui-graphics") 90 | implementation("androidx.compose.ui:ui-tooling-preview") 91 | implementation("androidx.compose.ui:ui-text-google-fonts") 92 | implementation("androidx.lifecycle:lifecycle-viewmodel-compose") 93 | implementation ("androidx.constraintlayout:constraintlayout-compose:1.1.1") 94 | 95 | // To evaluate mathematical expressions 96 | implementation("net.objecthunter:exp4j:0.4.8") 97 | 98 | // Dagger 99 | implementation("com.google.dagger:hilt-android:2.51.1") 100 | implementation("androidx.compose.material3:material3") 101 | kapt("com.google.dagger:hilt-compiler:2.51.1") 102 | 103 | 104 | // Testing 105 | androidTestImplementation(platform("androidx.compose:compose-bom:2025.05.01")) 106 | androidTestImplementation("androidx.compose.ui:ui-test-junit4") 107 | 108 | // Compose debug 109 | debugImplementation("androidx.compose.ui:ui-tooling") 110 | debugImplementation("androidx.compose.ui:ui-test-manifest") 111 | } -------------------------------------------------------------------------------- /app/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 -------------------------------------------------------------------------------- /app/src/main/AndroidManifest.xml: -------------------------------------------------------------------------------- 1 | 2 | 4 | 5 | 18 | 23 | 28 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | 50 | 51 | 52 | 53 | 54 | 55 | 56 | 57 | 58 | 59 | 64 | 69 | 75 | 76 | 84 | 85 | 86 | 87 | 88 | 89 | 90 | 91 | 92 | 93 | 94 | 95 | 96 | 97 | 98 | 99 | 100 | 101 | 107 | 108 | 116 | 117 | 118 | 119 | 120 | 121 | 122 | 123 | 124 | 125 | 126 | 127 | 128 | 129 | 130 | 131 | 132 | 133 | 139 | 140 | 148 | 149 | 150 | 151 | 152 | 153 | 154 | 155 | 156 | 157 | 158 | 159 | 160 | 161 | 162 | 163 | 164 | 165 | 171 | 172 | 180 | 181 | 182 | 183 | 184 | 185 | 186 | 187 | 188 | 189 | 190 | 191 | 192 | 193 | 194 | 195 | 196 | 197 | 203 | 204 | 211 | 212 | 213 | 214 | 215 | 216 | 217 | 218 | 219 | 220 | 221 | 222 | 223 | 224 | 225 | 226 | 227 | 228 | 234 | 235 | 243 | 244 | 245 | 246 | 247 | 248 | 249 | 250 | 251 | 252 | 253 | 254 | 255 | 256 | 257 | 258 | 259 | 260 | 264 | 265 | 266 | -------------------------------------------------------------------------------- /app/src/main/icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/corphish/TextTools/743de19a692652e3f6d050e8d978180be0e66fc6/app/src/main/icon.png -------------------------------------------------------------------------------- /app/src/main/java/com/corphish/quicktools/TextToolsApplication.kt: -------------------------------------------------------------------------------- 1 | package com.corphish.quicktools 2 | 3 | import android.app.Application 4 | import dagger.hilt.android.HiltAndroidApp 5 | 6 | @HiltAndroidApp 7 | class TextToolsApplication: Application() -------------------------------------------------------------------------------- /app/src/main/java/com/corphish/quicktools/activities/EvalActivity.kt: -------------------------------------------------------------------------------- 1 | package com.corphish.quicktools.activities 2 | 3 | import android.content.ClipData 4 | import android.content.ClipboardManager 5 | import android.content.Intent 6 | import android.widget.Toast 7 | import androidx.activity.compose.setContent 8 | import androidx.activity.viewModels 9 | import androidx.compose.foundation.layout.Arrangement 10 | import androidx.compose.foundation.layout.Row 11 | import androidx.compose.foundation.layout.fillMaxWidth 12 | import androidx.compose.foundation.layout.padding 13 | import androidx.compose.material3.Checkbox 14 | import androidx.compose.material3.Text 15 | import androidx.compose.runtime.Composable 16 | import androidx.compose.runtime.mutableStateOf 17 | import androidx.compose.runtime.remember 18 | import androidx.compose.ui.Alignment 19 | import androidx.compose.ui.Modifier 20 | import androidx.compose.ui.res.stringResource 21 | import androidx.compose.ui.unit.dp 22 | import androidx.lifecycle.lifecycleScope 23 | import com.corphish.quicktools.R 24 | import com.corphish.quicktools.data.Constants 25 | import com.corphish.quicktools.data.Result 26 | import com.corphish.quicktools.ui.common.ListDialog 27 | import com.corphish.quicktools.ui.theme.QuickToolsTheme 28 | import com.corphish.quicktools.ui.theme.TypographyV2 29 | import com.corphish.quicktools.utils.ClipboardHelper 30 | import com.corphish.quicktools.viewmodels.EvalViewModel 31 | import com.corphish.quicktools.viewmodels.EvalViewModel.Companion.EVAL_RESULT_APPEND 32 | import com.corphish.quicktools.viewmodels.EvalViewModel.Companion.EVAL_RESULT_COPY_TO_CLIPBOARD 33 | import com.corphish.quicktools.viewmodels.EvalViewModel.Companion.EVAL_RESULT_MODE_ASK_NEXT_TIME 34 | import com.corphish.quicktools.viewmodels.EvalViewModel.Companion.EVAL_RESULT_REPLACE 35 | import com.corphish.quicktools.viewmodels.EvaluateResult 36 | import dagger.hilt.android.AndroidEntryPoint 37 | import kotlinx.coroutines.launch 38 | 39 | @AndroidEntryPoint 40 | class EvalActivity : NoUIActivity() { 41 | // Result generator 42 | private val _replaceResultGenerator: (String, String) -> String = { _, result -> result } 43 | private val _appendResultGenerator: (String, String) -> String = 44 | { input, result -> "$input = $result" } 45 | 46 | private val evalViewModel: EvalViewModel by viewModels() 47 | 48 | override fun handleIntent(intent: Intent): Boolean { 49 | if (intent.hasExtra(Intent.EXTRA_PROCESS_TEXT)) { 50 | val readonly = intent.getBooleanExtra(Intent.EXTRA_PROCESS_TEXT_READONLY, false) 51 | val forceCopy = intent.getBooleanExtra(Constants.INTENT_FORCE_COPY, false) 52 | 53 | if (readonly) { 54 | // We are only interested in editable text 55 | Toast.makeText(this, R.string.editable_error, Toast.LENGTH_LONG).show() 56 | return true 57 | } 58 | 59 | val text = intent.getCharSequenceExtra(Intent.EXTRA_PROCESS_TEXT).toString() 60 | 61 | lifecycleScope.launch { 62 | evalViewModel.shouldForceCopy(forceCopy) 63 | } 64 | 65 | lifecycleScope.launch { 66 | evalViewModel.evalMode.collect { mode -> 67 | when (mode) { 68 | EVAL_RESULT_MODE_ASK_NEXT_TIME -> { 69 | setContent { 70 | UserSelectionDialog( 71 | onSelected = { selectedMode, rememberChoice -> 72 | evalViewModel.denoteModeSelectionByUser(selectedMode) 73 | evalViewModel.denoteUserRememberChoice(rememberChoice) 74 | evalViewModel.evaluate(text) 75 | }, 76 | onDismiss = { finish() } 77 | ) 78 | } 79 | } 80 | 81 | else -> { 82 | evalViewModel.evaluate(text) 83 | } 84 | } 85 | } 86 | } 87 | 88 | lifecycleScope.launch { 89 | evalViewModel.evalResult.collect { result: Result -> 90 | when (result) { 91 | is Result.Success -> { 92 | val str = result.value.resultString 93 | when (val mode = result.value.finalMode) { 94 | EVAL_RESULT_REPLACE, EVAL_RESULT_APPEND -> { 95 | handleEvaluationIntent(text, str, mode) 96 | } 97 | 98 | EVAL_RESULT_COPY_TO_CLIPBOARD -> { 99 | ClipboardHelper.copyToClipboard(this@EvalActivity, str) 100 | } 101 | } 102 | 103 | finish() 104 | } 105 | 106 | Result.Error -> { 107 | Toast.makeText( 108 | this@EvalActivity, 109 | R.string.invalid_expression, 110 | Toast.LENGTH_LONG 111 | ).show() 112 | 113 | finish() 114 | } 115 | 116 | Result.Initial -> { 117 | // Do nothing 118 | } 119 | } 120 | } 121 | } 122 | } 123 | 124 | return false 125 | } 126 | 127 | private fun handleEvaluationIntent(text: String, result: String, mode: Int) { 128 | val resultGenerator: (String, String) -> String = 129 | if (mode == EVAL_RESULT_APPEND) _appendResultGenerator else _replaceResultGenerator 130 | 131 | val resultIntent = Intent() 132 | resultIntent.putExtra(Intent.EXTRA_PROCESS_TEXT, resultGenerator(text, result)) 133 | setResult(RESULT_OK, resultIntent) 134 | } 135 | } 136 | 137 | @Composable 138 | fun UserSelectionDialog( 139 | onSelected: (Int, Boolean) -> Unit, 140 | onDismiss: () -> Unit 141 | ) { 142 | val rememberUserChoiceEnabled = remember { 143 | mutableStateOf(false) 144 | } 145 | 146 | QuickToolsTheme { 147 | ListDialog( 148 | title = stringResource(id = R.string.eval_title_small), 149 | message = stringResource(id = R.string.eval_result_desc), 150 | list = listOf( 151 | R.string.eval_mode_result, 152 | R.string.eval_mode_append, 153 | R.string.eval_mode_copy 154 | ), 155 | supportBack = false, 156 | dismissible = true, 157 | stringSelector = { 158 | stringResource(id = it) 159 | }, 160 | additionalContent = { 161 | Row( 162 | modifier = Modifier 163 | .padding(top = 8.dp) 164 | .fillMaxWidth(), 165 | verticalAlignment = Alignment.CenterVertically, 166 | horizontalArrangement = Arrangement.Center 167 | ) { 168 | 169 | Checkbox( 170 | checked = rememberUserChoiceEnabled.value, 171 | onCheckedChange = { rememberUserChoiceEnabled.value = it }, 172 | enabled = true, 173 | ) 174 | 175 | Text( 176 | text = stringResource(id = R.string.remember_this_choice), 177 | style = TypographyV2.bodyMedium 178 | ) 179 | } 180 | }, 181 | iconSelector = { R.drawable.ic_numbers }, 182 | onItemSelected = { 183 | when (it) { 184 | 0 -> { 185 | onSelected(EVAL_RESULT_REPLACE, rememberUserChoiceEnabled.value) 186 | } 187 | 188 | 1 -> { 189 | onSelected(EVAL_RESULT_APPEND, rememberUserChoiceEnabled.value) 190 | } 191 | 192 | 2 -> { 193 | onSelected(EVAL_RESULT_COPY_TO_CLIPBOARD, rememberUserChoiceEnabled.value) 194 | } 195 | } 196 | }) { onDismiss() } 197 | } 198 | } -------------------------------------------------------------------------------- /app/src/main/java/com/corphish/quicktools/activities/NoUIActivity.kt: -------------------------------------------------------------------------------- 1 | package com.corphish.quicktools.activities 2 | 3 | import android.content.Intent 4 | import android.os.Bundle 5 | import androidx.appcompat.app.AppCompatActivity 6 | 7 | /** 8 | * Base activity class for other feature classes. 9 | */ 10 | abstract class NoUIActivity: AppCompatActivity() { 11 | 12 | /** 13 | * Main intent handling logic goes here that must be overridden by other feature classes. 14 | * @return Boolean value indicating whether the activity should be finished after handling the 15 | * intent or not. 16 | */ 17 | abstract fun handleIntent(intent: Intent): Boolean 18 | 19 | override fun onCreate(savedInstanceState: Bundle?) { 20 | super.onCreate(savedInstanceState) 21 | handleIntentWrapper(intent) 22 | } 23 | 24 | override fun onNewIntent(intent: Intent) { 25 | super.onNewIntent(intent) 26 | handleIntentWrapper(intent) 27 | } 28 | 29 | private fun handleIntentWrapper(intent: Intent) { 30 | if (handleIntent(intent)) 31 | finish() 32 | } 33 | } -------------------------------------------------------------------------------- /app/src/main/java/com/corphish/quicktools/activities/OnBoardingActivity.kt: -------------------------------------------------------------------------------- 1 | package com.corphish.quicktools.activities 2 | 3 | import android.content.Intent 4 | import android.os.Bundle 5 | import androidx.activity.ComponentActivity 6 | import androidx.activity.compose.setContent 7 | import androidx.activity.enableEdgeToEdge 8 | import androidx.activity.viewModels 9 | import androidx.annotation.StringRes 10 | import androidx.compose.foundation.background 11 | import androidx.compose.foundation.layout.Arrangement 12 | import androidx.compose.foundation.layout.Box 13 | import androidx.compose.foundation.layout.Column 14 | import androidx.compose.foundation.layout.PaddingValues 15 | import androidx.compose.foundation.layout.calculateEndPadding 16 | import androidx.compose.foundation.layout.calculateStartPadding 17 | import androidx.compose.foundation.layout.fillMaxSize 18 | import androidx.compose.foundation.layout.fillMaxWidth 19 | import androidx.compose.foundation.layout.padding 20 | import androidx.compose.foundation.layout.size 21 | import androidx.compose.foundation.shape.CircleShape 22 | import androidx.compose.material.icons.Icons 23 | import androidx.compose.material.icons.automirrored.filled.KeyboardArrowRight 24 | import androidx.compose.material.icons.filled.Done 25 | import androidx.compose.material.icons.filled.Settings 26 | import androidx.compose.material3.Card 27 | import androidx.compose.material3.CardColors 28 | import androidx.compose.material3.Icon 29 | import androidx.compose.material3.IconButton 30 | import androidx.compose.material3.MaterialTheme 31 | import androidx.compose.material3.Scaffold 32 | import androidx.compose.material3.Text 33 | import androidx.compose.runtime.Composable 34 | import androidx.compose.runtime.getValue 35 | import androidx.compose.runtime.mutableIntStateOf 36 | import androidx.compose.runtime.mutableStateOf 37 | import androidx.compose.runtime.remember 38 | import androidx.compose.runtime.setValue 39 | import androidx.compose.ui.Alignment 40 | import androidx.compose.ui.Modifier 41 | import androidx.compose.ui.draw.clip 42 | import androidx.compose.ui.res.painterResource 43 | import androidx.compose.ui.res.stringResource 44 | import androidx.compose.ui.text.font.FontWeight 45 | import androidx.compose.ui.text.style.TextAlign 46 | import androidx.compose.ui.tooling.preview.Preview 47 | import androidx.compose.ui.unit.LayoutDirection 48 | import androidx.compose.ui.unit.dp 49 | import com.corphish.quicktools.R 50 | import com.corphish.quicktools.repository.AppMode 51 | import com.corphish.quicktools.ui.theme.BrandFontFamily 52 | import com.corphish.quicktools.ui.theme.QuickToolsTheme 53 | import com.corphish.quicktools.viewmodels.OnBoardingViewModel 54 | import dagger.hilt.android.AndroidEntryPoint 55 | 56 | @AndroidEntryPoint 57 | class OnBoardingActivity : ComponentActivity() { 58 | private val viewModel: OnBoardingViewModel by viewModels() 59 | 60 | private fun switchToMain() { 61 | startActivity(Intent(this, MainActivity::class.java)) 62 | } 63 | 64 | override fun onCreate(savedInstanceState: Bundle?) { 65 | super.onCreate(savedInstanceState) 66 | enableEdgeToEdge() 67 | setContent { 68 | QuickToolsTheme { 69 | Scaffold(modifier = Modifier.fillMaxSize()) { innerPadding -> 70 | OnBoarding( 71 | paddingValues = innerPadding, 72 | onAppModeSelected = { 73 | viewModel.setAppMode(it) 74 | }, 75 | onFinish = { 76 | viewModel.setOnBoardingDone(true) 77 | switchToMain() 78 | finish() 79 | } 80 | ) 81 | } 82 | } 83 | } 84 | } 85 | } 86 | 87 | @Composable 88 | fun OnBoarding( 89 | paddingValues: PaddingValues = PaddingValues(), 90 | onAppModeSelected: (AppMode) -> Unit = {}, 91 | onFinish: () -> Unit = {}, 92 | ) { 93 | var page by remember { mutableIntStateOf(0) } 94 | 95 | when (page) { 96 | 0 -> InitialPage( 97 | paddingValues = paddingValues 98 | ) { page = 1 } 99 | 1 -> ModeSelectionScreen( 100 | paddingValues = paddingValues, 101 | onModeSelected = { onAppModeSelected(it) }, 102 | onFinish = { onFinish() } 103 | ) 104 | } 105 | } 106 | 107 | @Composable 108 | fun InitialPage( 109 | paddingValues: PaddingValues = PaddingValues(), 110 | onNextPressed: () -> Unit = {} 111 | ) { 112 | Column( 113 | modifier = Modifier.fillMaxSize(), 114 | horizontalAlignment = Alignment.CenterHorizontally, 115 | verticalArrangement = Arrangement.Center 116 | ) { 117 | Icon( 118 | painter = painterResource(R.drawable.ic_launcher_foreground), 119 | contentDescription = "", 120 | tint = MaterialTheme.colorScheme.primary, 121 | modifier = Modifier.size(256.dp) 122 | ) 123 | 124 | Text( 125 | text = stringResource(R.string.app_name), 126 | style = MaterialTheme.typography.headlineMedium, 127 | fontFamily = BrandFontFamily, 128 | fontWeight = FontWeight.Bold, 129 | color = MaterialTheme.colorScheme.primary, 130 | ) 131 | 132 | Text( 133 | text = stringResource(R.string.app_desc_short), 134 | style = MaterialTheme.typography.bodyMedium, 135 | modifier = Modifier.padding(horizontal = 16.dp), 136 | textAlign = TextAlign.Center 137 | ) 138 | 139 | IconButton( 140 | onClick = { onNextPressed() }, 141 | modifier = Modifier 142 | .padding(vertical = 32.dp) 143 | .clip(CircleShape) 144 | .size(64.dp) 145 | .background(MaterialTheme.colorScheme.primary) 146 | ) { 147 | Icon( 148 | Icons.AutoMirrored.Filled.KeyboardArrowRight, 149 | contentDescription = "", 150 | tint = MaterialTheme.colorScheme.onPrimary 151 | ) 152 | } 153 | } 154 | } 155 | 156 | @Composable 157 | fun ModeSelectionScreen( 158 | paddingValues: PaddingValues = PaddingValues(), 159 | onModeSelected: (AppMode) -> Unit = {}, 160 | onFinish: () -> Unit = {}, 161 | ) { 162 | var selectedMode by remember { mutableStateOf(AppMode.SINGLE) } 163 | Column( 164 | modifier = Modifier 165 | .padding( 166 | start = paddingValues.calculateStartPadding(LayoutDirection.Ltr).plus(16.dp), 167 | end = paddingValues.calculateEndPadding(LayoutDirection.Ltr).plus(16.dp), 168 | top = paddingValues.calculateTopPadding().plus(16.dp), 169 | bottom = paddingValues.calculateBottomPadding().plus(16.dp) 170 | ) 171 | .fillMaxSize() 172 | ) { 173 | Icon( 174 | Icons.Filled.Settings, 175 | contentDescription = "", 176 | tint = MaterialTheme.colorScheme.primary, 177 | modifier = Modifier.size(64.dp) 178 | ) 179 | 180 | Text( 181 | text = stringResource(R.string.mode_select_title), 182 | style = MaterialTheme.typography.headlineMedium, 183 | fontFamily = BrandFontFamily, 184 | fontWeight = FontWeight.Bold, 185 | color = MaterialTheme.colorScheme.primary, 186 | ) 187 | 188 | Text( 189 | text = stringResource(R.string.mode_select_desc), 190 | style = MaterialTheme.typography.bodyMedium, 191 | modifier = Modifier.padding(bottom = 32.dp) 192 | ) 193 | 194 | ModeCard( 195 | title = R.string.mode_single_title, 196 | desc = R.string.mode_single_desc, 197 | isSelected = selectedMode == AppMode.SINGLE, 198 | onClick = { 199 | selectedMode = AppMode.SINGLE 200 | onModeSelected(AppMode.SINGLE) 201 | } 202 | ) 203 | 204 | ModeCard( 205 | title = R.string.mode_multi_title, 206 | desc = R.string.mode_multi_desc, 207 | isSelected = selectedMode == AppMode.MULTI, 208 | onClick = { 209 | selectedMode = AppMode.MULTI 210 | onModeSelected(AppMode.MULTI) 211 | } 212 | ) 213 | 214 | Box(modifier = Modifier.weight(1f)) 215 | 216 | IconButton( 217 | onClick = { onFinish() }, 218 | modifier = Modifier 219 | .padding(vertical = 16.dp) 220 | .clip(CircleShape) 221 | .size(64.dp) 222 | .background(MaterialTheme.colorScheme.primary) 223 | .align(Alignment.CenterHorizontally) 224 | ) { 225 | Icon( 226 | Icons.Default.Done, 227 | contentDescription = "", 228 | tint = MaterialTheme.colorScheme.onPrimary 229 | ) 230 | } 231 | } 232 | } 233 | 234 | @Composable 235 | fun ModeCard( 236 | @StringRes title: Int, 237 | @StringRes desc: Int, 238 | isSelected: Boolean = false, 239 | onClick: () -> Unit = {}, 240 | ) { 241 | Card( 242 | onClick = { onClick() }, 243 | modifier = Modifier 244 | .fillMaxWidth() 245 | .padding(top = 4.dp), 246 | colors = CardColors( 247 | containerColor = if (isSelected) MaterialTheme.colorScheme.primaryContainer else MaterialTheme.colorScheme.surfaceContainer, 248 | contentColor = if (isSelected) MaterialTheme.colorScheme.onPrimaryContainer else MaterialTheme.colorScheme.onSurface, 249 | disabledContainerColor = MaterialTheme.colorScheme.surface, 250 | disabledContentColor = MaterialTheme.colorScheme.onSurface 251 | ) 252 | ) { 253 | Column(modifier = Modifier.padding(16.dp)) { 254 | Text( 255 | stringResource(title), 256 | fontWeight = FontWeight.Bold, 257 | fontFamily = BrandFontFamily 258 | ) 259 | Text( 260 | stringResource(desc), 261 | style = MaterialTheme.typography.bodySmall, 262 | ) 263 | } 264 | } 265 | } 266 | 267 | @Preview(showBackground = true) 268 | @Composable 269 | fun OnBoardingPreview() { 270 | QuickToolsTheme { 271 | OnBoarding() 272 | } 273 | } -------------------------------------------------------------------------------- /app/src/main/java/com/corphish/quicktools/activities/OptionsActivity.kt: -------------------------------------------------------------------------------- 1 | package com.corphish.quicktools.activities 2 | 3 | import android.app.Activity 4 | import android.content.Intent 5 | import androidx.activity.compose.setContent 6 | import androidx.activity.result.contract.ActivityResultContracts 7 | import androidx.activity.viewModels 8 | import androidx.annotation.DrawableRes 9 | import androidx.annotation.StringRes 10 | import androidx.compose.runtime.collectAsState 11 | import androidx.compose.runtime.getValue 12 | import androidx.compose.runtime.mutableStateOf 13 | import androidx.compose.runtime.remember 14 | import androidx.compose.ui.res.stringResource 15 | import com.corphish.quicktools.R 16 | import com.corphish.quicktools.data.Constants 17 | import com.corphish.quicktools.extensions.truncate 18 | import com.corphish.quicktools.repository.FeatureIds 19 | import com.corphish.quicktools.ui.common.ListDialog 20 | import com.corphish.quicktools.ui.theme.QuickToolsTheme 21 | import com.corphish.quicktools.viewmodels.OptionsViewModel 22 | import dagger.hilt.android.AndroidEntryPoint 23 | 24 | @AndroidEntryPoint 25 | class OptionsActivity : NoUIActivity() { 26 | private val viewModel: OptionsViewModel by viewModels() 27 | 28 | private val _options = listOf( 29 | Option(FeatureIds.WHATSAPP, R.string.whatsapp, R.drawable.ic_whatsapp, WUPActivity::class.java, false), 30 | Option(FeatureIds.EVAL, R.string.eval_title_small, R.drawable.ic_numbers, EvalActivity::class.java, true), 31 | Option(FeatureIds.TRANSFORM, R.string.transform_long, R.drawable.ic_text_transform, TransformActivity::class.java, true), 32 | Option(FeatureIds.TEXT_COUNT, R.string.text_count, R.drawable.ic_text_count, TextCountActivity::class.java, false), 33 | Option(FeatureIds.SAVE_TEXT, R.string.save_text_title, R.drawable.ic_save, SaveTextActivity::class.java, false), 34 | Option(FeatureIds.FIND_AND_REPLACE, R.string.title_activity_find_and_replace, R.drawable.ic_find_and_replace, FindAndReplaceActivity::class.java, true) 35 | ) 36 | 37 | private val router = registerForActivityResult(ActivityResultContracts.StartActivityForResult()) { 38 | if (it.resultCode == RESULT_OK) { 39 | setResult(RESULT_OK, it.data) 40 | } 41 | 42 | finish() 43 | } 44 | 45 | override fun handleIntent(intent: Intent): Boolean { 46 | if (intent.hasExtra(Intent.EXTRA_PROCESS_TEXT)) { 47 | val text = intent.getCharSequenceExtra(Intent.EXTRA_PROCESS_TEXT).toString() 48 | val readonly = intent.getBooleanExtra(Intent.EXTRA_PROCESS_TEXT_READONLY, false) 49 | val forceCopy = intent.getBooleanExtra(Constants.INTENT_FORCE_COPY, false) 50 | setContent { 51 | QuickToolsTheme { 52 | val enabledFeatures by viewModel.enabledFeatures.collectAsState() 53 | val optionDialog = remember { 54 | mutableStateOf(true) 55 | } 56 | 57 | if (optionDialog.value) { 58 | val list = _options.filter { enabledFeatures.contains(it.id) && (!readonly || !it.requiresEditable) } 59 | ListDialog( 60 | title = stringResource(id = R.string.app_name), 61 | message = stringResource(id = R.string.app_single_op, text.truncate()), 62 | list = list, 63 | stringSelector = { stringResource(id = it.optionResourceId) }, 64 | iconSelector = { it.icon }, 65 | onItemSelected = { 66 | val routeIntent = Intent(this, list[it].handlingClass) 67 | routeIntent.putExtra(Intent.EXTRA_PROCESS_TEXT, text) 68 | routeIntent.putExtra(Intent.EXTRA_PROCESS_TEXT_READONLY, readonly) 69 | routeIntent.putExtra(Constants.INTENT_FORCE_COPY, forceCopy) 70 | router.launch(routeIntent) 71 | optionDialog.value = false 72 | }) { 73 | optionDialog.value = false 74 | finish() 75 | } 76 | } 77 | } 78 | } 79 | return false 80 | } 81 | 82 | return true 83 | } 84 | 85 | /** 86 | * Denotes an option. 87 | */ 88 | data class Option( 89 | /** 90 | * Id of the option. 91 | */ 92 | val id: FeatureIds, 93 | 94 | /** 95 | * String resource id of the option. 96 | */ 97 | @StringRes val optionResourceId: Int, 98 | 99 | /** 100 | * Icon of this option 101 | */ 102 | @DrawableRes val icon: Int, 103 | 104 | /** 105 | * Activity that handles the option function. 106 | */ 107 | val handlingClass: Class, 108 | 109 | /** 110 | * Boolean indicating whether this option requires editable 111 | * text input. 112 | */ 113 | val requiresEditable: Boolean 114 | ) 115 | } -------------------------------------------------------------------------------- /app/src/main/java/com/corphish/quicktools/activities/RouterActivity.kt: -------------------------------------------------------------------------------- 1 | package com.corphish.quicktools.activities 2 | 3 | import android.content.Intent 4 | import android.os.Bundle 5 | import androidx.activity.viewModels 6 | import androidx.appcompat.app.AppCompatActivity 7 | import androidx.lifecycle.lifecycleScope 8 | import com.corphish.quicktools.viewmodels.OnBoardingViewModel 9 | import dagger.hilt.android.AndroidEntryPoint 10 | import kotlinx.coroutines.launch 11 | 12 | @AndroidEntryPoint 13 | class RouterActivity : AppCompatActivity() { 14 | private val viewModel: OnBoardingViewModel by viewModels() 15 | 16 | override fun onCreate(savedInstanceState: Bundle?) { 17 | super.onCreate(savedInstanceState) 18 | 19 | lifecycleScope.launch { 20 | viewModel.onBoardingDone.collect { onBoardingDone -> 21 | val intent = if (onBoardingDone) { 22 | Intent(this@RouterActivity, MainActivity::class.java) 23 | } else { 24 | Intent(this@RouterActivity, OnBoardingActivity::class.java) 25 | } 26 | 27 | startActivity(intent) 28 | finish() 29 | } 30 | } 31 | } 32 | } -------------------------------------------------------------------------------- /app/src/main/java/com/corphish/quicktools/activities/SaveTextActivity.kt: -------------------------------------------------------------------------------- 1 | package com.corphish.quicktools.activities 2 | 3 | import android.app.Activity 4 | import android.content.Intent 5 | import android.os.Build 6 | import android.widget.Toast 7 | import androidx.activity.compose.setContent 8 | import androidx.activity.result.contract.ActivityResultContracts 9 | import androidx.activity.viewModels 10 | import androidx.compose.runtime.Composable 11 | import androidx.compose.ui.res.stringResource 12 | import androidx.lifecycle.lifecycleScope 13 | import com.corphish.quicktools.R 14 | import com.corphish.quicktools.data.Result 15 | import com.corphish.quicktools.ui.common.ListDialog 16 | import com.corphish.quicktools.ui.theme.QuickToolsTheme 17 | import com.corphish.quicktools.viewmodels.SaveTextViewModel 18 | import dagger.hilt.android.AndroidEntryPoint 19 | import kotlinx.coroutines.launch 20 | 21 | @AndroidEntryPoint 22 | class SaveTextActivity : NoUIActivity() { 23 | 24 | private val viewmodel: SaveTextViewModel by viewModels() 25 | 26 | override fun handleIntent(intent: Intent): Boolean { 27 | val text = intent.getCharSequenceExtra(Intent.EXTRA_PROCESS_TEXT).toString() 28 | 29 | setContent { 30 | QuickToolsTheme { 31 | SaveTextSelectionDialog( 32 | onProcessNoteSelected = { processNote(text) }, 33 | onProcessFileSelected = { processTextFile(text) }, 34 | onDismiss = { finish() } 35 | ) 36 | } 37 | } 38 | 39 | lifecycleScope.launch { 40 | viewmodel.saveTextStatus.collect { 41 | when (it) { 42 | is Result.Initial -> { 43 | // Do nothing 44 | } 45 | is Result.Error -> { 46 | Toast.makeText(this@SaveTextActivity, R.string.save_text_error, Toast.LENGTH_LONG).show() 47 | finish() 48 | } 49 | is Result.Success -> { 50 | Toast.makeText(this@SaveTextActivity, R.string.save_text_success, Toast.LENGTH_LONG).show() 51 | finish() 52 | } 53 | } 54 | } 55 | } 56 | 57 | // Do not finish or else the dialog will go away 58 | return false 59 | } 60 | 61 | private fun processNote(text: String) { 62 | if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.UPSIDE_DOWN_CAKE) { 63 | try { 64 | val intent = Intent(Intent.ACTION_CREATE_NOTE) 65 | intent.putExtra(Intent.EXTRA_TEXT, text) 66 | startActivity(intent) 67 | } catch (e: Exception) { 68 | Toast.makeText(this, R.string.no_note_taking_apps, Toast.LENGTH_LONG).show() 69 | } 70 | } else { 71 | // Ideally the option should not be visible 72 | Toast.makeText(this, R.string.note_taking_not_supported, Toast.LENGTH_LONG).show() 73 | } 74 | 75 | finish() 76 | } 77 | 78 | // Looks like it is not possible to transmit the selected text via the intent 79 | // and receive it in the result. So we save it as instance variable. 80 | // P.S - Passing via intent.putExtra() and then retrieving in intent.getExtra() does not work. 81 | private var mSelectedText = "" 82 | private val mResultLauncher = registerForActivityResult(ActivityResultContracts.StartActivityForResult()) { result -> 83 | if (result.resultCode == Activity.RESULT_OK) { 84 | // There are no request codes 85 | val data: Intent? = result.data 86 | if (data != null) { 87 | viewmodel.saveText(data.data, mSelectedText) 88 | mSelectedText = "" 89 | } 90 | } 91 | } 92 | 93 | private fun processTextFile(text: String) { 94 | val intent = Intent(Intent.ACTION_CREATE_DOCUMENT) 95 | intent.setType("text/plain") 96 | mSelectedText = text 97 | mResultLauncher.launch(intent) 98 | } 99 | } 100 | 101 | @Composable 102 | fun SaveTextSelectionDialog( 103 | onProcessNoteSelected: () -> Unit, 104 | onProcessFileSelected: () -> Unit, 105 | onDismiss: () -> Unit, 106 | ) { 107 | // A surface container using the 'background' color from the theme 108 | ListDialog( 109 | title = stringResource(id = R.string.save_text_title), 110 | message = stringResource(id = R.string.save_text_message), 111 | list = listOf( 112 | R.string.save_option_txt, 113 | // R.string.save_option_note, Note taking supported in keep only in A14 but not working in keep 114 | ), 115 | onItemSelected = { 116 | when (it) { 117 | 0 -> { 118 | // Text 119 | onProcessFileSelected() 120 | } 121 | 122 | 1 -> { 123 | // Note 124 | onProcessNoteSelected() 125 | } 126 | } 127 | }, 128 | stringSelector = { stringResource(id = it) }, 129 | iconSelector = { R.drawable.ic_save } 130 | ) { 131 | onDismiss() 132 | } 133 | } -------------------------------------------------------------------------------- /app/src/main/java/com/corphish/quicktools/activities/SimulationActivity.kt: -------------------------------------------------------------------------------- 1 | package com.corphish.quicktools.activities 2 | 3 | import android.content.Intent 4 | import android.os.Bundle 5 | import androidx.activity.ComponentActivity 6 | import androidx.activity.compose.setContent 7 | import androidx.activity.enableEdgeToEdge 8 | import androidx.compose.foundation.background 9 | import androidx.compose.foundation.layout.calculateEndPadding 10 | import androidx.compose.foundation.layout.calculateStartPadding 11 | import androidx.compose.foundation.layout.fillMaxSize 12 | import androidx.compose.foundation.layout.padding 13 | import androidx.compose.foundation.layout.size 14 | import androidx.compose.foundation.shape.CircleShape 15 | import androidx.compose.material.icons.Icons 16 | import androidx.compose.material.icons.automirrored.filled.ArrowRight 17 | import androidx.compose.material3.ExperimentalMaterial3Api 18 | import androidx.compose.material3.Icon 19 | import androidx.compose.material3.IconButton 20 | import androidx.compose.material3.MaterialTheme 21 | import androidx.compose.material3.OutlinedTextField 22 | import androidx.compose.material3.Scaffold 23 | import androidx.compose.material3.Text 24 | import androidx.compose.runtime.Composable 25 | import androidx.compose.runtime.getValue 26 | import androidx.compose.runtime.mutableStateOf 27 | import androidx.compose.runtime.remember 28 | import androidx.compose.runtime.setValue 29 | import androidx.compose.ui.Modifier 30 | import androidx.compose.ui.draw.clip 31 | import androidx.compose.ui.res.stringResource 32 | import androidx.compose.ui.text.style.TextAlign 33 | import androidx.compose.ui.tooling.preview.Preview 34 | import androidx.compose.ui.unit.LayoutDirection 35 | import androidx.compose.ui.unit.dp 36 | import androidx.constraintlayout.compose.ConstraintLayout 37 | import androidx.constraintlayout.compose.Dimension 38 | import com.corphish.quicktools.R 39 | import com.corphish.quicktools.data.Constants 40 | import com.corphish.quicktools.ui.common.CustomTopAppBar 41 | import com.corphish.quicktools.ui.theme.QuickToolsTheme 42 | 43 | class SimulationActivity : ComponentActivity() { 44 | @OptIn(ExperimentalMaterial3Api::class) 45 | override fun onCreate(savedInstanceState: Bundle?) { 46 | super.onCreate(savedInstanceState) 47 | enableEdgeToEdge() 48 | setContent { 49 | QuickToolsTheme { 50 | Scaffold( 51 | modifier = Modifier.fillMaxSize(), 52 | topBar = { 53 | CustomTopAppBar( 54 | id = R.string.simulate, 55 | onNavigationClick = { finish() }) 56 | } 57 | ) { innerPadding -> 58 | Simulation( 59 | modifier = Modifier.padding(innerPadding), 60 | onSimulate = { 61 | val intent = Intent(Intent.ACTION_PROCESS_TEXT).apply { 62 | type = "text/plain" 63 | setClass(this@SimulationActivity, OptionsActivity::class.java) 64 | putExtra(Intent.EXTRA_PROCESS_TEXT, it) 65 | putExtra(Intent.EXTRA_PROCESS_TEXT_READONLY, false) 66 | putExtra(Constants.INTENT_FORCE_COPY, true) 67 | } 68 | 69 | startActivity(intent) 70 | } 71 | ) 72 | } 73 | } 74 | } 75 | } 76 | } 77 | 78 | @Composable 79 | fun Simulation(modifier: Modifier = Modifier, onSimulate: (String) -> Unit = {}) { 80 | var inputText by remember { mutableStateOf("") } 81 | ConstraintLayout( 82 | modifier = modifier.fillMaxSize() 83 | ) { 84 | val (userInput, infoText, launchSimulation) = createRefs() 85 | 86 | OutlinedTextField( 87 | value = inputText, 88 | onValueChange = { 89 | inputText = it 90 | }, 91 | modifier = Modifier.constrainAs(userInput) { 92 | top.linkTo(parent.top, margin = 16.dp) 93 | bottom.linkTo(infoText.top, margin = 8.dp) 94 | start.linkTo(parent.start, margin = 8.dp) 95 | end.linkTo(parent.end, margin = 8.dp) 96 | height = Dimension.fillToConstraints 97 | width = Dimension.fillToConstraints 98 | }, 99 | label = { Text(text = stringResource(id = R.string.input)) }, 100 | ) 101 | 102 | Text( 103 | stringResource(id = R.string.simulate_details), 104 | style = MaterialTheme.typography.bodySmall, 105 | textAlign = TextAlign.Center, 106 | modifier = Modifier.constrainAs(infoText) { 107 | top.linkTo(userInput.bottom, margin = 8.dp) 108 | bottom.linkTo(launchSimulation.top, margin = 8.dp) 109 | start.linkTo(parent.start, margin = 8.dp) 110 | end.linkTo(parent.end, margin = 8.dp) 111 | width = Dimension.fillToConstraints 112 | } 113 | ) 114 | 115 | IconButton( 116 | onClick = { onSimulate(inputText) }, 117 | modifier = Modifier 118 | .constrainAs(launchSimulation) { 119 | top.linkTo(infoText.bottom, margin = 8.dp) 120 | start.linkTo(parent.start) 121 | end.linkTo(parent.end) 122 | bottom.linkTo(parent.bottom, margin = 16.dp) 123 | width = Dimension.wrapContent 124 | height = Dimension.wrapContent 125 | } 126 | .clip(CircleShape) 127 | .size(64.dp) 128 | .background(MaterialTheme.colorScheme.primary) 129 | ) { 130 | Icon( 131 | Icons.AutoMirrored.Filled.ArrowRight, 132 | contentDescription = null, 133 | tint = MaterialTheme.colorScheme.onPrimary, 134 | modifier = Modifier.size(48.dp) 135 | ) 136 | } 137 | } 138 | } 139 | 140 | @Preview(showBackground = true) 141 | @Composable 142 | fun GreetingPreview3() { 143 | QuickToolsTheme { 144 | Simulation() 145 | } 146 | } -------------------------------------------------------------------------------- /app/src/main/java/com/corphish/quicktools/activities/TryOutActivity.kt: -------------------------------------------------------------------------------- 1 | package com.corphish.quicktools.activities 2 | 3 | import android.app.Activity 4 | import android.os.Bundle 5 | import androidx.activity.ComponentActivity 6 | import androidx.activity.compose.setContent 7 | import androidx.compose.foundation.layout.Arrangement 8 | import androidx.compose.foundation.layout.Box 9 | import androidx.compose.foundation.layout.Column 10 | import androidx.compose.foundation.layout.Row 11 | import androidx.compose.foundation.layout.fillMaxSize 12 | import androidx.compose.foundation.layout.fillMaxWidth 13 | import androidx.compose.foundation.layout.padding 14 | import androidx.compose.material.icons.Icons 15 | import androidx.compose.material.icons.filled.CheckCircle 16 | import androidx.compose.material.icons.filled.Warning 17 | import androidx.compose.material3.AlertDialog 18 | import androidx.compose.material3.Button 19 | import androidx.compose.material3.FilledTonalButton 20 | import androidx.compose.material3.Icon 21 | import androidx.compose.material3.MaterialTheme 22 | import androidx.compose.material3.Surface 23 | import androidx.compose.material3.Text 24 | import androidx.compose.material3.TextButton 25 | import androidx.compose.material3.TextField 26 | import androidx.compose.runtime.Composable 27 | import androidx.compose.runtime.getValue 28 | import androidx.compose.runtime.mutableStateOf 29 | import androidx.compose.runtime.remember 30 | import androidx.compose.runtime.setValue 31 | import androidx.compose.ui.Alignment 32 | import androidx.compose.ui.Modifier 33 | import androidx.compose.ui.graphics.vector.ImageVector 34 | import androidx.compose.ui.platform.LocalContext 35 | import androidx.compose.ui.platform.LocalUriHandler 36 | import androidx.compose.ui.res.stringResource 37 | import androidx.compose.ui.tooling.preview.Preview 38 | import androidx.compose.ui.unit.dp 39 | import com.corphish.quicktools.R 40 | import com.corphish.quicktools.data.Constants 41 | import com.corphish.quicktools.ui.theme.QuickToolsTheme 42 | import com.corphish.quicktools.ui.theme.Typography 43 | 44 | /** 45 | * Try outs are disabled as of now because it looks like there is a known issue with 46 | * Jetpack Compose textfields which does not show context menu. 47 | */ 48 | class TryOutActivity : ComponentActivity() { 49 | override fun onCreate(savedInstanceState: Bundle?) { 50 | super.onCreate(savedInstanceState) 51 | val mode = intent.getStringExtra(TRY_OUT_FLOW) ?: TRY_OUT_FLOW_WUP 52 | setContent { 53 | QuickToolsTheme { 54 | // A surface container using the 'background' color from the theme 55 | Surface( 56 | modifier = Modifier.fillMaxSize(), 57 | color = MaterialTheme.colorScheme.background 58 | ) { 59 | TryOut(mode) 60 | } 61 | } 62 | } 63 | } 64 | 65 | companion object { 66 | // Flows 67 | const val TRY_OUT_FLOW_WUP = "wup" 68 | const val TRY_OUT_FLOW_EVAL = "eval" 69 | const val TRY_OUT_FLOW = "flow" 70 | } 71 | } 72 | 73 | @Composable 74 | fun TryOut(flow: String) { 75 | val openSuccessDialog = remember { mutableStateOf(false) } 76 | val openErrorDialog = remember { mutableStateOf(false) } 77 | var text by remember { mutableStateOf("") } 78 | val activity = (LocalContext.current as? Activity) 79 | val uriHandler = LocalUriHandler.current 80 | 81 | Column( 82 | modifier = Modifier.padding(16.dp) 83 | ) { 84 | Text( 85 | text = stringResource(id = R.string.app_name), 86 | style = Typography.headlineLarge, 87 | color = MaterialTheme.colorScheme.primary, 88 | modifier = Modifier.padding(bottom = 16.dp) 89 | ) 90 | Text( 91 | text = stringResource( 92 | id = when (flow) { 93 | TryOutActivity.TRY_OUT_FLOW_WUP -> R.string.try_wup 94 | TryOutActivity.TRY_OUT_FLOW_EVAL -> R.string.try_eval 95 | else -> R.string.trial_error 96 | } 97 | ), 98 | style = Typography.bodyMedium 99 | ) 100 | Box(modifier = Modifier.fillMaxSize()) { 101 | Column( 102 | modifier = Modifier.align(Alignment.BottomCenter) 103 | ) { 104 | TextField( 105 | modifier = Modifier.fillMaxWidth(), 106 | value = text, 107 | onValueChange = { text = it }, 108 | label = { Text("Enter text here") } 109 | ) 110 | 111 | Row( 112 | modifier = Modifier 113 | .padding(top = 16.dp) 114 | .fillMaxWidth(), 115 | horizontalArrangement = Arrangement.SpaceBetween 116 | ) { 117 | FilledTonalButton( 118 | onClick = { openErrorDialog.value = true }, 119 | modifier = Modifier 120 | .fillMaxWidth() 121 | .weight(1f) 122 | .padding(end = 4.dp) 123 | ) { Text(text = stringResource(id = R.string.trial_error)) } 124 | Button( 125 | onClick = { openSuccessDialog.value = true }, 126 | modifier = Modifier 127 | .fillMaxWidth() 128 | .weight(1f) 129 | .padding(start = 4.dp) 130 | ) { Text(text = stringResource(id = R.string.trial_success)) } 131 | } 132 | } 133 | } 134 | } 135 | 136 | when { 137 | openSuccessDialog.value -> { 138 | MessageDialog( 139 | hasDismissButton = false, 140 | onDismissRequest = { /* Not needed */ }, 141 | onConfirmation = { 142 | openSuccessDialog.value = false 143 | activity?.finish() 144 | }, 145 | dialogTitle = stringResource(id = R.string.trial_success), 146 | dialogText = stringResource(id = R.string.trial_success_msg), 147 | icon = Icons.Default.CheckCircle 148 | ) 149 | } 150 | 151 | openErrorDialog.value -> { 152 | MessageDialog( 153 | hasDismissButton = true, 154 | onDismissRequest = { 155 | openErrorDialog.value = false 156 | activity?.finish() 157 | }, 158 | onConfirmation = { 159 | openErrorDialog.value = false 160 | activity?.finish() 161 | uriHandler.openUri(Constants.ISSUES_PAGE_LINK) 162 | }, 163 | dialogTitle = stringResource(id = R.string.trial_error), 164 | dialogText = stringResource(id = R.string.trial_error_msg), 165 | icon = Icons.Default.Warning 166 | ) 167 | } 168 | } 169 | } 170 | 171 | @Composable 172 | fun MessageDialog( 173 | hasDismissButton: Boolean, 174 | onDismissRequest: () -> Unit, 175 | onConfirmation: () -> Unit, 176 | dialogTitle: String, 177 | dialogText: String, 178 | icon: ImageVector, 179 | ) { 180 | AlertDialog( 181 | icon = { 182 | Icon(icon, contentDescription = "Example Icon") 183 | }, 184 | title = { 185 | Text(text = dialogTitle) 186 | }, 187 | text = { 188 | Text(text = dialogText) 189 | }, 190 | onDismissRequest = { 191 | onDismissRequest() 192 | }, 193 | confirmButton = { 194 | TextButton( 195 | onClick = { 196 | onConfirmation() 197 | } 198 | ) { 199 | Text(stringResource(id = android.R.string.ok)) 200 | } 201 | }, 202 | dismissButton = { 203 | if (hasDismissButton) { 204 | TextButton( 205 | onClick = { 206 | onDismissRequest() 207 | } 208 | ) { 209 | Text(stringResource(id = android.R.string.cancel)) 210 | } 211 | } 212 | } 213 | ) 214 | } 215 | 216 | @Preview(showBackground = true) 217 | @Composable 218 | fun GreetingPreview2() { 219 | QuickToolsTheme { 220 | TryOut(TryOutActivity.TRY_OUT_FLOW_WUP) 221 | } 222 | } -------------------------------------------------------------------------------- /app/src/main/java/com/corphish/quicktools/activities/WUPActivity.kt: -------------------------------------------------------------------------------- 1 | package com.corphish.quicktools.activities 2 | 3 | import android.content.Intent 4 | import android.net.Uri 5 | import android.widget.Toast 6 | import androidx.activity.viewModels 7 | import androidx.lifecycle.lifecycleScope 8 | import com.corphish.quicktools.R 9 | import com.corphish.quicktools.data.Constants 10 | import com.corphish.quicktools.data.Result 11 | import com.corphish.quicktools.viewmodels.WUPViewModel 12 | import dagger.hilt.android.AndroidEntryPoint 13 | import kotlinx.coroutines.launch 14 | 15 | /** 16 | * WUP (WhatsApp Unknown Phone number) activity handles messaging to 17 | * unknown phone numbers in whatsapp without saving them as a contact by 18 | * opening it in wa.me/. 19 | */ 20 | @AndroidEntryPoint 21 | class WUPActivity : NoUIActivity() { 22 | private val wupViewModel: WUPViewModel by viewModels() 23 | 24 | override fun handleIntent(intent: Intent): Boolean { 25 | if (intent.hasExtra(Intent.EXTRA_PROCESS_TEXT)) { 26 | val text = intent.getCharSequenceExtra(Intent.EXTRA_PROCESS_TEXT)?.toString() ?: "" 27 | 28 | lifecycleScope.launch { 29 | wupViewModel.processedPhoneNumber.collect { 30 | when (it) { 31 | is Result.Success -> { 32 | openInWeb(phoneNumber = it.value) 33 | } 34 | 35 | is Result.Error -> { 36 | Toast.makeText(this@WUPActivity, R.string.invalid_phone_number, Toast.LENGTH_LONG).show() 37 | } 38 | 39 | is Result.Initial -> { 40 | // Do nothing 41 | } 42 | } 43 | } 44 | } 45 | 46 | wupViewModel.determinePhoneNumber(text) 47 | } 48 | 49 | return true 50 | } 51 | 52 | private fun openInWeb(phoneNumber: String) { 53 | val url = "${Constants.WHATSAPP_API_LINK}$phoneNumber" 54 | val browserIntent = Intent(Intent.ACTION_VIEW) 55 | browserIntent.data = Uri.parse(url) 56 | startActivity(browserIntent) 57 | } 58 | } -------------------------------------------------------------------------------- /app/src/main/java/com/corphish/quicktools/data/Constants.kt: -------------------------------------------------------------------------------- 1 | package com.corphish.quicktools.data 2 | 3 | /** 4 | * Constants. 5 | */ 6 | object Constants { 7 | // Phone number regular expression 8 | const val PHONE_NUMBER_REGEX = "^\\s*(?:\\+?(\\d{1,3}))?[-. (]*(\\d{3})[-. )]*(\\d{3})[-. ]*(\\d{4})(?: *x(\\d+))?\\s*\$" 9 | val COUNTRY_CODE_REGEX = "^\\+\\d{1,3}\$".toRegex() 10 | 11 | // Remove these special characters from the phone number 12 | val PHONE_NUMBER_SPECIAL_CHARACTERS = listOf(" ", "-", "(", ")") 13 | 14 | // URLs 15 | const val SOURCE_LINK = "https://github.com/corphish/TextTools/" 16 | const val CONTRIBUTORS_LINK = "${SOURCE_LINK}blob/main/CONTRIBUTORS.md" 17 | const val RELEASES_PAGE_LINK = "${SOURCE_LINK}releases" 18 | const val ISSUES_PAGE_LINK = "${SOURCE_LINK}issues" 19 | const val DONATE_LINK = "https://www.paypal.com/paypalme/corphish" 20 | const val WHATSAPP_API_LINK = "https://wa.me/" 21 | 22 | // Intent to force copy instead of applying 23 | const val INTENT_FORCE_COPY = "force_copy" 24 | } -------------------------------------------------------------------------------- /app/src/main/java/com/corphish/quicktools/data/Result.kt: -------------------------------------------------------------------------------- 1 | package com.corphish.quicktools.data 2 | 3 | sealed class Result { 4 | // Called when we have a successful call of operation with a value 5 | class Success(val value: T): Result() 6 | 7 | // Called when there is an error while performing an operation 8 | data object Error : Result() 9 | 10 | // Initial value 11 | data object Initial: Result() 12 | } -------------------------------------------------------------------------------- /app/src/main/java/com/corphish/quicktools/extensions/StringExtensions.kt: -------------------------------------------------------------------------------- 1 | package com.corphish.quicktools.extensions 2 | 3 | /** 4 | * Truncates text till a certain length 5 | */ 6 | fun String.truncate(len: Int = 16): String { 7 | val suffix = "..." 8 | return if (this.length <= len) { 9 | this 10 | } else { 11 | this.substring(0, len - suffix.length) + suffix 12 | } 13 | } -------------------------------------------------------------------------------- /app/src/main/java/com/corphish/quicktools/features/Feature.kt: -------------------------------------------------------------------------------- 1 | package com.corphish.quicktools.features 2 | 3 | import androidx.annotation.DrawableRes 4 | import androidx.annotation.StringRes 5 | import com.corphish.quicktools.R 6 | import com.corphish.quicktools.activities.TryOutActivity 7 | import com.corphish.quicktools.repository.FeatureIds 8 | 9 | /** 10 | * Data class that defines a feature. 11 | */ 12 | data class Feature( 13 | val id: FeatureIds, 14 | @DrawableRes val icon: Int, 15 | @StringRes val featureTitle: Int, 16 | @StringRes val featureDesc: Int, 17 | @StringRes val contextMenuText: Int, 18 | val flow: String, 19 | ) { 20 | companion object { 21 | val LIST = listOf( 22 | Feature( 23 | id = FeatureIds.WHATSAPP, 24 | icon = R.drawable.ic_whatsapp, 25 | featureTitle = R.string.wup_title, 26 | featureDesc = R.string.wup_desc, 27 | contextMenuText = R.string.context_menu_whatsapp, 28 | flow = TryOutActivity.TRY_OUT_FLOW_WUP, 29 | ), 30 | Feature( 31 | id = FeatureIds.EVAL, 32 | icon = R.drawable.ic_numbers, 33 | featureTitle = R.string.eval_title, 34 | featureDesc = R.string.eval_desc, 35 | contextMenuText = R.string.context_menu_eval, 36 | flow = TryOutActivity.TRY_OUT_FLOW_EVAL, 37 | ), 38 | Feature( 39 | id = FeatureIds.TRANSFORM, 40 | icon = R.drawable.ic_text_transform, 41 | featureTitle = R.string.transform_long, 42 | featureDesc = R.string.transform_desc, 43 | contextMenuText = R.string.context_menu_transform, 44 | flow = TryOutActivity.TRY_OUT_FLOW 45 | ), 46 | Feature( 47 | id = FeatureIds.TEXT_COUNT, 48 | icon = R.drawable.ic_text_count, 49 | featureTitle = R.string.text_count, 50 | featureDesc = R.string.text_count_desc, 51 | contextMenuText = R.string.context_menu_text_count, 52 | flow = TryOutActivity.TRY_OUT_FLOW 53 | ), 54 | Feature( 55 | id = FeatureIds.SAVE_TEXT, 56 | icon = R.drawable.ic_save, 57 | featureTitle = R.string.save_text_title, 58 | featureDesc = R.string.save_text_desc, 59 | contextMenuText = R.string.context_menu_save, 60 | flow = TryOutActivity.TRY_OUT_FLOW 61 | ), 62 | Feature( 63 | id = FeatureIds.FIND_AND_REPLACE, 64 | icon = R.drawable.ic_find_and_replace, 65 | featureTitle = R.string.title_activity_find_and_replace, 66 | featureDesc = R.string.find_and_replace_desc, 67 | contextMenuText = R.string.context_menu_find_and_replace, 68 | flow = TryOutActivity.TRY_OUT_FLOW 69 | ) 70 | ) 71 | } 72 | } 73 | -------------------------------------------------------------------------------- /app/src/main/java/com/corphish/quicktools/modules/AppModule.kt: -------------------------------------------------------------------------------- 1 | package com.corphish.quicktools.modules 2 | 3 | import android.content.Context 4 | import com.corphish.quicktools.repository.ContextMenuOptionsRepository 5 | import com.corphish.quicktools.repository.ContextMenuOptionsRepositoryImpl 6 | import com.corphish.quicktools.repository.SettingsRepository 7 | import com.corphish.quicktools.repository.TextRepository 8 | import com.corphish.quicktools.repository.TextRepositoryImpl 9 | import dagger.Module 10 | import dagger.Provides 11 | import dagger.hilt.InstallIn 12 | import dagger.hilt.android.qualifiers.ApplicationContext 13 | import dagger.hilt.components.SingletonComponent 14 | import javax.inject.Singleton 15 | 16 | @Module 17 | @InstallIn(SingletonComponent::class) 18 | object AppModule { 19 | @Provides 20 | @Singleton 21 | fun provideSettingsRepository(@ApplicationContext context: Context) = 22 | SettingsRepository(context) 23 | 24 | @Provides 25 | @Singleton 26 | fun provideTextRepository(@ApplicationContext context: Context): TextRepository = 27 | TextRepositoryImpl(context) 28 | 29 | @Provides 30 | @Singleton 31 | fun provideContextMenuOptionsRepository(@ApplicationContext context: Context): ContextMenuOptionsRepository = 32 | ContextMenuOptionsRepositoryImpl(context) 33 | } -------------------------------------------------------------------------------- /app/src/main/java/com/corphish/quicktools/repository/ContextMenuOptionsRepository.kt: -------------------------------------------------------------------------------- 1 | package com.corphish.quicktools.repository 2 | 3 | /** 4 | * Repository for the context menu options. 5 | */ 6 | interface ContextMenuOptionsRepository { 7 | 8 | /** 9 | * Gets the current app mode. 10 | */ 11 | fun getCurrentAppMode(): AppMode 12 | 13 | /** 14 | * Sets the current app mode. 15 | */ 16 | fun setCurrentAppMode(mode: AppMode) 17 | 18 | /** 19 | * Returns the currently enabled feature set. 20 | */ 21 | fun getCurrentlyEnabledFeatures(): List 22 | 23 | /** 24 | * Enables or disables a feature. 25 | */ 26 | fun enableOrDisableFeature(feature: FeatureIds, enabled: Boolean) 27 | } 28 | 29 | enum class AppMode { 30 | SINGLE, 31 | MULTI 32 | } 33 | 34 | enum class FeatureIds { 35 | EVAL, 36 | WHATSAPP, 37 | TRANSFORM, 38 | TEXT_COUNT, 39 | SAVE_TEXT, 40 | FIND_AND_REPLACE 41 | } -------------------------------------------------------------------------------- /app/src/main/java/com/corphish/quicktools/repository/ContextMenuOptionsRepositoryImpl.kt: -------------------------------------------------------------------------------- 1 | package com.corphish.quicktools.repository 2 | 3 | import android.content.ComponentName 4 | import android.content.Context 5 | import android.content.pm.PackageManager 6 | import androidx.core.content.edit 7 | import androidx.preference.PreferenceManager 8 | 9 | /** 10 | * Implementation of the [ContextMenuOptionsRepository]. 11 | * This class is also responsible for enabling/disabling actual components. 12 | */ 13 | class ContextMenuOptionsRepositoryImpl(private val context: Context) : 14 | ContextMenuOptionsRepository { 15 | private val _modeKey = "context_menu_mode" 16 | private val _featuresKeySuffix = "context_menu_features_" 17 | private val _sharedPreferenceManager = PreferenceManager.getDefaultSharedPreferences(context) 18 | private val _packageManager = context.packageManager 19 | 20 | // Manifest name mappings 21 | private val _optionsActivityName = context.packageName + ".activities.OptionsActivity" 22 | private val _multiFeatureAliasMapping = mapOf( 23 | FeatureIds.EVAL to context.packageName + ".activities.EvalActivityAlias", 24 | FeatureIds.WHATSAPP to context.packageName + ".activities.WUPActivityAlias", 25 | FeatureIds.TRANSFORM to context.packageName + ".activities.TransformActivityAlias", 26 | FeatureIds.TEXT_COUNT to context.packageName + ".activities.TextCountActivityAlias", 27 | FeatureIds.FIND_AND_REPLACE to context.packageName + ".activities.FindAndReplaceActivityAlias", 28 | FeatureIds.SAVE_TEXT to context.packageName + ".activities.SaveTextActivityAlias" 29 | ) 30 | 31 | override fun getCurrentAppMode(): AppMode { 32 | val mode = _sharedPreferenceManager.getString(_modeKey, AppMode.SINGLE.name) 33 | ?: return AppMode.SINGLE 34 | 35 | return AppMode.valueOf(mode) 36 | } 37 | 38 | override fun setCurrentAppMode(mode: AppMode) { 39 | _sharedPreferenceManager.edit { 40 | putString(_modeKey, mode.name) 41 | } 42 | 43 | switchModeTo(mode) 44 | } 45 | 46 | private fun switchModeTo(mode: AppMode) { 47 | when (mode) { 48 | AppMode.SINGLE -> { 49 | // Enable options activity 50 | val optionsActivityComp = ComponentName(context.packageName, _optionsActivityName) 51 | _packageManager.setComponentEnabledSetting( 52 | optionsActivityComp, 53 | PackageManager.COMPONENT_ENABLED_STATE_ENABLED, 54 | PackageManager.DONT_KILL_APP 55 | ) 56 | 57 | // Disable all multi feature aliases 58 | _multiFeatureAliasMapping.forEach { 59 | _packageManager.setComponentEnabledSetting( 60 | ComponentName(context.packageName, it.value), 61 | PackageManager.COMPONENT_ENABLED_STATE_DISABLED, 62 | PackageManager.DONT_KILL_APP 63 | ) 64 | } 65 | } 66 | 67 | AppMode.MULTI -> { 68 | // Disable options activity 69 | val optionsActivityComp = ComponentName(context.packageName, _optionsActivityName) 70 | _packageManager.setComponentEnabledSetting( 71 | optionsActivityComp, 72 | PackageManager.COMPONENT_ENABLED_STATE_DISABLED, 73 | PackageManager.DONT_KILL_APP 74 | ) 75 | 76 | // Enable all the multi feature aliases 77 | val currentFeatures = getCurrentlyEnabledFeatures() 78 | _multiFeatureAliasMapping.forEach { 79 | _packageManager.setComponentEnabledSetting( 80 | ComponentName(context.packageName, it.value), 81 | if (currentFeatures.contains(it.key)) PackageManager.COMPONENT_ENABLED_STATE_ENABLED else PackageManager.COMPONENT_ENABLED_STATE_DISABLED, 82 | PackageManager.DONT_KILL_APP 83 | ) 84 | } 85 | } 86 | } 87 | } 88 | 89 | override fun getCurrentlyEnabledFeatures(): List { 90 | val enabledFeatures = mutableListOf() 91 | _multiFeatureAliasMapping.forEach { (featureIds, _) -> 92 | if (_sharedPreferenceManager.getBoolean(_featuresKeySuffix + featureIds.name, true)) { 93 | enabledFeatures.add(featureIds) 94 | } 95 | } 96 | 97 | return enabledFeatures 98 | } 99 | 100 | override fun enableOrDisableFeature(feature: FeatureIds, enabled: Boolean) { 101 | _sharedPreferenceManager.edit { 102 | putBoolean(_featuresKeySuffix + feature.name, enabled) 103 | } 104 | 105 | // Enable or disable the component if we are in multi mode 106 | // For single no action required 107 | val mode = getCurrentAppMode() 108 | if (mode == AppMode.MULTI) { 109 | val featureAlias = _multiFeatureAliasMapping[feature] ?: return 110 | val component = ComponentName(context.packageName, featureAlias) 111 | _packageManager.setComponentEnabledSetting( 112 | component, 113 | if (enabled) PackageManager.COMPONENT_ENABLED_STATE_ENABLED else PackageManager.COMPONENT_ENABLED_STATE_DISABLED, 114 | PackageManager.DONT_KILL_APP 115 | ) 116 | } 117 | } 118 | } -------------------------------------------------------------------------------- /app/src/main/java/com/corphish/quicktools/repository/SettingsRepository.kt: -------------------------------------------------------------------------------- 1 | package com.corphish.quicktools.repository 2 | 3 | import android.content.Context 4 | import androidx.core.content.edit 5 | import androidx.preference.PreferenceManager 6 | import com.corphish.quicktools.viewmodels.EvalViewModel.Companion.EVAL_RESULT_MODE_ASK_NEXT_TIME 7 | 8 | class SettingsRepository( 9 | context: Context 10 | ) { 11 | private val _prependCCEnabledKey = "prepend_country_code_enabled" 12 | private val _prependCCKey = "prepend_country_code" 13 | private val _decimalPoints = "decimal_points" 14 | private val _evaluateResultMode = "eval_result_mode" 15 | private val _onboardingDone = "onboarding_done" 16 | 17 | private val _sharedPreferenceManager = PreferenceManager.getDefaultSharedPreferences(context) 18 | 19 | fun getPrependCountryCodeEnabled() = 20 | _sharedPreferenceManager.getBoolean(_prependCCEnabledKey, false) 21 | 22 | fun setPrependCountryCodeEnabled(enabled: Boolean) { 23 | _sharedPreferenceManager.edit { 24 | putBoolean(_prependCCEnabledKey, enabled) 25 | } 26 | } 27 | 28 | fun getPrependCountryCode() = 29 | _sharedPreferenceManager.getString(_prependCCKey, "") 30 | 31 | fun setPrependCountryCode(code: String) { 32 | _sharedPreferenceManager.edit { 33 | putString(_prependCCKey, code) 34 | } 35 | } 36 | 37 | fun getDecimalPoints() = 38 | _sharedPreferenceManager.getInt(_decimalPoints, 2) 39 | 40 | fun setDecimalPoints(points: Int) { 41 | _sharedPreferenceManager.edit { 42 | putInt(_decimalPoints, points) 43 | } 44 | } 45 | 46 | fun getEvaluateResultMode() = 47 | _sharedPreferenceManager.getInt(_evaluateResultMode, EVAL_RESULT_MODE_ASK_NEXT_TIME) 48 | 49 | fun setEvaluateResultMode(mode: Int) { 50 | _sharedPreferenceManager.edit { 51 | putInt(_evaluateResultMode, mode) 52 | } 53 | } 54 | 55 | fun getOnboardingDone() = 56 | _sharedPreferenceManager.getBoolean(_onboardingDone, false) 57 | 58 | fun setOnboardingDone(done: Boolean) { 59 | _sharedPreferenceManager.edit { 60 | putBoolean(_onboardingDone, done) 61 | } 62 | } 63 | } -------------------------------------------------------------------------------- /app/src/main/java/com/corphish/quicktools/repository/TextRepository.kt: -------------------------------------------------------------------------------- 1 | package com.corphish.quicktools.repository 2 | 3 | import android.net.Uri 4 | 5 | /** 6 | * Common definition for text repository capabilities for file writes. 7 | */ 8 | interface TextRepository { 9 | /** 10 | * Writes the given text to somewhere. 11 | * @return Boolean indicating success state. 12 | */ 13 | fun writeText(uri: Uri?, text: String): Boolean 14 | } -------------------------------------------------------------------------------- /app/src/main/java/com/corphish/quicktools/repository/TextRepositoryImpl.kt: -------------------------------------------------------------------------------- 1 | package com.corphish.quicktools.repository 2 | 3 | import android.content.Context 4 | import android.net.Uri 5 | import java.io.BufferedWriter 6 | import java.io.IOException 7 | import java.io.OutputStream 8 | import java.io.OutputStreamWriter 9 | 10 | /** 11 | * Uri based text repository for file writes 12 | */ 13 | class TextRepositoryImpl(val context: Context): TextRepository { 14 | override fun writeText(uri: Uri?, text: String): Boolean { 15 | if (uri == null) { 16 | return false 17 | } 18 | 19 | val outputStream: OutputStream? 20 | try { 21 | outputStream = context.contentResolver.openOutputStream(uri) 22 | val bw = BufferedWriter(OutputStreamWriter(outputStream)) 23 | bw.write(text) 24 | bw.flush() 25 | bw.close() 26 | } catch (e: IOException) { 27 | e.printStackTrace() 28 | return false 29 | } 30 | 31 | return true 32 | } 33 | } -------------------------------------------------------------------------------- /app/src/main/java/com/corphish/quicktools/text/TextReplacementAction.kt: -------------------------------------------------------------------------------- 1 | package com.corphish.quicktools.text 2 | 3 | data class TextReplacementAction( 4 | // Old text that was replaced 5 | val oldText: String, 6 | 7 | // New text that the old text was replaced with 8 | val newText: String, 9 | ) 10 | -------------------------------------------------------------------------------- /app/src/main/java/com/corphish/quicktools/text/TextReplacementManager.kt: -------------------------------------------------------------------------------- 1 | package com.corphish.quicktools.text 2 | 3 | import android.util.Log 4 | import androidx.compose.ui.text.TextRange 5 | 6 | /** 7 | * Class that handles text replacement to some initial text with proper tracking of actions. 8 | */ 9 | class TextReplacementManager(private val inputString: String) { 10 | // List of actions performed 11 | private val mActionList = mutableListOf() 12 | 13 | // Current state of action 14 | private var mActionPointer = 0 15 | 16 | private var mCurrentText = inputString 17 | 18 | private val _debug = false 19 | 20 | init { 21 | dumpState("Init") 22 | } 23 | 24 | /** 25 | * Resets the text to initial state. 26 | */ 27 | fun reset(): String { 28 | mCurrentText = inputString 29 | mActionList.clear() 30 | mActionPointer = 0 31 | 32 | return mCurrentText 33 | } 34 | 35 | /** 36 | * Replaces one occurrence of the text defined by the range with the newText. 37 | */ 38 | fun replaceOne(range: TextRange, newText: String): String { 39 | val res = mCurrentText.substring(0, range.start) + newText + mCurrentText.substring(range.end) 40 | 41 | addAction(TextReplacementAction(mCurrentText, res)) 42 | 43 | mCurrentText = res 44 | return res 45 | } 46 | 47 | /** 48 | * Replaces all occurrence of the text defined by the range with the newText. 49 | */ 50 | fun replaceAll(oldText: String, newText: String, ignoreCase: Boolean): String { 51 | val res = mCurrentText.replace(oldText, newText, ignoreCase) 52 | 53 | addAction(TextReplacementAction(mCurrentText, res)) 54 | 55 | mCurrentText = res 56 | return res 57 | } 58 | 59 | /** 60 | * Checks whether it is possible to perform undo on current state. 61 | */ 62 | fun canUndo() = 63 | mActionList.isNotEmpty() && mActionPointer > 0 64 | 65 | /** 66 | * Checks whether it is possible to perform redo on current state. 67 | */ 68 | fun canRedo() = 69 | mActionList.isNotEmpty() && mActionPointer < mActionList.size 70 | 71 | private fun addAction(action: TextReplacementAction) { 72 | if (mActionList.isEmpty() || mActionPointer == mActionList.size) { 73 | mActionList += action 74 | mActionPointer += 1 75 | } else { 76 | // Currently we are in a state where an undo operation is done. 77 | // In such case, we add the action at the current pointer index 78 | // and remove the next action elements. 79 | mActionList.add(mActionPointer, action) 80 | mActionList.subList(mActionPointer + 1, mActionList.size).clear() 81 | mActionPointer += 1 82 | } 83 | 84 | dumpState("addAction") 85 | } 86 | 87 | /** 88 | * Performs the undo operation. 89 | */ 90 | fun undo(): String { 91 | if (mActionPointer == 0) { 92 | return mCurrentText 93 | } 94 | 95 | // We undo the last operation 96 | val action = mActionList[mActionPointer - 1] 97 | 98 | mCurrentText = action.oldText 99 | mActionPointer -= 1 100 | 101 | dumpState("Undo") 102 | 103 | return mCurrentText 104 | } 105 | 106 | /** 107 | * Performs the redo operation. 108 | */ 109 | fun redo(): String { 110 | if (mActionPointer == mActionList.size) { 111 | return mCurrentText 112 | } 113 | 114 | // We redo the current operation 115 | val action = mActionList[mActionPointer] 116 | 117 | mCurrentText = action.newText 118 | mActionPointer += 1 119 | 120 | dumpState("Redo") 121 | 122 | return mCurrentText 123 | } 124 | 125 | fun updateText(newText: String) { 126 | addAction(TextReplacementAction(mCurrentText, newText)) 127 | mCurrentText = newText 128 | } 129 | 130 | private fun dumpState(caller: String) { 131 | if (_debug) { 132 | Log.d("TextReplacementManager", "[$caller] actions=$mActionList, ptr=$mActionPointer") 133 | } 134 | } 135 | } -------------------------------------------------------------------------------- /app/src/main/java/com/corphish/quicktools/ui/common/AppBars.kt: -------------------------------------------------------------------------------- 1 | @file:OptIn(ExperimentalMaterial3Api::class) 2 | 3 | package com.corphish.quicktools.ui.common 4 | 5 | import androidx.annotation.StringRes 6 | import androidx.compose.foundation.layout.size 7 | import androidx.compose.material.icons.Icons 8 | import androidx.compose.material.icons.automirrored.outlined.KeyboardArrowLeft 9 | import androidx.compose.material3.ExperimentalMaterial3Api 10 | import androidx.compose.material3.Icon 11 | import androidx.compose.material3.IconButton 12 | import androidx.compose.material3.MaterialTheme 13 | import androidx.compose.material3.MediumTopAppBar 14 | import androidx.compose.material3.Text 15 | import androidx.compose.material3.TopAppBarDefaults 16 | import androidx.compose.material3.TopAppBarScrollBehavior 17 | import androidx.compose.runtime.Composable 18 | import androidx.compose.ui.Modifier 19 | import androidx.compose.ui.res.stringResource 20 | import androidx.compose.ui.unit.dp 21 | import com.corphish.quicktools.ui.theme.BrandFontFamily 22 | import com.corphish.quicktools.ui.theme.TypographyV2 23 | 24 | @Composable 25 | fun CustomTopAppBar( 26 | @StringRes id: Int, 27 | onNavigationClick: () -> Unit = {}, 28 | scrollBehavior: TopAppBarScrollBehavior = TopAppBarDefaults.enterAlwaysScrollBehavior(), 29 | ) { 30 | MediumTopAppBar( 31 | title = { 32 | Text( 33 | text = stringResource(id = id), 34 | style = TypographyV2.headlineMedium, 35 | color = MaterialTheme.colorScheme.primary, 36 | fontFamily = BrandFontFamily, 37 | ) 38 | }, 39 | navigationIcon = { 40 | IconButton(onClick = { onNavigationClick() }) { 41 | Icon( 42 | imageVector = Icons.AutoMirrored.Outlined.KeyboardArrowLeft, 43 | contentDescription = "", 44 | modifier = Modifier.size(32.dp), 45 | tint = MaterialTheme.colorScheme.primary 46 | ) 47 | } 48 | }, 49 | scrollBehavior = scrollBehavior 50 | ) 51 | } -------------------------------------------------------------------------------- /app/src/main/java/com/corphish/quicktools/ui/common/Buttons.kt: -------------------------------------------------------------------------------- 1 | package com.corphish.quicktools.ui.common 2 | 3 | import androidx.compose.foundation.clickable 4 | import androidx.compose.foundation.layout.Column 5 | import androidx.compose.foundation.layout.padding 6 | import androidx.compose.foundation.layout.size 7 | import androidx.compose.foundation.shape.CircleShape 8 | import androidx.compose.material3.Icon 9 | import androidx.compose.material3.IconButton 10 | import androidx.compose.material3.IconButtonDefaults 11 | import androidx.compose.material3.MaterialTheme 12 | import androidx.compose.material3.Text 13 | import androidx.compose.runtime.Composable 14 | import androidx.compose.ui.Alignment 15 | import androidx.compose.ui.Modifier 16 | import androidx.compose.ui.draw.clip 17 | import androidx.compose.ui.graphics.Color 18 | import androidx.compose.ui.graphics.painter.Painter 19 | import androidx.compose.ui.graphics.vector.ImageVector 20 | import androidx.compose.ui.text.font.FontWeight 21 | import androidx.compose.ui.unit.dp 22 | import com.corphish.quicktools.ui.theme.BrandFontFamily 23 | 24 | @Composable 25 | fun CircularButtonWithText( 26 | modifier: Modifier = Modifier, 27 | imageVector: ImageVector? = null, 28 | painterResource: Painter? = null, 29 | enabled: Boolean = true, 30 | text: String = "", 31 | onClick: () -> Unit = {} 32 | ) { 33 | Column( 34 | modifier = modifier.clickable { 35 | if (enabled) { 36 | onClick() 37 | } 38 | }, 39 | horizontalAlignment = Alignment.CenterHorizontally 40 | ) { 41 | IconButton( 42 | onClick = { onClick() }, 43 | enabled = enabled, 44 | colors = IconButtonDefaults.iconButtonColors( 45 | containerColor = MaterialTheme.colorScheme.primary, 46 | contentColor = MaterialTheme.colorScheme.onPrimary, 47 | disabledContainerColor = Color.Gray 48 | ), 49 | modifier = Modifier 50 | .clip(CircleShape) 51 | .size(48.dp) 52 | ) { 53 | if (imageVector != null) { 54 | Icon( 55 | imageVector = imageVector, 56 | contentDescription = "", 57 | modifier = Modifier.size(24.dp) 58 | ) 59 | } else if (painterResource != null) { 60 | Icon( 61 | painter = painterResource, 62 | contentDescription = "", 63 | modifier = Modifier.size(24.dp) 64 | ) 65 | } 66 | } 67 | 68 | Text( 69 | text = text, 70 | color = if (enabled) MaterialTheme.colorScheme.onBackground else Color.Gray, 71 | fontFamily = BrandFontFamily, 72 | fontWeight = FontWeight.W600, 73 | modifier = Modifier.padding(top = 4.dp) 74 | ) 75 | } 76 | } -------------------------------------------------------------------------------- /app/src/main/java/com/corphish/quicktools/ui/common/ListDialog.kt: -------------------------------------------------------------------------------- 1 | package com.corphish.quicktools.ui.common 2 | 3 | import androidx.annotation.DrawableRes 4 | import androidx.compose.foundation.Image 5 | import androidx.compose.foundation.background 6 | import androidx.compose.foundation.clickable 7 | import androidx.compose.foundation.layout.Box 8 | import androidx.compose.foundation.layout.Column 9 | import androidx.compose.foundation.layout.ColumnScope 10 | import androidx.compose.foundation.layout.Row 11 | import androidx.compose.foundation.layout.fillMaxWidth 12 | import androidx.compose.foundation.layout.padding 13 | import androidx.compose.foundation.layout.size 14 | import androidx.compose.foundation.shape.CircleShape 15 | import androidx.compose.foundation.shape.RoundedCornerShape 16 | import androidx.compose.material.icons.Icons 17 | import androidx.compose.material.icons.automirrored.outlined.KeyboardArrowLeft 18 | import androidx.compose.material3.Card 19 | import androidx.compose.material3.CardDefaults 20 | import androidx.compose.material3.Icon 21 | import androidx.compose.material3.IconButton 22 | import androidx.compose.material3.MaterialTheme 23 | import androidx.compose.material3.Text 24 | import androidx.compose.runtime.Composable 25 | import androidx.compose.ui.Alignment 26 | import androidx.compose.ui.Modifier 27 | import androidx.compose.ui.draw.clip 28 | import androidx.compose.ui.graphics.ColorFilter 29 | import androidx.compose.ui.res.painterResource 30 | import androidx.compose.ui.text.font.FontWeight 31 | import androidx.compose.ui.unit.dp 32 | import androidx.compose.ui.window.Dialog 33 | import androidx.compose.ui.window.DialogProperties 34 | import com.corphish.quicktools.ui.theme.BrandFontFamily 35 | import com.corphish.quicktools.ui.theme.TypographyV2 36 | 37 | /** 38 | * A list of selectable options shown inside a dialog. 39 | */ 40 | @Composable 41 | fun ListDialog( 42 | title: String, 43 | message: String, 44 | list: List, 45 | supportBack: Boolean = false, 46 | onItemSelected: (Int) -> Unit, 47 | stringSelector: @Composable (T) -> String, 48 | iconSelector: @Composable (T) -> Int, 49 | onBackPressed: () -> Unit = {}, 50 | additionalContent: @Composable ColumnScope.() -> Unit = {}, 51 | dismissible: Boolean = true, 52 | onDismissRequest: () -> Unit 53 | ) { 54 | Dialog( 55 | onDismissRequest = { onDismissRequest() }, 56 | properties = DialogProperties( 57 | dismissOnBackPress = dismissible, 58 | dismissOnClickOutside = dismissible 59 | ) 60 | ) { 61 | Card( 62 | modifier = Modifier 63 | .fillMaxWidth() 64 | .padding(16.dp), 65 | shape = RoundedCornerShape(16.dp), 66 | elevation = CardDefaults.cardElevation(defaultElevation = 8.dp) 67 | ) { 68 | Column( 69 | modifier = Modifier 70 | .padding(16.dp), 71 | ) { 72 | Row( 73 | verticalAlignment = Alignment.CenterVertically, 74 | modifier = Modifier.padding(bottom = 4.dp) 75 | ) { 76 | if (supportBack) { 77 | IconButton( 78 | onClick = { onBackPressed() }, 79 | modifier = Modifier 80 | .clip(CircleShape) 81 | .size(48.dp) 82 | .background(MaterialTheme.colorScheme.primary) 83 | ) { 84 | Icon( 85 | imageVector = Icons.AutoMirrored.Outlined.KeyboardArrowLeft, 86 | contentDescription = "", 87 | modifier = Modifier.size(32.dp), 88 | tint = MaterialTheme.colorScheme.onPrimary 89 | ) 90 | } 91 | } 92 | 93 | Text( 94 | text = title, 95 | style = TypographyV2.headlineMedium, 96 | fontFamily = BrandFontFamily, 97 | color = MaterialTheme.colorScheme.primary, 98 | modifier = Modifier.padding(start = if (supportBack) 16.dp else 0.dp) 99 | ) 100 | } 101 | 102 | Text( 103 | text = message, 104 | style = TypographyV2.bodyMedium, 105 | modifier = Modifier.padding(bottom = 16.dp) 106 | ) 107 | 108 | for ((index, item) in list.withIndex()) { 109 | ListDialogItem(text = stringSelector(item), icon = iconSelector(item)) { 110 | onItemSelected(index) 111 | } 112 | } 113 | 114 | additionalContent() 115 | } 116 | } 117 | } 118 | } 119 | 120 | @Composable 121 | fun ListDialogItem(text: String, @DrawableRes icon: Int, onClick: () -> Unit) { 122 | Box( 123 | modifier = Modifier 124 | .fillMaxWidth() 125 | .clickable { onClick() } 126 | ) { 127 | Row( 128 | modifier = Modifier.padding(all = 8.dp), 129 | verticalAlignment = Alignment.CenterVertically, 130 | ) { 131 | Box( 132 | modifier = Modifier 133 | .size(36.dp) 134 | .clip(CircleShape) 135 | .background(MaterialTheme.colorScheme.primary), 136 | contentAlignment = Alignment.Center 137 | ) { 138 | Image( 139 | painterResource(id = icon), 140 | contentDescription = "", 141 | modifier = Modifier.size(24.dp), 142 | colorFilter = ColorFilter.tint(color = MaterialTheme.colorScheme.onPrimary) 143 | ) 144 | } 145 | 146 | Text( 147 | text = text, 148 | style = TypographyV2.labelMedium, 149 | fontFamily = BrandFontFamily, 150 | fontWeight = FontWeight.W400, 151 | modifier = Modifier.padding(start = 16.dp) 152 | ) 153 | } 154 | } 155 | } -------------------------------------------------------------------------------- /app/src/main/java/com/corphish/quicktools/ui/theme/Color.kt: -------------------------------------------------------------------------------- 1 | package com.corphish.quicktools.ui.theme 2 | 3 | import androidx.compose.ui.graphics.Color 4 | 5 | val BlueForeground = Color(0xFF0099FF) 6 | val BlueBackground = Color(0xFFCCEBFF) -------------------------------------------------------------------------------- /app/src/main/java/com/corphish/quicktools/ui/theme/Theme.kt: -------------------------------------------------------------------------------- 1 | package com.corphish.quicktools.ui.theme 2 | 3 | import android.app.Activity 4 | import android.os.Build 5 | import androidx.compose.foundation.isSystemInDarkTheme 6 | import androidx.compose.material3.MaterialTheme 7 | import androidx.compose.material3.darkColorScheme 8 | import androidx.compose.material3.dynamicDarkColorScheme 9 | import androidx.compose.material3.dynamicLightColorScheme 10 | import androidx.compose.material3.lightColorScheme 11 | import androidx.compose.runtime.Composable 12 | import androidx.compose.runtime.SideEffect 13 | import androidx.compose.ui.graphics.toArgb 14 | import androidx.compose.ui.platform.LocalContext 15 | import androidx.compose.ui.platform.LocalView 16 | import androidx.core.view.WindowCompat 17 | 18 | private val DarkColorScheme = darkColorScheme( 19 | primary = BlueForeground, 20 | secondary = BlueBackground, 21 | tertiary = BlueForeground 22 | ) 23 | 24 | private val LightColorScheme = lightColorScheme( 25 | primary = BlueForeground, 26 | secondary = BlueBackground, 27 | tertiary = BlueForeground 28 | 29 | /* Other default colors to override 30 | background = Color(0xFFFFFBFE), 31 | surface = Color(0xFFFFFBFE), 32 | onPrimary = Color.White, 33 | onSecondary = Color.White, 34 | onTertiary = Color.White, 35 | onBackground = Color(0xFF1C1B1F), 36 | onSurface = Color(0xFF1C1B1F), 37 | */ 38 | ) 39 | 40 | @Composable 41 | fun QuickToolsTheme( 42 | darkTheme: Boolean = isSystemInDarkTheme(), 43 | // Dynamic color is available on Android 12+ 44 | dynamicColor: Boolean = true, 45 | content: @Composable () -> Unit 46 | ) { 47 | val colorScheme = when { 48 | dynamicColor && Build.VERSION.SDK_INT >= Build.VERSION_CODES.S -> { 49 | val context = LocalContext.current 50 | if (darkTheme) dynamicDarkColorScheme(context) else dynamicLightColorScheme(context) 51 | } 52 | 53 | darkTheme -> DarkColorScheme 54 | else -> LightColorScheme 55 | } 56 | val view = LocalView.current 57 | 58 | if (!view.isInEditMode) { 59 | SideEffect { 60 | val window = (view.context as Activity).window 61 | 62 | // API 35 like status bar looks wierd in API 34 and below 63 | // So we continue to use old status bar in older APIs 64 | if (Build.VERSION.SDK_INT < Build.VERSION_CODES.VANILLA_ICE_CREAM) { 65 | window.statusBarColor = colorScheme.primary.toArgb() 66 | WindowCompat.getInsetsController(window, view).isAppearanceLightStatusBars = darkTheme 67 | } else { 68 | WindowCompat.getInsetsController(window, view).isAppearanceLightStatusBars = 69 | !darkTheme 70 | } 71 | } 72 | } 73 | 74 | MaterialTheme( 75 | colorScheme = colorScheme, 76 | typography = Typography, 77 | content = content 78 | ) 79 | } -------------------------------------------------------------------------------- /app/src/main/java/com/corphish/quicktools/ui/theme/Type.kt: -------------------------------------------------------------------------------- 1 | package com.corphish.quicktools.ui.theme 2 | 3 | import androidx.compose.material3.Typography 4 | import androidx.compose.ui.text.TextStyle 5 | import androidx.compose.ui.text.font.FontFamily 6 | import androidx.compose.ui.text.font.FontWeight 7 | import androidx.compose.ui.text.googlefonts.Font 8 | import androidx.compose.ui.text.googlefonts.GoogleFont 9 | import androidx.compose.ui.unit.sp 10 | import com.corphish.quicktools.R 11 | 12 | val provider = GoogleFont.Provider( 13 | providerAuthority = "com.google.android.gms.fonts", 14 | providerPackage = "com.google.android.gms", 15 | certificates = R.array.com_google_android_gms_fonts_certs 16 | ) 17 | 18 | val fontName = GoogleFont("Montserrat") 19 | 20 | val BrandFontFamily = FontFamily( 21 | Font(googleFont = fontName, fontProvider = provider, weight = FontWeight.W200), 22 | Font(googleFont = fontName, fontProvider = provider, weight = FontWeight.W300), 23 | Font(googleFont = fontName, fontProvider = provider), 24 | Font(googleFont = fontName, fontProvider = provider, weight = FontWeight.W500), 25 | Font(googleFont = fontName, fontProvider = provider, weight = FontWeight.W600), 26 | ) 27 | 28 | // Set of Material typography styles to start with 29 | val Typography = Typography( 30 | bodyLarge = TextStyle( 31 | fontFamily = FontFamily.Default, 32 | fontWeight = FontWeight.Normal, 33 | fontSize = 16.sp, 34 | lineHeight = 24.sp, 35 | letterSpacing = 0.5.sp 36 | ), 37 | titleLarge = TextStyle( 38 | fontFamily = FontFamily.Default, 39 | fontWeight = FontWeight.Normal, 40 | fontSize = 22.sp, 41 | lineHeight = 28.sp, 42 | letterSpacing = 0.sp 43 | ), 44 | labelSmall = TextStyle( 45 | fontFamily = FontFamily.Default, 46 | fontWeight = FontWeight.Medium, 47 | fontSize = 11.sp, 48 | lineHeight = 16.sp, 49 | letterSpacing = 0.5.sp 50 | ) 51 | ) 52 | 53 | val TypographyV2 = Typography( 54 | headlineLarge = TextStyle( 55 | fontFamily = BrandFontFamily, 56 | fontWeight = FontWeight.W500, 57 | fontSize = 48.sp 58 | ), 59 | headlineMedium = TextStyle( 60 | fontFamily = BrandFontFamily, 61 | fontWeight = FontWeight.Bold, 62 | fontSize = 32.sp 63 | ), 64 | headlineSmall = TextStyle( 65 | fontFamily = BrandFontFamily, 66 | fontWeight = FontWeight.W500, 67 | fontSize = 28.sp 68 | ), 69 | labelLarge = TextStyle( 70 | fontFamily = BrandFontFamily, 71 | fontWeight = FontWeight.W500, 72 | fontSize = 24.sp 73 | ), 74 | labelMedium = TextStyle( 75 | fontFamily = BrandFontFamily, 76 | fontWeight = FontWeight.W500, 77 | fontSize = 16.sp 78 | ), 79 | labelSmall = TextStyle( 80 | fontFamily = BrandFontFamily, 81 | fontWeight = FontWeight.W600, 82 | fontSize = 12.sp 83 | ), 84 | bodyLarge = TextStyle( 85 | fontSize = 16.sp, 86 | letterSpacing = 0.5.sp 87 | ), 88 | bodyMedium = TextStyle( 89 | fontSize = 14.sp, 90 | letterSpacing = 0.5.sp 91 | ), 92 | bodySmall = TextStyle( 93 | fontSize = 12.sp, 94 | letterSpacing = 0.5.sp 95 | ), 96 | displayLarge = TextStyle( 97 | fontFamily = BrandFontFamily, 98 | fontSize = 16.sp, 99 | letterSpacing = 0.5.sp 100 | ), 101 | displayMedium = TextStyle( 102 | fontFamily = BrandFontFamily, 103 | fontSize = 14.sp, 104 | letterSpacing = 0.5.sp 105 | ), 106 | displaySmall = TextStyle( 107 | fontFamily = BrandFontFamily, 108 | fontSize = 12.sp, 109 | letterSpacing = 0.5.sp 110 | ) 111 | ) -------------------------------------------------------------------------------- /app/src/main/java/com/corphish/quicktools/utils/ClipboardHelper.kt: -------------------------------------------------------------------------------- 1 | package com.corphish.quicktools.utils 2 | 3 | import android.content.ClipData 4 | import android.content.ClipboardManager 5 | import android.content.Context 6 | import android.widget.Toast 7 | import androidx.appcompat.app.AppCompatActivity.CLIPBOARD_SERVICE 8 | import com.corphish.quicktools.R 9 | 10 | /** 11 | * Helper to copy to clipboard 12 | */ 13 | object ClipboardHelper { 14 | fun copyToClipboard(context: Context, text: String) { 15 | val clipboard = context.getSystemService(CLIPBOARD_SERVICE) as ClipboardManager 16 | val clip = ClipData.newPlainText("text_tools_result", text) 17 | clipboard.setPrimaryClip(clip) 18 | Toast.makeText(context, R.string.copied_to_clipboard, Toast.LENGTH_LONG).show() 19 | } 20 | } -------------------------------------------------------------------------------- /app/src/main/java/com/corphish/quicktools/viewmodels/EvalViewModel.kt: -------------------------------------------------------------------------------- 1 | package com.corphish.quicktools.viewmodels 2 | 3 | import androidx.lifecycle.ViewModel 4 | import androidx.lifecycle.viewModelScope 5 | import com.corphish.quicktools.data.Result 6 | import com.corphish.quicktools.repository.SettingsRepository 7 | import dagger.hilt.android.lifecycle.HiltViewModel 8 | import kotlinx.coroutines.flow.MutableStateFlow 9 | import kotlinx.coroutines.flow.StateFlow 10 | import kotlinx.coroutines.launch 11 | import net.objecthunter.exp4j.ExpressionBuilder 12 | import java.text.DecimalFormat 13 | import javax.inject.Inject 14 | import kotlin.math.ceil 15 | import kotlin.math.floor 16 | 17 | @HiltViewModel 18 | class EvalViewModel @Inject constructor(private val settingsRepository: SettingsRepository) : 19 | ViewModel() { 20 | private val _evalMode = MutableStateFlow(settingsRepository.getEvaluateResultMode()) 21 | val evalMode: StateFlow = _evalMode 22 | 23 | // To be populated when user finally selects a mode, if at all they select 24 | private var _userSelectedMode = _evalMode.value 25 | 26 | fun denoteModeSelectionByUser(selectedMode: Int) { 27 | this._userSelectedMode = selectedMode 28 | } 29 | 30 | fun denoteUserRememberChoice(choice: Boolean) { 31 | if (choice) { 32 | settingsRepository.setEvaluateResultMode(_userSelectedMode) 33 | } 34 | } 35 | 36 | suspend fun shouldForceCopy(choice: Boolean) { 37 | if (choice) { 38 | _userSelectedMode = EVAL_RESULT_COPY_TO_CLIPBOARD 39 | _evalMode.emit(EVAL_RESULT_COPY_TO_CLIPBOARD) 40 | } 41 | } 42 | 43 | private val _evalResult = MutableStateFlow>(Result.Initial) 44 | val evalResult: StateFlow> = _evalResult 45 | 46 | fun evaluate(text: String) { 47 | viewModelScope.launch { 48 | val decimalPoints = settingsRepository.getDecimalPoints() 49 | try { 50 | val expression = ExpressionBuilder(text).build() 51 | val result = expression.evaluate() 52 | 53 | _evalResult.value = Result.Success( 54 | EvaluateResult( 55 | resultString = if (ceil(result) == floor(result)) { 56 | result.toInt().toString() 57 | } else { 58 | val decimalFormat = DecimalFormat("0.${"#".repeat(decimalPoints)}") 59 | decimalFormat.format(result) 60 | }, 61 | finalMode = _userSelectedMode 62 | ) 63 | ) 64 | } catch (e: Exception) { 65 | _evalResult.value = Result.Error 66 | } 67 | } 68 | } 69 | 70 | companion object { 71 | // Eval result mode choices will be shown to user next time 72 | const val EVAL_RESULT_MODE_ASK_NEXT_TIME = 0 73 | 74 | // Eval result will be replaced by the selected text 75 | const val EVAL_RESULT_REPLACE = 1 76 | 77 | // Eval result will be appended after the selected text with = sign 78 | const val EVAL_RESULT_APPEND = 2 79 | 80 | // Eval result will be copied to the clipboard 81 | const val EVAL_RESULT_COPY_TO_CLIPBOARD = 3 82 | } 83 | } 84 | 85 | data class EvaluateResult( 86 | val resultString: String, 87 | val finalMode: Int, 88 | ) -------------------------------------------------------------------------------- /app/src/main/java/com/corphish/quicktools/viewmodels/MainViewModel.kt: -------------------------------------------------------------------------------- 1 | package com.corphish.quicktools.viewmodels 2 | 3 | import androidx.lifecycle.ViewModel 4 | import androidx.lifecycle.viewModelScope 5 | import com.corphish.quicktools.repository.AppMode 6 | import com.corphish.quicktools.repository.ContextMenuOptionsRepository 7 | import com.corphish.quicktools.repository.FeatureIds 8 | import dagger.hilt.android.lifecycle.HiltViewModel 9 | import kotlinx.coroutines.flow.MutableStateFlow 10 | import kotlinx.coroutines.flow.asStateFlow 11 | import kotlinx.coroutines.launch 12 | import javax.inject.Inject 13 | 14 | @HiltViewModel 15 | class MainViewModel @Inject constructor(private val contextMenuOptionsRepository: ContextMenuOptionsRepository) : 16 | ViewModel() { 17 | private val _enabledFeatures = 18 | MutableStateFlow(listOf()) 19 | val enabledFeatures = _enabledFeatures.asStateFlow() 20 | 21 | private val _appMode = MutableStateFlow(AppMode.SINGLE) 22 | val appMode = _appMode.asStateFlow() 23 | 24 | fun init() { 25 | _enabledFeatures.value = contextMenuOptionsRepository.getCurrentlyEnabledFeatures() 26 | _appMode.value = contextMenuOptionsRepository.getCurrentAppMode() 27 | } 28 | 29 | fun enableOrDisableFeature(feature: FeatureIds, enabled: Boolean) { 30 | viewModelScope.launch { 31 | contextMenuOptionsRepository.enableOrDisableFeature(feature, enabled) 32 | _enabledFeatures.value = contextMenuOptionsRepository.getCurrentlyEnabledFeatures() 33 | } 34 | } 35 | } -------------------------------------------------------------------------------- /app/src/main/java/com/corphish/quicktools/viewmodels/OnBoardingViewModel.kt: -------------------------------------------------------------------------------- 1 | package com.corphish.quicktools.viewmodels 2 | 3 | import androidx.lifecycle.ViewModel 4 | import androidx.lifecycle.viewModelScope 5 | import com.corphish.quicktools.repository.AppMode 6 | import com.corphish.quicktools.repository.ContextMenuOptionsRepository 7 | import com.corphish.quicktools.repository.SettingsRepository 8 | import dagger.hilt.android.lifecycle.HiltViewModel 9 | import kotlinx.coroutines.flow.MutableStateFlow 10 | import kotlinx.coroutines.flow.asStateFlow 11 | import kotlinx.coroutines.launch 12 | import javax.inject.Inject 13 | 14 | @HiltViewModel 15 | class OnBoardingViewModel @Inject constructor( 16 | private val settingsRepository: SettingsRepository, 17 | private val contextOptionsRepository: ContextMenuOptionsRepository, 18 | ) : ViewModel() { 19 | private val _onBoardingDone = MutableStateFlow(settingsRepository.getOnboardingDone()) 20 | val onBoardingDone = _onBoardingDone.asStateFlow() 21 | 22 | fun setOnBoardingDone(done: Boolean) { 23 | viewModelScope.launch { 24 | settingsRepository.setOnboardingDone(done) 25 | _onBoardingDone.value = done 26 | } 27 | } 28 | 29 | fun setAppMode(mode: AppMode) { 30 | viewModelScope.launch { 31 | contextOptionsRepository.setCurrentAppMode(mode) 32 | } 33 | } 34 | } -------------------------------------------------------------------------------- /app/src/main/java/com/corphish/quicktools/viewmodels/OptionsViewModel.kt: -------------------------------------------------------------------------------- 1 | package com.corphish.quicktools.viewmodels 2 | 3 | import androidx.lifecycle.ViewModel 4 | import com.corphish.quicktools.repository.ContextMenuOptionsRepository 5 | import dagger.hilt.android.lifecycle.HiltViewModel 6 | import kotlinx.coroutines.flow.MutableStateFlow 7 | import kotlinx.coroutines.flow.asStateFlow 8 | import javax.inject.Inject 9 | 10 | @HiltViewModel 11 | class OptionsViewModel @Inject constructor(contextMenuOptionsRepository: ContextMenuOptionsRepository) : 12 | ViewModel() { 13 | private val _enabledFeatures = MutableStateFlow(contextMenuOptionsRepository.getCurrentlyEnabledFeatures()) 14 | val enabledFeatures = _enabledFeatures.asStateFlow() 15 | } -------------------------------------------------------------------------------- /app/src/main/java/com/corphish/quicktools/viewmodels/SaveTextViewModel.kt: -------------------------------------------------------------------------------- 1 | package com.corphish.quicktools.viewmodels 2 | 3 | import android.net.Uri 4 | import androidx.lifecycle.ViewModel 5 | import androidx.lifecycle.viewModelScope 6 | import com.corphish.quicktools.data.Result 7 | import com.corphish.quicktools.repository.TextRepository 8 | import dagger.hilt.android.lifecycle.HiltViewModel 9 | import kotlinx.coroutines.flow.MutableStateFlow 10 | import kotlinx.coroutines.flow.StateFlow 11 | import kotlinx.coroutines.launch 12 | import javax.inject.Inject 13 | 14 | @HiltViewModel 15 | class SaveTextViewModel @Inject constructor(private val textRepository: TextRepository) : 16 | ViewModel() { 17 | private val _saveTextStatus = MutableStateFlow>(Result.Initial) 18 | val saveTextStatus: StateFlow> = _saveTextStatus 19 | 20 | fun saveText(uri: Uri?, text: String) { 21 | viewModelScope.launch { 22 | val res = textRepository.writeText(uri, text) 23 | _saveTextStatus.value = if (res) Result.Success(true) else Result.Error 24 | } 25 | } 26 | } -------------------------------------------------------------------------------- /app/src/main/java/com/corphish/quicktools/viewmodels/SettingsViewModel.kt: -------------------------------------------------------------------------------- 1 | package com.corphish.quicktools.viewmodels 2 | 3 | import androidx.lifecycle.ViewModel 4 | import androidx.lifecycle.viewModelScope 5 | import com.corphish.quicktools.BuildConfig 6 | import com.corphish.quicktools.data.Constants 7 | import com.corphish.quicktools.repository.AppMode 8 | import com.corphish.quicktools.repository.ContextMenuOptionsRepository 9 | import com.corphish.quicktools.repository.SettingsRepository 10 | import dagger.hilt.android.lifecycle.HiltViewModel 11 | import kotlinx.coroutines.flow.MutableStateFlow 12 | import kotlinx.coroutines.flow.StateFlow 13 | import kotlinx.coroutines.launch 14 | import javax.inject.Inject 15 | 16 | @HiltViewModel 17 | class SettingsViewModel @Inject constructor( 18 | private val settingsRepository: SettingsRepository, 19 | private val contextMenuOptionsRepository: ContextMenuOptionsRepository, 20 | ): ViewModel() { 21 | private val _prependCountryCodeEnabled = MutableStateFlow(settingsRepository.getPrependCountryCodeEnabled()) 22 | private val _prependCountryCode = MutableStateFlow(settingsRepository.getPrependCountryCode()) 23 | private val _prependCountryCodeIsValid = MutableStateFlow(Constants.COUNTRY_CODE_REGEX.matches(_prependCountryCode.value ?: "")) 24 | private val _decimalPoints = MutableStateFlow(settingsRepository.getDecimalPoints()) 25 | private val _evalResultMode = MutableStateFlow(settingsRepository.getEvaluateResultMode()) 26 | private val _appVersionName = MutableStateFlow(BuildConfig.VERSION_NAME) 27 | private val _appVersionCode = MutableStateFlow(BuildConfig.VERSION_CODE) 28 | private val _appMode = MutableStateFlow(contextMenuOptionsRepository.getCurrentAppMode()) 29 | 30 | val prependCountryCodeEnabled: StateFlow = _prependCountryCodeEnabled 31 | val prependCountryCode: StateFlow = _prependCountryCode 32 | val prependCountryCodeIsValid: StateFlow = _prependCountryCodeIsValid 33 | val decimalPoints: StateFlow = _decimalPoints 34 | val evalResultMode: StateFlow = _evalResultMode 35 | val appVersionName: StateFlow = _appVersionName 36 | val appVersionCode: StateFlow = _appVersionCode 37 | val appMode: StateFlow = _appMode 38 | 39 | fun updatePrependCountryCodeEnabled(enabled: Boolean) { 40 | viewModelScope.launch { 41 | settingsRepository.setPrependCountryCodeEnabled(enabled) 42 | _prependCountryCodeEnabled.value = enabled 43 | } 44 | } 45 | 46 | fun updatePrependCountryCode(code: String) { 47 | viewModelScope.launch { 48 | val match = Constants.COUNTRY_CODE_REGEX.matches(code) 49 | _prependCountryCode.value = code 50 | 51 | if (match) { 52 | settingsRepository.setPrependCountryCode(code) 53 | _prependCountryCodeIsValid.value = true 54 | } else { 55 | _prependCountryCodeIsValid.value = false 56 | } 57 | } 58 | } 59 | 60 | /** 61 | * Should be called when activity is exiting to finalise the country code prepend 62 | * enabled setting. 63 | * Ideally, we turn off country code prepend enabled if the country code is invalid. 64 | */ 65 | fun invalidateCountryCodePrependSetting() { 66 | viewModelScope.launch { 67 | if (!_prependCountryCodeIsValid.value) { 68 | updatePrependCountryCodeEnabled(false) 69 | settingsRepository.setPrependCountryCode("") 70 | } 71 | } 72 | } 73 | 74 | fun updateDecimalPoints(count: Int) { 75 | viewModelScope.launch { 76 | settingsRepository.setDecimalPoints(count) 77 | _decimalPoints.value = count 78 | } 79 | } 80 | 81 | fun updateEvaluateResultMode(mode: Int) { 82 | viewModelScope.launch { 83 | settingsRepository.setEvaluateResultMode(mode) 84 | _evalResultMode.value = mode 85 | } 86 | } 87 | 88 | fun updateAppMode(mode: AppMode) { 89 | viewModelScope.launch { 90 | contextMenuOptionsRepository.setCurrentAppMode(mode) 91 | _appMode.value = mode 92 | } 93 | } 94 | } -------------------------------------------------------------------------------- /app/src/main/java/com/corphish/quicktools/viewmodels/TextCountViewModel.kt: -------------------------------------------------------------------------------- 1 | package com.corphish.quicktools.viewmodels 2 | 3 | import androidx.lifecycle.ViewModel 4 | import androidx.lifecycle.viewModelScope 5 | import kotlinx.coroutines.flow.MutableStateFlow 6 | import kotlinx.coroutines.flow.asStateFlow 7 | import kotlinx.coroutines.launch 8 | 9 | class TextCountViewModel : ViewModel() { 10 | private val _text = MutableStateFlow("") 11 | val text = _text.asStateFlow() 12 | 13 | private val _characterCount = MutableStateFlow(0) 14 | val characterCount = _characterCount.asStateFlow() 15 | 16 | private val _letterCount = MutableStateFlow(0) 17 | val letterCount = _letterCount.asStateFlow() 18 | 19 | private val _digitCount = MutableStateFlow(0) 20 | val digitCount = _digitCount.asStateFlow() 21 | 22 | private val _wordCount = MutableStateFlow(0) 23 | val wordCount = _wordCount.asStateFlow() 24 | 25 | private val _spaceCount = MutableStateFlow(0) 26 | val spaceCount = _spaceCount.asStateFlow() 27 | 28 | private val _symbolCount = MutableStateFlow(0) 29 | val symbolCount = _symbolCount.asStateFlow() 30 | 31 | private val _wordFrequency = MutableStateFlow(listOf>()) 32 | val wordFrequency = _wordFrequency.asStateFlow() 33 | 34 | fun setTextAndProcess(text: String) { 35 | viewModelScope.launch { 36 | _text.value = text 37 | 38 | val freq = mutableMapOf() 39 | val list = mutableListOf>() 40 | 41 | _characterCount.value = 0 42 | _letterCount.value = 0 43 | _digitCount.value = 0 44 | _wordCount.value = 0 45 | _spaceCount.value = 0 46 | _symbolCount.value = 0 47 | _wordFrequency.value = mutableListOf() 48 | 49 | _characterCount.value = text.length 50 | for (c in text.toCharArray()) { 51 | if (c == ' ') { 52 | _spaceCount.value += 1 53 | } else if (Character.isLetter(c)) { 54 | _letterCount.value += 1 55 | } else if (Character.isDigit(c)) { 56 | _digitCount.value += 1 57 | } else { 58 | _symbolCount.value += 1 59 | } 60 | } 61 | 62 | for (w in text.split(" ")) { 63 | if (w.isEmpty()) { 64 | continue 65 | } 66 | 67 | _wordCount.value += 1 68 | freq[w] = (freq[w] ?: 0) + 1 69 | } 70 | 71 | for (e in freq.entries) { 72 | list += e.key to e.value 73 | } 74 | 75 | list.sortBy { -it.second } 76 | _wordFrequency.value = list.toList() 77 | } 78 | } 79 | } -------------------------------------------------------------------------------- /app/src/main/java/com/corphish/quicktools/viewmodels/TextReplacementViewModel.kt: -------------------------------------------------------------------------------- 1 | package com.corphish.quicktools.viewmodels 2 | 3 | import androidx.compose.ui.text.TextRange 4 | import androidx.lifecycle.ViewModel 5 | import androidx.lifecycle.viewModelScope 6 | import com.corphish.quicktools.text.TextReplacementManager 7 | import kotlinx.coroutines.flow.MutableStateFlow 8 | import kotlinx.coroutines.flow.asStateFlow 9 | import kotlinx.coroutines.launch 10 | 11 | class TextReplacementViewModel : ViewModel() { 12 | private lateinit var _textReplacementManager: TextReplacementManager 13 | 14 | // Selected text can be edited further by the user 15 | private val _mainText = MutableStateFlow("") 16 | val mainText = _mainText.asStateFlow() 17 | 18 | // Find and replace text states 19 | private val _findText = MutableStateFlow("") 20 | private val _replaceText = MutableStateFlow("") 21 | private val _ignoreCase = MutableStateFlow(false) 22 | val findText = _findText.asStateFlow() 23 | val replaceText = _replaceText.asStateFlow() 24 | val ignoreCase = _ignoreCase.asStateFlow() 25 | 26 | // Counter states 27 | private val _counterIndex = MutableStateFlow(0) 28 | private val _counterTotal = MutableStateFlow(0) 29 | val counterIndex = _counterIndex.asStateFlow() 30 | val counterTotal = _counterTotal.asStateFlow() 31 | 32 | // Find range state 33 | private val _findRanges = MutableStateFlow(listOf()) 34 | val findRanges = _findRanges.asStateFlow() 35 | 36 | // Undo and redo states 37 | private val _undoState = MutableStateFlow(false) 38 | private val _redoState = MutableStateFlow(false) 39 | val undoState = _undoState.asStateFlow() 40 | val redoState = _redoState.asStateFlow() 41 | 42 | fun initializeWith(originalString: String) { 43 | _textReplacementManager = TextReplacementManager(originalString) 44 | _mainText.value = originalString 45 | } 46 | 47 | private fun invokeFind() { 48 | if (_findText.value.isNotEmpty()) { 49 | _findRanges.value = findText(_mainText.value, _findText.value, _ignoreCase.value) 50 | _counterIndex.value = 0 51 | _counterTotal.value = _findRanges.value.size 52 | } else { 53 | _findRanges.value = listOf() 54 | _counterIndex.value = 0 55 | _counterTotal.value = 0 56 | } 57 | 58 | _undoState.value = _textReplacementManager.canUndo() 59 | _redoState.value = _textReplacementManager.canRedo() 60 | } 61 | 62 | fun undo() { 63 | viewModelScope.launch { 64 | _mainText.value = _textReplacementManager.undo() 65 | invokeFind() 66 | } 67 | } 68 | 69 | fun redo() { 70 | viewModelScope.launch { 71 | _mainText.value = _textReplacementManager.redo() 72 | invokeFind() 73 | } 74 | } 75 | 76 | fun reset() { 77 | viewModelScope.launch { 78 | _mainText.value = _textReplacementManager.reset() 79 | _findRanges.value = listOf() 80 | _counterIndex.value = 0 81 | _counterTotal.value = 0 82 | _undoState.value = _textReplacementManager.canUndo() 83 | _redoState.value = _textReplacementManager.canRedo() 84 | _findText.value = "" 85 | _replaceText.value = "" 86 | } 87 | } 88 | 89 | fun replaceFirst() { 90 | viewModelScope.launch { 91 | _mainText.value = _textReplacementManager.replaceOne( 92 | _findRanges.value[_counterIndex.value], 93 | _replaceText.value 94 | ) 95 | invokeFind() 96 | } 97 | } 98 | 99 | fun replaceAll() { 100 | viewModelScope.launch { 101 | _mainText.value = _textReplacementManager.replaceAll( 102 | _findText.value, 103 | _replaceText.value, 104 | _ignoreCase.value 105 | ) 106 | invokeFind() 107 | } 108 | } 109 | 110 | fun setFindText(text: String) { 111 | viewModelScope.launch { 112 | _findText.value = text 113 | invokeFind() 114 | } 115 | } 116 | 117 | fun setReplaceText(text: String) { 118 | viewModelScope.launch { 119 | _replaceText.value = text 120 | } 121 | } 122 | 123 | fun setIgnoreCase(state: Boolean) { 124 | viewModelScope.launch { 125 | _ignoreCase.value = state 126 | invokeFind() 127 | } 128 | } 129 | 130 | fun updateMainText(text: String) { 131 | viewModelScope.launch { 132 | _mainText.value = text 133 | _textReplacementManager.updateText(text) 134 | _undoState.value = _textReplacementManager.canUndo() 135 | _redoState.value = _textReplacementManager.canRedo() 136 | } 137 | } 138 | 139 | fun decrementCounter() { 140 | viewModelScope.launch { 141 | _counterIndex.value = (_counterIndex.value - 1) % _counterTotal.value 142 | if (_counterIndex.value < 0) { 143 | _counterIndex.value = _counterTotal.value - 1 144 | } 145 | } 146 | } 147 | 148 | fun incrementCounter() { 149 | viewModelScope.launch { 150 | _counterIndex.value = (_counterIndex.value + 1) % _counterTotal.value 151 | } 152 | } 153 | 154 | private fun findText( 155 | mainInput: String, 156 | findText: String, 157 | ignoreCase: Boolean 158 | ): List { 159 | val result = mutableListOf() 160 | var index = -findText.length 161 | do { 162 | index = mainInput.indexOf( 163 | findText, 164 | startIndex = index + findText.length, 165 | ignoreCase = ignoreCase 166 | ) 167 | if (index != -1) { 168 | result += TextRange(start = index, end = index + findText.length) 169 | } 170 | } while (index >= 0) 171 | 172 | return result 173 | } 174 | } -------------------------------------------------------------------------------- /app/src/main/java/com/corphish/quicktools/viewmodels/WUPViewModel.kt: -------------------------------------------------------------------------------- 1 | package com.corphish.quicktools.viewmodels 2 | 3 | import androidx.lifecycle.ViewModel 4 | import androidx.lifecycle.viewModelScope 5 | import com.corphish.quicktools.data.Constants 6 | import com.corphish.quicktools.data.Result 7 | import com.corphish.quicktools.repository.SettingsRepository 8 | import dagger.hilt.android.lifecycle.HiltViewModel 9 | import kotlinx.coroutines.flow.MutableStateFlow 10 | import kotlinx.coroutines.flow.StateFlow 11 | import kotlinx.coroutines.launch 12 | import javax.inject.Inject 13 | 14 | @HiltViewModel 15 | class WUPViewModel @Inject constructor(private val settingsRepository: SettingsRepository): ViewModel() { 16 | // Nullable type to denote when phone number is invalid 17 | private val _processedPhoneNumber = MutableStateFlow>(Result.Initial) 18 | val processedPhoneNumber: StateFlow> = _processedPhoneNumber 19 | 20 | fun determinePhoneNumber(data: String?) { 21 | viewModelScope.launch { 22 | if (data == null) { 23 | _processedPhoneNumber.value = Result.Error 24 | } else { 25 | val modified = specialCharactersRemovedFrom(data) 26 | val regex = Regex(Constants.PHONE_NUMBER_REGEX) 27 | if (regex.matches(modified)) { 28 | _processedPhoneNumber.value = Result.Success(countryCodedNumber(modified)) 29 | } else { 30 | _processedPhoneNumber.value = Result.Error 31 | } 32 | } 33 | } 34 | } 35 | 36 | private fun countryCodedNumber(phoneNumber: String): String { 37 | return if (settingsRepository.getPrependCountryCodeEnabled()) { 38 | val code = settingsRepository.getPrependCountryCode() 39 | if (code == null) { 40 | phoneNumber 41 | } else if (phoneNumber.startsWith(code)) { 42 | // If the number already starts with country code, no need to append. 43 | phoneNumber 44 | } else if (phoneNumber.startsWith("+")) { 45 | // If the number starts with some country code that is not the user specified 46 | // country code, it must be considered. 47 | phoneNumber 48 | } else { 49 | "$code$phoneNumber" 50 | } 51 | } else { 52 | phoneNumber 53 | } 54 | } 55 | 56 | private fun specialCharactersRemovedFrom(phoneNumber: String) : String { 57 | var res = phoneNumber 58 | for (char in Constants.PHONE_NUMBER_SPECIAL_CHARACTERS) { 59 | res = res.replace(char, "") 60 | } 61 | 62 | return res 63 | } 64 | } -------------------------------------------------------------------------------- /app/src/main/res/drawable/ic_copy.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/ic_done.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/ic_done_all.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/ic_find_and_replace.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/ic_launcher_foreground.xml: -------------------------------------------------------------------------------- 1 | 7 | 11 | 14 | 15 | 16 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/ic_next.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/ic_numbers.xml: -------------------------------------------------------------------------------- 1 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/ic_open_in_new.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/ic_previous.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/ic_redo.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/ic_reset.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/ic_save.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/ic_text_count.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/ic_text_transform.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/ic_undo.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/ic_whatsapp.xml: -------------------------------------------------------------------------------- 1 | 8 | 11 | 12 | -------------------------------------------------------------------------------- /app/src/main/res/mipmap-anydpi/ic_launcher.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /app/src/main/res/mipmap-anydpi/ic_launcher_round.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /app/src/main/res/mipmap-hdpi/ic_launcher.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/corphish/TextTools/743de19a692652e3f6d050e8d978180be0e66fc6/app/src/main/res/mipmap-hdpi/ic_launcher.webp -------------------------------------------------------------------------------- /app/src/main/res/mipmap-hdpi/ic_launcher_round.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/corphish/TextTools/743de19a692652e3f6d050e8d978180be0e66fc6/app/src/main/res/mipmap-hdpi/ic_launcher_round.webp -------------------------------------------------------------------------------- /app/src/main/res/mipmap-mdpi/ic_launcher.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/corphish/TextTools/743de19a692652e3f6d050e8d978180be0e66fc6/app/src/main/res/mipmap-mdpi/ic_launcher.webp -------------------------------------------------------------------------------- /app/src/main/res/mipmap-mdpi/ic_launcher_round.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/corphish/TextTools/743de19a692652e3f6d050e8d978180be0e66fc6/app/src/main/res/mipmap-mdpi/ic_launcher_round.webp -------------------------------------------------------------------------------- /app/src/main/res/mipmap-xhdpi/ic_launcher.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/corphish/TextTools/743de19a692652e3f6d050e8d978180be0e66fc6/app/src/main/res/mipmap-xhdpi/ic_launcher.webp -------------------------------------------------------------------------------- /app/src/main/res/mipmap-xhdpi/ic_launcher_round.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/corphish/TextTools/743de19a692652e3f6d050e8d978180be0e66fc6/app/src/main/res/mipmap-xhdpi/ic_launcher_round.webp -------------------------------------------------------------------------------- /app/src/main/res/mipmap-xxhdpi/ic_launcher.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/corphish/TextTools/743de19a692652e3f6d050e8d978180be0e66fc6/app/src/main/res/mipmap-xxhdpi/ic_launcher.webp -------------------------------------------------------------------------------- /app/src/main/res/mipmap-xxhdpi/ic_launcher_round.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/corphish/TextTools/743de19a692652e3f6d050e8d978180be0e66fc6/app/src/main/res/mipmap-xxhdpi/ic_launcher_round.webp -------------------------------------------------------------------------------- /app/src/main/res/mipmap-xxxhdpi/ic_launcher.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/corphish/TextTools/743de19a692652e3f6d050e8d978180be0e66fc6/app/src/main/res/mipmap-xxxhdpi/ic_launcher.webp -------------------------------------------------------------------------------- /app/src/main/res/mipmap-xxxhdpi/ic_launcher_round.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/corphish/TextTools/743de19a692652e3f6d050e8d978180be0e66fc6/app/src/main/res/mipmap-xxxhdpi/ic_launcher_round.webp -------------------------------------------------------------------------------- /app/src/main/res/resources.properties: -------------------------------------------------------------------------------- 1 | unqualifiedResLocale=en-US -------------------------------------------------------------------------------- /app/src/main/res/values-es/strings.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | Herramientas de texto 4 | Colección de funciones útiles relacionadas con texto a las que se puede acceder desde muchas aplicaciones dentro del sistema Android seleccionando un texto dentro de ellas. Las opciones admitidas aparecen en el menú contextual que aparece después de seleccionar un texto. Es posible que tengas que revelarlos presionando el ícono de 3 puntos en el menú contextual. La lista de funciones compatibles seguirá creciendo. 5 | Seleccione la función que le gustaría realizar. Texto seleccionado - %1$s 6 | Caracteristicas 7 | 8 | -------------------------------------------------------------------------------- /app/src/main/res/values-ko/strings.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 텍스트 툴즈 4 | 다양한 앱들에서 사용할 수 있는, 텍스트를 선택하면 나오는 안드로이드의 컨텍스트 메뉴 기능을 확장해 선택한 텍스트와 관련해 유용한 기능들을 사용할 수 있도록 합니다. 컨텍스트 메뉴의 3개의 점 버튼을 누르는 것을 통해 접근할 수 있으며, 더 다양한 기능을 추가할 예정입니다. 5 | 선택하신 %1$s 에 대해 실행하고자 하는 기능을 선택하세요. 6 | 기능 7 | 컨텍스트 메뉴 옵션: 8 | 왓츠앱 관련 9 | 왓츠앱 10 | 선택한 연락처를 저장하지 않고 바로 왓츠앱으로 전송 11 | 선택한 연락처로 저장 없이 메시지를 전송합니다. 일회성으로 정보를 보내는 데 유용합니다. 12 | 수식 계산 13 | 선택한 수식의 결과 계산 14 | 계산하기 15 | 선택한 수식을 계산해 결과로 대체합니다. 편집 가능한 텍스트 영역에서만 동작합니다. 16 | 계산 가능한 올바른 수식 표현이 아닙니다. 17 | 기능 시험해보기 18 | - 텍스트 필드에 임의의 전화번호를 입력해보세요.\n- 길게 누르고 블록 영역을 조절해, 입력한 전화번호를 선택하세요.\n- [T\u00B2] 의 왓츠앱을 선택하세요. 보이지 않는 경우, 3개의 점을 눌러 해당 버튼이 나타나게 해야 합니다.\n- 기기에 왓츠앱이 설치되어 있고, 전화번호의 사용자가 왓츠앱을 사용중인 경우 입력한 번호 상대방과의 대화창이 열립니다. 19 | - 텍스트 필드에 (1+2^3+4*5)의 괄호 안과 같은 간단한 수식을 입력해보세요.\n- 길게 누르고 블록 영역을 조절해, 입력한 수식을 선택하세요.\n- [T\u00B2] 의 계산하기를 선택하세요. 보이지 않는 경우, 3개의 점을 눌러 해당 버튼이 나타나게 해야 합니다.\n- 인식 가능한 형태로 입력한 수식의 결과를 확인 가능합니다. 20 | 예상치 못한 에러가 발생했습니다. 21 | 잘 따라오셨습니다! 이제 어느 앱에서도 텍스트를 선택할 수 있다면 같은 작업을 하실 수 있습니다. 22 | 제대로 동작하지 않는 경우, GitHub의 Issue에 해당 문제를 등록해 다음 버전에서 문제를 해결하도록 할 수 있습니다. 지금 진행하시겠습니까? 23 | 작동했습니다. 24 | 작동에 문제가 발생했습니다. 25 | 앱 정보 26 | 버전: %1$s (%2$d) 27 | 오픈 소스 28 | 이 앱은 오픈 소스 프로젝트입니다. 활발한 개발 참여나 문제 보고가 다음 버전에 큰 도움이 됩니다! 29 | 확인하기 30 | 유효하지 않은 형식의 전화번호입니다. 31 | 설정 32 | 전화번호 앞에 자동으로 국가코드 추가하기 33 | 선택한 전화번호에 국가코드가 없는 경우, 이 옵션을 활성화할 경우 입력한 국가코드를 자동으로 앞에 추가합니다. 34 | 메시징 35 | 자동추가 국가코드값 36 | 소수점 자리수 37 | 수식 계산 결과로 정수가 아닌 값이 나올 경우, 표시할 소수점의 자리수입니다. 결과가 정수면 표시하지 않습니다. 38 | 앱 버전목록 39 | 변환하기 40 | 변환하기 41 | 선택한 문자열을 어떤 형식으로 변환할지 선택해주세요. 42 | 문자열 변환하기 43 | 선택한 문자열의 대소문자 변환, 다른 문자로 둘러싸기 등의 다양한 기능을 제공합니다. 44 | 문자열 둘러싸기 45 | 대소문자 변경 46 | 선택한 문자열을 감쌀 기호를 선택하세요. 47 | \'\' (작은 따옴표) 48 | \"\" (큰따옴표) 49 | () (소괄호) 50 | {} (중괄호) 51 | [] (대괄호) 52 | 사용자 지정 기호 또는 문자열 53 | 문자열을 감싸는데 단순 문자뿐만이 아니라 숫자 및 다른 문자열도 사용 가능합니다. 커스텀으로 입력하신 문자열 중 앞쪽 절반이 선택한 문자열의 앞을, 뒤쪽 절반이 뒤를 감싸게 됩니다. 54 | 커스텀 문자열을 절반으로 나눠 선택한 문자열의 앞뒤를 감싸는 구조이기 때문에, 전체 커스텀 문자열은 짝수개의 문자들이어야 합니다. 55 | 선택한 문자열을 어떤 대소문자 형식으로 변환할지 선택해주세요. (한글은 대소문자 개념이 없습니다) 56 | 대문자 57 | 소문자 58 | 제목 (첫 글자만 대문자) 59 | 제목 (전체 대문자) 60 | 대소문자 무작위 61 | 도움을 주신 분들 62 | 미리보기 활성화 63 | 결과: %1$s 64 | 이 기능은 편집 가능한 텍스트 영역에서만 동작합니다. 65 | 프리뷰는 전체 중 일부만을 보여주며, 변환은 선택하신 모든 영역에 전체 적용됩니다. 66 | 67 | -------------------------------------------------------------------------------- /app/src/main/res/values-pt-rBR/strings.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | Ferramentas de Texto 4 | Coleção de funções úteis relacionadas a texto que podem ser acessadas em vários aplicativos dentro do sistema Android selecionando um texto dentro dele. As opções suportadas aparecem no menu de contexto que aparece depois de selecionar um texto. Você pode ter que revelá-los pressionando o ícone de 3 pontos no menu de contexto. A lista de funções suportadas continuará crescendo. 5 | Coleção de funções úteis relacionadas com texto que podem ser acessadas em vários aplicativos dentro do sistema Android selecionando um texto dentro no campo de texto 6 | Selecione a função que você gostaria de executar. Texto selecionado - %1$s 7 | Funções 8 | Opção do menu contexto: 9 | [T\u00B2] WhatsApp 10 | WhatsApp 11 | Conversar com números não salvos no WhatsApp 12 | Escreva mensagens facilmente para qualquer número não salvo no WhatsApp. Útil se você quer mandar mensagens apenas brevemente. 13 | [T\u00B2] Avaliar 14 | Calcular expressões matemáticas 15 | Avaliar 16 | Avalie expressões matemáticas no ato e substitua a expressão por seu resultado. Funciona apenas no texto selecionado que possa ser editado. 17 | Expressão matemática inválida 18 | Experimentar 19 | - No campo de texto abaixo, digite qualquer número de telefone.\n- Toque e mantenha pressionado o número de telefone que acabou de digitar.\n- Toque na opção [T\u00B2] WhatsApp. Se a opção não estiver visível, toque nos três pontos para revelá-la.\n- Se o número de telefone estiver disponível no WhatsApp (e se o WhatsApp estiver instalado), ele deverá abrir o bate-papo no WhatsApp. 20 | - No campo de texto abaixo, digite qualquer expressão matemática (por exemplo, 1 + 2 + 3).\n- Toque e mantenha pressionada a expressão que acabou de digitar.\n- Toque na opção [T\u00B2] Avaliar. Se a opção não estiver visível, toque nos 3 pontos para revelá-la.\n- Se a expressão matemática digitada for válida, você verá o resultado. 21 | Ocorreu um erro inesperado 22 | Ótimo! Você pode fazer o mesmo dentro de qualquer aplicativo, contanto que você possa selecionar um texto. 23 | Se não funcionou, você pode levantar uma questão no repositório do Github. Gostaria de fazer isso agora? 24 | Funcionou 25 | Não funcionou 26 | Informação do app 27 | Versão - %1$s (%2$d) 28 | Código Aberto 29 | Este app tem código aberto! Sinta-se à vontade para contribuir, levantar problemas e conferir. 30 | Dar uma olhada 31 | Número de telefone inválido 32 | Configurações 33 | Adicionar código do país automaticamente 34 | Se o número de telefone selecionado não tiver um código de país, o código de país predefinido será adicionado automaticamente se habilitado. 35 | Mensagens 36 | Código de país predefinido 37 | Casas decimais 38 | Número de dígitos mostrados após o ponto decimal como resultado de uma função avaliada. Se o resultado for um inteiro (número inteiro sem número decimal), o ponto decimal não será mostrado. 39 | Página de compilações 40 | [T\u00B2] Transformar 41 | Transformar 42 | Selecione como você deseja transformar o texto 43 | Transformar texto 44 | Fornece várias maneiras de transformar o texto selecionado como mudar maiúsculas/minúsculas, cercá-lo com outro texto e muito mais. 45 | Envolver texto 46 | Alterar caixa 47 | Selecione o caractere com o qual você gostaria de encapsular o texto 48 | \'\' (Vírgula invertida) 49 | \"\" (Vírgula invertida dupla) 50 | () (Parênteses) 51 | {} (Chaves) 52 | [] (Colchetes) 53 | Invólucro personalizado 54 | O invólucro de texto personalizado deve conter um número par de letras. A primeira metade irá ao começo e a segunda metade irá ao final do texto selecionado. 55 | Um invólucro de texto personalizado deve conter um número par de letras 56 | Escolha como você deseja transformar o texto 57 | MAIÚSCULAS 58 | minúsculas 59 | Maiúscula (apenas primeira palavra) 60 | Maiúsculas (Todas As Palavras) 61 | CaPiTaLiZaÇãO AlEaTóRiA 62 | Colaboradores 63 | Ativar prévia 64 | Resultado - %1$s 65 | Esta opção funciona apenas em entradas de texto editáveis 66 | O resultado pré-visualizado está abreviado. A transformação será aplicada a toda a seleção. 67 | Doar 68 | Você gosta deste app? Se quiser, você pode fazer uma pequena doação. Isso ajuda a manter-me motivado e a trabalhar neste projeto para torná-lo ainda melhor. 69 | [T\u00B2] Salvar 70 | Salvar texto 71 | Salvar o texto selecionado para que você possa acessá-lo mais tarde 72 | Permite que você salve o texto selecionado (para algum arquivo) para que você possa acessá-lo mais tarde. 73 | Como arquivo .txt 74 | Como uma nota 75 | Nenhum aplicativo de anotações encontrado 76 | Tomada de notas não suportada 77 | O texto selecionado foi salvo com sucesso 78 | Falha ao salvar o texto 79 | Localizar e Substituir 80 | [T\u00B2] Localizar e Substituir 81 | Substituir 82 | Substituir Todos 83 | Localizar Próxima (%1$d/%2$d) 84 | Substituir 85 | Localizar 86 | Ignorar maiúsculas/minúsculas 87 | Desfazer 88 | Salvar 89 | Aplicar 90 | Redefinir 91 | Refazer 92 | Encontre rapidamente um determinado texto e os substitua pelo texto da sua escolha no texto selecionado. 93 | Selecione o que você gostaria de fazer com o resultado 94 | Substituir o texto selecionado pelo resultado 95 | Acrescentar resultado ao texto selecionado 96 | Copiar resultado para área de transferência 97 | Lembrar desta escolha 98 | Não tenho certeza, pergunte-me da próxima vez 99 | Manipulação de resultados 100 | Copiado para a área de transferência 101 | Ordenar linhas 102 | Contar texto 103 | Repetir texto 104 | Remover texto 105 | Adicionar prefixo/sufixo 106 | Número de linhas 107 | Inverter texto 108 | Pré-visualizar 109 | Entrar com texto 110 | Nenhum 111 | Remover primeira 112 | Remover última 113 | Remover tudo 114 | Prefixo 115 | Sufixo 116 | Decorar texto 117 | Negrito (serif) 118 | Itálico (serif) 119 | Negrito/Itálico (serif) 120 | Negrito (sans) 121 | Itálico (sans) 122 | Negrito/Itálico (sans) 123 | Riscado (curto) 124 | Riscado (longo) 125 | Palavras inversas 126 | Linhas inversas 127 | Remover espaços em branco 128 | Remover quebras de linha 129 | Remover linhas em branco 130 | Remover palavras duplicadas 131 | Remover palavras duplicadas (maiúsculas e minúsculas) 132 | Remover linhas duplicadas 133 | Remover linhas duplicadas (maiúsculas e minúsculas) 134 | Cursiva 135 | Contagem de Texto 136 | Caracteres 137 | Letras 138 | Palavras 139 | Dígitos 140 | Espaços 141 | Outros símbolos 142 | Frequência de palavras 143 | Estatísticas 144 | [T\u00B2] Contagem de Texto 145 | Obtenha uma análise detalhada sobre a contagem de caracteres, contagem de palavras, contagem de letras e muito mais para o texto selecionado. 146 | Tutorial 147 | Opções do menu de contexto 148 | Escolha como você deseja que as opções do menu de contexto apareçam quando você selecionar o texto. Ele também pode ser alterado mais tarde nas configurações. 149 | Opção única 150 | Apenas uma opção de menu de contexto aparecerá que quando clicado revelará todas as ações suportadas 151 | Múltiplas opções 152 | Todas as ações suportadas pelo aplicativo serão mostradas no menu de contexto 153 | Também é possível escolher quais opções devem estar disponíveis no menu de contexto. 154 | Editar 155 | Feito 156 | App 157 | Código de país inválido 158 | 159 | -------------------------------------------------------------------------------- /app/src/main/res/values/colors.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | -------------------------------------------------------------------------------- /app/src/main/res/values/font_certs.xml: -------------------------------------------------------------------------------- 1 | 2 | 17 | 18 | 19 | @array/com_google_android_gms_fonts_certs_dev 20 | @array/com_google_android_gms_fonts_certs_prod 21 | 22 | 23 | 24 | MIIEqDCCA5CgAwIBAgIJANWFuGx90071MA0GCSqGSIb3DQEBBAUAMIGUMQswCQYDVQQGEwJVUzETMBEGA1UECBMKQ2FsaWZvcm5pYTEWMBQGA1UEBxMNTW91bnRhaW4gVmlldzEQMA4GA1UEChMHQW5kcm9pZDEQMA4GA1UECxMHQW5kcm9pZDEQMA4GA1UEAxMHQW5kcm9pZDEiMCAGCSqGSIb3DQEJARYTYW5kcm9pZEBhbmRyb2lkLmNvbTAeFw0wODA0MTUyMzM2NTZaFw0zNTA5MDEyMzM2NTZaMIGUMQswCQYDVQQGEwJVUzETMBEGA1UECBMKQ2FsaWZvcm5pYTEWMBQGA1UEBxMNTW91bnRhaW4gVmlldzEQMA4GA1UEChMHQW5kcm9pZDEQMA4GA1UECxMHQW5kcm9pZDEQMA4GA1UEAxMHQW5kcm9pZDEiMCAGCSqGSIb3DQEJARYTYW5kcm9pZEBhbmRyb2lkLmNvbTCCASAwDQYJKoZIhvcNAQEBBQADggENADCCAQgCggEBANbOLggKv+IxTdGNs8/TGFy0PTP6DHThvbbR24kT9ixcOd9W+EaBPWW+wPPKQmsHxajtWjmQwWfna8mZuSeJS48LIgAZlKkpFeVyxW0qMBujb8X8ETrWy550NaFtI6t9+u7hZeTfHwqNvacKhp1RbE6dBRGWynwMVX8XW8N1+UjFaq6GCJukT4qmpN2afb8sCjUigq0GuMwYXrFVee74bQgLHWGJwPmvmLHC69EH6kWr22ijx4OKXlSIx2xT1AsSHee70w5iDBiK4aph27yH3TxkXy9V89TDdexAcKk/cVHYNnDBapcavl7y0RiQ4biu8ymM8Ga/nmzhRKya6G0cGw8CAQOjgfwwgfkwHQYDVR0OBBYEFI0cxb6VTEM8YYY6FbBMvAPyT+CyMIHJBgNVHSMEgcEwgb6AFI0cxb6VTEM8YYY6FbBMvAPyT+CyoYGapIGXMIGUMQswCQYDVQQGEwJVUzETMBEGA1UECBMKQ2FsaWZvcm5pYTEWMBQGA1UEBxMNTW91bnRhaW4gVmlldzEQMA4GA1UEChMHQW5kcm9pZDEQMA4GA1UECxMHQW5kcm9pZDEQMA4GA1UEAxMHQW5kcm9pZDEiMCAGCSqGSIb3DQEJARYTYW5kcm9pZEBhbmRyb2lkLmNvbYIJANWFuGx90071MAwGA1UdEwQFMAMBAf8wDQYJKoZIhvcNAQEEBQADggEBABnTDPEF+3iSP0wNfdIjIz1AlnrPzgAIHVvXxunW7SBrDhEglQZBbKJEk5kT0mtKoOD1JMrSu1xuTKEBahWRbqHsXclaXjoBADb0kkjVEJu/Lh5hgYZnOjvlba8Ld7HCKePCVePoTJBdI4fvugnL8TsgK05aIskyY0hKI9L8KfqfGTl1lzOv2KoWD0KWwtAWPoGChZxmQ+nBli+gwYMzM1vAkP+aayLe0a1EQimlOalO762r0GXO0ks+UeXde2Z4e+8S/pf7pITEI/tP+MxJTALw9QUWEv9lKTk+jkbqxbsh8nfBUapfKqYn0eidpwq2AzVp3juYl7//fKnaPhJD9gs= 25 | 26 | 27 | 28 | 29 | MIIEQzCCAyugAwIBAgIJAMLgh0ZkSjCNMA0GCSqGSIb3DQEBBAUAMHQxCzAJBgNVBAYTAlVTMRMwEQYDVQQIEwpDYWxpZm9ybmlhMRYwFAYDVQQHEw1Nb3VudGFpbiBWaWV3MRQwEgYDVQQKEwtHb29nbGUgSW5jLjEQMA4GA1UECxMHQW5kcm9pZDEQMA4GA1UEAxMHQW5kcm9pZDAeFw0wODA4MjEyMzEzMzRaFw0zNjAxMDcyMzEzMzRaMHQxCzAJBgNVBAYTAlVTMRMwEQYDVQQIEwpDYWxpZm9ybmlhMRYwFAYDVQQHEw1Nb3VudGFpbiBWaWV3MRQwEgYDVQQKEwtHb29nbGUgSW5jLjEQMA4GA1UECxMHQW5kcm9pZDEQMA4GA1UEAxMHQW5kcm9pZDCCASAwDQYJKoZIhvcNAQEBBQADggENADCCAQgCggEBAKtWLgDYO6IIrgqWbxJOKdoR8qtW0I9Y4sypEwPpt1TTcvZApxsdyxMJZ2JORland2qSGT2y5b+3JKkedxiLDmpHpDsz2WCbdxgxRczfey5YZnTJ4VZbH0xqWVW/8lGmPav5xVwnIiJS6HXk+BVKZF+JcWjAsb/GEuq/eFdpuzSqeYTcfi6idkyugwfYwXFU1+5fZKUaRKYCwkkFQVfcAs1fXA5V+++FGfvjJ/CxURaSxaBvGdGDhfXE28LWuT9ozCl5xw4Yq5OGazvV24mZVSoOO0yZ31j7kYvtwYK6NeADwbSxDdJEqO4k//0zOHKrUiGYXtqw/A0LFFtqoZKFjnkCAQOjgdkwgdYwHQYDVR0OBBYEFMd9jMIhF1Ylmn/Tgt9r45jk14alMIGmBgNVHSMEgZ4wgZuAFMd9jMIhF1Ylmn/Tgt9r45jk14aloXikdjB0MQswCQYDVQQGEwJVUzETMBEGA1UECBMKQ2FsaWZvcm5pYTEWMBQGA1UEBxMNTW91bnRhaW4gVmlldzEUMBIGA1UEChMLR29vZ2xlIEluYy4xEDAOBgNVBAsTB0FuZHJvaWQxEDAOBgNVBAMTB0FuZHJvaWSCCQDC4IdGZEowjTAMBgNVHRMEBTADAQH/MA0GCSqGSIb3DQEBBAUAA4IBAQBt0lLO74UwLDYKqs6Tm8/yzKkEu116FmH4rkaymUIE0P9KaMftGlMexFlaYjzmB2OxZyl6euNXEsQH8gjwyxCUKRJNexBiGcCEyj6z+a1fuHHvkiaai+KL8W1EyNmgjmyy8AW7P+LLlkR+ho5zEHatRbM/YAnqGcFh5iZBqpknHf1SKMXFh4dd239FJ1jWYfbMDMy3NS5CTMQ2XFI1MvcyUTdZPErjQfTbQe3aDQsQcafEQPD+nqActifKZ0Np0IS9L9kR/wbNvyz6ENwPiTrjV2KRkEjH78ZMcUQXg0L3BYHJ3lc69Vs5Ddf9uUGGMYldX3WfMBEmh/9iFBDAaTCK 30 | 31 | 32 | -------------------------------------------------------------------------------- /app/src/main/res/values/ic_launcher_background.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | #99D6FF 4 | -------------------------------------------------------------------------------- /app/src/main/res/values/strings.xml: -------------------------------------------------------------------------------- 1 | 2 | Text Tools 3 | Collection of useful text related functions which can be accessed within many apps inside the Android System by selecting a text within it. The supported options appear in the context menu that appears after selecting a text. You may have to reveal them by pressing the 3 dots icon in the context menu. The list of supported functions will keep on growing. 4 | Collection of useful text related functions which can be accessed within many apps inside the Android System by selecting a text within it 5 | Select the function that you would like to perform. Selected text - %1$s 6 | 7 | Features 8 | Context menu option: 9 | 10 | [T\u00B2] WhatsApp 11 | WhatsApp 12 | Text unsaved numbers in WhatsApp 13 | Easily text any unsaved number in WhatsApp. Useful if you want to text people for temporary purposes. 14 | 15 | [T\u00B2] Evaluate 16 | Evaluate mathematical expressions 17 | Evaluate 18 | Evaluate mathematical expressions inline and replace the expression with its result. Works only on text selected that can be edited. 19 | Invalid mathematical expression 20 | Try It Out 21 | 22 | - In the below text field, type any phone number.\n- Tap and hold on the phone number you just typed.\n- Tap on the [T\u00B2] WhatsApp option. If the option is not visible, tap on the 3 dots to reveal it.\n- If the phone number is available on WhatsApp (and if WhatsApp is installed) it should open the chat in WhatsApp. 23 | - In the below text field, type any mathematical expression (for example, 1 + 2 + 3).\n- Tap and hold on the expression you just typed.\n- Tap on the [T\u00B2] Evaluate option. If the option is not visible, tap on the 3 dots to reveal it.\n- If the mathematical expression typed was valid, you should see its result. 24 | Unexpected error occurred 25 | Great! You can do the same inside any app as long as you can select a text. 26 | If it did not work, you can raise an issue in the Github repo. Would you like to do that now? 27 | It worked 28 | It did not work 29 | 30 | App Info 31 | Version - %1$s (%2$d) 32 | 33 | Open Source 34 | This app is open source! Feel free to contribute, raise issue and check it out. 35 | Check it out 36 | Invalid phone number 37 | Settings 38 | Automatically add country code 39 | If the selected phone number does not have any country code, the preset country code will be automatically added if enabled. 40 | Messaging 41 | Preset country code 42 | Decimal points 43 | Number of digits shown after the decimal point as a result of an evaluate function. If the result is an integer (whole number with no decimal), the decimal point will not be shown. 44 | Releases page 45 | 46 | [T\u00B2] Transform 47 | Transform 48 | Select how you want to transform the text 49 | Transform text 50 | Provides various ways to transform the selected text like changing its case, wrapping it with other text and more. 51 | Wrap text 52 | Change case 53 | 54 | Select the character with which you would like to wrap the text 55 | \'\' (Single inverted commas) 56 | \"\" (Double inverted commas) 57 | () (First brackets) 58 | {} (Curly braces) 59 | [] (Square brackets) 60 | Custom wrapper text 61 | The custom wrapper text should contain even number of letters. The first half of the custom wrapper text will be added to beginning of the selected text and the remaining will be added to the end of the selected text. 62 | Custom wrapper text must contain even number of letters 63 | 64 | Select how you want to change the case 65 | UPPER CASE 66 | lower case 67 | Title case (first word only) 68 | Title Case (All Words) 69 | rAnDOm cASe 70 | Contributors 71 | 72 | Enable preview 73 | Result - %1$s 74 | 75 | This option works only on editable text input 76 | Previewed result is truncated. Transformation will be applied to the entire selection. 77 | Donate 78 | Do you like this app? If so, you can make a small donation. This helps to keep me motivated and work on this project to make it even better. 79 | [T\u00B2] Save 80 | Save text 81 | Save the selected text so that you can access it later 82 | Lets you save the selected text (to some file) so that you can access it later. 83 | As .txt file 84 | As a note 85 | No note taking apps found 86 | Note taking is not supported 87 | Successfully saved the selected text 88 | Failed to save text 89 | 90 | Find & Replace 91 | [T\u00B2] Find & Replace 92 | Replace 93 | Replace All 94 | Find Next (%1$d/%2$d) 95 | Replace 96 | Find 97 | Ignore case 98 | Undo 99 | Save 100 | Apply 101 | Reset 102 | Redo 103 | Quickly find certain text and replace them with text of your choice within the selected text. 104 | Select what you would like to do with the result 105 | Replace selected text with result 106 | Append result to the selected text 107 | Copy result to the clipboard 108 | Remember this choice 109 | Not sure, ask me next time 110 | Result handling 111 | Copied to clipboard 112 | Sort lines 113 | Count text 114 | Repeat text 115 | Remove text 116 | Add prefix/suffix 117 | Number lines 118 | Reverse text 119 | Preview 120 | Input text 121 | None 122 | Remove first 123 | Remove last 124 | Remove all 125 | Prefix 126 | Suffix 127 | Decorate text 128 | Bold (serif) 129 | Italic (serif) 130 | Bold/Italic (serif) 131 | Bold (sans) 132 | Italic (sans) 133 | Bold/Italic (sans) 134 | Strikethrough (short) 135 | Strikethrough (long) 136 | Reverse words 137 | Reverse lines 138 | Remove whitespaces 139 | Remove line breaks 140 | Remove empty lines 141 | Remove duplicate words 142 | Remove duplicate words (case sensitive) 143 | Remove duplicate lines 144 | Remove duplicate lines (case sensitive) 145 | Cursive 146 | Text Count 147 | Characters 148 | Letters 149 | Words 150 | Digits 151 | Spaces 152 | Other symbols 153 | Word frequency 154 | Statistics 155 | [T\u00B2] Text Count 156 | Get detailed analysis about character count, word count, letter count and more for the selected text. 157 | Onboarding 158 | Context menu options 159 | Choose how you would like the context menu options to appear when you select text. It can also be changed later in the settings. 160 | Single option 161 | Only one context menu option will appear which when clicked will reveal all the supported actions 162 | Multiple options 163 | All the actions supported by the app will be shown in the context menu 164 | It is also possible to choose which options should be available in the context menu. 165 | Edit 166 | Done 167 | App 168 | Invalid country code 169 | Line break 170 | Replace whitespace 171 | After certain characters 172 | After certain words 173 | Copy to clipboard 174 | SimulationActivity 175 | Simulate 176 | If the Text Tools options are not seen for the selected text in an other app, that text can be copied and pasted here for simulation of text tools functions. 177 | Enter or paste the text here and then click the button below to simulate various functions of Text Tools. Any text produced by a function of Text Tools (like Text Transform) will not be reflected here but rather be copied to the clipboard. 178 | Max characters per line 179 | Squeeze 180 | -------------------------------------------------------------------------------- /app/src/main/res/values/themes.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 13 | -------------------------------------------------------------------------------- /app/src/main/res/xml/backup_rules.xml: -------------------------------------------------------------------------------- 1 | 8 | 9 | 13 | -------------------------------------------------------------------------------- /app/src/main/res/xml/data_extraction_rules.xml: -------------------------------------------------------------------------------- 1 | 6 | 7 | 8 | 12 | 13 | 19 | -------------------------------------------------------------------------------- /assets/eval.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/corphish/TextTools/743de19a692652e3f6d050e8d978180be0e66fc6/assets/eval.gif -------------------------------------------------------------------------------- /assets/find_replace.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/corphish/TextTools/743de19a692652e3f6d050e8d978180be0e66fc6/assets/find_replace.gif -------------------------------------------------------------------------------- /assets/save.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/corphish/TextTools/743de19a692652e3f6d050e8d978180be0e66fc6/assets/save.gif -------------------------------------------------------------------------------- /assets/single_options.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/corphish/TextTools/743de19a692652e3f6d050e8d978180be0e66fc6/assets/single_options.png -------------------------------------------------------------------------------- /assets/text_count.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/corphish/TextTools/743de19a692652e3f6d050e8d978180be0e66fc6/assets/text_count.gif -------------------------------------------------------------------------------- /assets/text_transform.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/corphish/TextTools/743de19a692652e3f6d050e8d978180be0e66fc6/assets/text_transform.gif -------------------------------------------------------------------------------- /assets/wup_1.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/corphish/TextTools/743de19a692652e3f6d050e8d978180be0e66fc6/assets/wup_1.gif -------------------------------------------------------------------------------- /assets/wup_2.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/corphish/TextTools/743de19a692652e3f6d050e8d978180be0e66fc6/assets/wup_2.gif -------------------------------------------------------------------------------- /build.gradle.kts: -------------------------------------------------------------------------------- 1 | // Top-level build file where you can add configuration options common to all sub-projects/modules. 2 | plugins { 3 | id("com.android.application") version "8.8.2" apply false 4 | id("org.jetbrains.kotlin.android") version "1.9.24" apply false 5 | id("com.google.dagger.hilt.android") version "2.51.1" apply false 6 | id("org.jetbrains.kotlin.plugin.compose") version "2.0.0" apply false 7 | } -------------------------------------------------------------------------------- /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. More details, visit 12 | # http://www.gradle.org/docs/current/userguide/multi_project_builds.html#sec: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/wrapper/gradle-wrapper.jar: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/corphish/TextTools/743de19a692652e3f6d050e8d978180be0e66fc6/gradle/wrapper/gradle-wrapper.jar -------------------------------------------------------------------------------- /gradle/wrapper/gradle-wrapper.properties: -------------------------------------------------------------------------------- 1 | #Sat Aug 19 10:53:41 IST 2023 2 | distributionBase=GRADLE_USER_HOME 3 | distributionPath=wrapper/dists 4 | distributionUrl=https\://services.gradle.org/distributions/gradle-8.10.2-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 | -------------------------------------------------------------------------------- /metadata/en-US/full_description.txt: -------------------------------------------------------------------------------- 1 | Collection of useful text related tools that can be accessed from the context menu that appears on text selection. The feature list will keep on growing. Let me know if you have any ideas. 2 | 3 | Current Features 4 | 1. Text unsaved numbers in WhatsApp - Often we find ourselves needing to text/send file/share location to someone for temporary purposes (if you ask me, I face this a lot in situations where I go to a printing centre where they ask me to share the file that I want to print over WhatsApp, so I have to save their contact, refresh my WhatsApp contact list and then send them). Having a context menu option to directly open a WhatsApp chat from their phone number would save a lot of time and keep your phonebook clean, right? 5 | 2. Evaluate mathematical expressions inline - We may often find ourselves to mathematically calculate something while texting someone (probably while making plans). Having an option to perform math calculations in line without needing to open calculator could save time. 6 | 3. Transform text - Transform the selected text inline. Supported transformations include changing the case, wrapping it with text etc. 7 | 4. Text count - Get detailed analysis about character count, word count, letter count and more for the selected text. 8 | 5. Save text to a file - Lets you save the selected text into a file so that it can be accessed later. 9 | 6. Find & Replace - Quickly find certain text and replace them with text of your choice within the selected text. 10 | 11 | Where are screenshots? 12 | The app adds capabilities to text selection wherever supported in the Android System, there is nothing much to do inside the app, hence very less screenshots. 13 | 14 | Installation Warning 15 | While installing the app (you have to sideload), Google Play may block the installation saying it has never seen the app before. While it is true (from Google Play POV), kindly proceed with installing the app. If you have doubts about the app being malicious, you are free to verify the same from the source code (it would not be open-source if the app was malicious in the first place, right?). -------------------------------------------------------------------------------- /metadata/en-US/images/icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/corphish/TextTools/743de19a692652e3f6d050e8d978180be0e66fc6/metadata/en-US/images/icon.png -------------------------------------------------------------------------------- /metadata/en-US/images/phoneScreenshots/1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/corphish/TextTools/743de19a692652e3f6d050e8d978180be0e66fc6/metadata/en-US/images/phoneScreenshots/1.png -------------------------------------------------------------------------------- /metadata/en-US/images/phoneScreenshots/2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/corphish/TextTools/743de19a692652e3f6d050e8d978180be0e66fc6/metadata/en-US/images/phoneScreenshots/2.png -------------------------------------------------------------------------------- /metadata/en-US/images/phoneScreenshots/3.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/corphish/TextTools/743de19a692652e3f6d050e8d978180be0e66fc6/metadata/en-US/images/phoneScreenshots/3.png -------------------------------------------------------------------------------- /metadata/en-US/images/phoneScreenshots/4.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/corphish/TextTools/743de19a692652e3f6d050e8d978180be0e66fc6/metadata/en-US/images/phoneScreenshots/4.png -------------------------------------------------------------------------------- /metadata/en-US/images/phoneScreenshots/5.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/corphish/TextTools/743de19a692652e3f6d050e8d978180be0e66fc6/metadata/en-US/images/phoneScreenshots/5.png -------------------------------------------------------------------------------- /metadata/en-US/images/phoneScreenshots/6.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/corphish/TextTools/743de19a692652e3f6d050e8d978180be0e66fc6/metadata/en-US/images/phoneScreenshots/6.png -------------------------------------------------------------------------------- /metadata/en-US/images/phoneScreenshots/7.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/corphish/TextTools/743de19a692652e3f6d050e8d978180be0e66fc6/metadata/en-US/images/phoneScreenshots/7.png -------------------------------------------------------------------------------- /metadata/en-US/images/phoneScreenshots/8.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/corphish/TextTools/743de19a692652e3f6d050e8d978180be0e66fc6/metadata/en-US/images/phoneScreenshots/8.png -------------------------------------------------------------------------------- /metadata/en-US/short_description.txt: -------------------------------------------------------------------------------- 1 | Useful text related functions that can be done on the selected text -------------------------------------------------------------------------------- /metadata/en-US/title.txt: -------------------------------------------------------------------------------- 1 | Text Tools -------------------------------------------------------------------------------- /settings.gradle.kts: -------------------------------------------------------------------------------- 1 | @file:Suppress("UnstableApiUsage") 2 | 3 | pluginManagement { 4 | repositories { 5 | google() 6 | mavenCentral() 7 | gradlePluginPortal() 8 | } 9 | } 10 | dependencyResolutionManagement { 11 | repositoriesMode.set(RepositoriesMode.FAIL_ON_PROJECT_REPOS) 12 | repositories { 13 | google() 14 | mavenCentral() 15 | } 16 | } 17 | 18 | rootProject.name = "Quick Tools" 19 | include(":app") 20 | --------------------------------------------------------------------------------