├── .github
├── FUNDING.yml
├── ISSUE_TEMPLATE
│ ├── bug-report.md
│ └── feature-request.md
├── pull_request_template.md
└── workflows
│ └── android-build.yml
├── .gitignore
├── LICENSE
├── README.md
├── app
├── .gitignore
├── build.gradle.kts
├── proguard-rules.pro
└── src
│ ├── androidTest
│ └── java
│ │ └── com
│ │ └── vishnu
│ │ └── whatsappcleaner
│ │ └── ExampleInstrumentedTest.kt
│ ├── main
│ ├── AndroidManifest.xml
│ ├── java
│ │ └── com
│ │ │ └── vishnu
│ │ │ └── whatsappcleaner
│ │ │ ├── Constants.kt
│ │ │ ├── MainActivity.kt
│ │ │ ├── MainViewModel.kt
│ │ │ ├── data
│ │ │ ├── FileRepository.kt
│ │ │ └── StoreData.kt
│ │ │ ├── model
│ │ │ ├── ListDirectory.kt
│ │ │ └── ListFile.kt
│ │ │ └── ui
│ │ │ ├── Components.kt
│ │ │ ├── CustomTabLayout.kt
│ │ │ ├── DetailsScreen.kt
│ │ │ ├── HomeScreen.kt
│ │ │ ├── PermissionScreen.kt
│ │ │ └── theme
│ │ │ ├── Color.kt
│ │ │ ├── Theme.kt
│ │ │ └── Type.kt
│ └── res
│ │ ├── drawable
│ │ ├── audio.xml
│ │ ├── check_circle.xml
│ │ ├── check_circle_filled.xml
│ │ ├── clean.xml
│ │ ├── document.xml
│ │ ├── empty.xml
│ │ ├── error.xml
│ │ ├── gif.xml
│ │ ├── ic_grid_view.xml
│ │ ├── ic_select_all.xml
│ │ ├── ic_sort.xml
│ │ ├── ic_view_list.xml
│ │ ├── image.xml
│ │ ├── open_in.xml
│ │ ├── permission_hint.webp
│ │ ├── profile.xml
│ │ ├── recycling.xml
│ │ ├── recycling_round.xml
│ │ ├── status.xml
│ │ ├── sticker.xml
│ │ ├── unknown.xml
│ │ ├── video.xml
│ │ ├── video_notes.xml
│ │ ├── voice.xml
│ │ └── wallpaper.xml
│ │ ├── font
│ │ └── geist.ttf
│ │ ├── values
│ │ ├── colors.xml
│ │ ├── strings.xml
│ │ └── themes.xml
│ │ └── xml
│ │ ├── backup_rules.xml
│ │ ├── data_extraction_rules.xml
│ │ └── provider_paths.xml
│ └── test
│ └── java
│ └── com
│ └── vishnu
│ └── whatsappcleaner
│ └── ExampleUnitTest.kt
├── build.gradle.kts
├── fastlane
└── metadata
│ └── android
│ └── en-US
│ ├── changelogs
│ └── 8.txt
│ ├── full_description.txt
│ ├── images
│ ├── featureGraphic.png
│ ├── icon.png
│ └── phoneScreenshots
│ │ ├── 1.png
│ │ ├── 2.png
│ │ └── 3.png
│ ├── short_description.txt
│ └── title.txt
├── gradle.properties
├── gradle
├── libs.versions.toml
└── wrapper
│ ├── gradle-wrapper.jar
│ └── gradle-wrapper.properties
├── gradlew
├── gradlew.bat
├── settings.gradle.kts
└── spotless-header
/.github/FUNDING.yml:
--------------------------------------------------------------------------------
1 | buy_me_a_coffee: "VishnuSanal"
2 |
--------------------------------------------------------------------------------
/.github/ISSUE_TEMPLATE/bug-report.md:
--------------------------------------------------------------------------------
1 | ---
2 | name: Bug Report
3 | about: Report bugs to help us improve
4 | title: ''
5 | labels: ''
6 | assignees: ''
7 |
8 | ---
9 |
10 | **Describe the bug**
11 | A clear and concise description of what the bug is.
12 |
13 | **To Reproduce**
14 | Steps to reproduce the behavior:
15 | 1. Go to '...'
16 | 2. Click on '....'
17 | 3. Scroll down to '....'
18 | 4. See error
19 |
20 | **Expected behavior**
21 | A clear and concise description of what you expected to happen.
22 |
23 | **Screenshots**
24 | If applicable, add screenshots to help explain your problem.
25 |
26 | **Crash logs**
27 | If possible, add crash logs to help us figure out the problem.
28 |
29 | **Smartphone (please complete the following information):**
30 | - Device: [e.g. OnePlus 8 Pro]
31 | - Andorid Version: [e.g. Android 10]
32 | - App Version: [e.g. v2.6.1]
33 |
34 | **Additional context**
35 | Add any other context about the problem here.
36 |
--------------------------------------------------------------------------------
/.github/ISSUE_TEMPLATE/feature-request.md:
--------------------------------------------------------------------------------
1 | ---
2 | name: Feature Request
3 | about: 'Suggest a Feature '
4 | title: ''
5 | labels: ''
6 | assignees: ''
7 |
8 | ---
9 |
10 | **Is your feature request related to a problem? Please describe.**
11 | A clear and concise description of what the problem is. Ex. I'm always frustrated when [...]
12 |
13 | **Describe the solution you'd like**
14 | A clear and concise description of what you want to happen.
15 |
16 | **Describe alternatives you've considered**
17 | A clear and concise description of any alternative solutions or features you've considered.
18 |
19 | **Additional context**
20 | Add any other context or screenshots about the feature request here.
21 |
--------------------------------------------------------------------------------
/.github/pull_request_template.md:
--------------------------------------------------------------------------------
1 | # PR Info
2 |
3 | ## Issue Details
4 |
5 |
6 |
7 | - Fixes #----
8 | - **Addresses** #----
9 |
10 | ## Tests
11 |
12 | - [ ] `./gradlew spotlessCheck`
13 | - [ ] `./gradlew testDebug`
14 |
15 | ## Type of change
16 |
17 |
18 |
19 | - **Bug Fix**
20 | - **New Feature**
21 | - **Breaking Change**
22 | - **Other**
23 |
24 | ## Additional Info
25 |
26 |
27 |
--------------------------------------------------------------------------------
/.github/workflows/android-build.yml:
--------------------------------------------------------------------------------
1 | name: Android Build
2 |
3 | on: [push, pull_request]
4 |
5 | jobs:
6 | build:
7 | runs-on: ubuntu-latest
8 | steps:
9 | - name: Checkout
10 | uses: actions/checkout@v4
11 |
12 | - name: Set Up JDK Environment
13 | uses: actions/setup-java@v4
14 | with:
15 | distribution: "adopt"
16 | java-version: 21
17 |
18 | - name: Spotless Test
19 | run: ./gradlew spotlessCheck --stacktrace
20 |
21 | - name: Gradle Build
22 | run: ./gradlew testDebug --stacktrace
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 |
2 | # Created by https://www.gitignore.io/api/android,androidstudio
3 | # Edit at https://www.gitignore.io/?templates=android,androidstudio
4 |
5 | ### Android ###
6 | # Built application files
7 | *.apk
8 | *.ap_
9 | *.aab
10 |
11 | # Files for the ART/Dalvik VM
12 | *.dex
13 |
14 | # Java class files
15 | *.class
16 |
17 | # Generated files
18 | bin/
19 | gen/
20 | out/
21 |
22 | # Gradle files
23 | .gradle/
24 | build/
25 |
26 | # Local configuration file (sdk path, etc)
27 | local.properties
28 |
29 | # Proguard folder generated by Eclipse
30 | proguard/
31 |
32 | # Log Files
33 | *.log
34 |
35 | # Android Studio Navigation editor temp files
36 | .navigation/
37 |
38 | # Android Studio captures folder
39 | captures/
40 |
41 | # IntelliJ
42 | *.iml
43 | .idea/
44 | .idea/workspace.xml
45 | .idea/tasks.xml
46 | .idea/gradle.xml
47 | .idea/assetWizardSettings.xml
48 | .idea/dictionaries
49 | .idea/libraries
50 | .idea/caches
51 | # Android Studio 3 in .gitignore file.
52 | .idea/caches/build_file_checksums.ser
53 | .idea/modules.xml
54 |
55 | # Keystore files
56 | # Uncomment the following lines if you do not want to check your keystore files in.
57 | #*.jks
58 | #*.keystore
59 |
60 | # External native build folder generated in Android Studio 2.2 and later
61 | .externalNativeBuild
62 |
63 | # Google Services (e.g. APIs or Firebase)
64 | google-services.json
65 |
66 | # Freeline
67 | freeline.py
68 | freeline/
69 | freeline_project_description.json
70 |
71 | # fastlane
72 | fastlane/report.xml
73 | fastlane/Preview.html
74 | fastlane/screenshots
75 | fastlane/test_output
76 | fastlane/readme.md
77 |
78 | # Version control
79 | vcs.xml
80 |
81 | # lint
82 | lint/intermediates/
83 | lint/generated/
84 | lint/outputs/
85 | lint/tmp/
86 | # lint/reports/
87 |
88 | ### Android Patch ###
89 | gen-external-apklibs
90 | output.json
91 |
92 | ### AndroidStudio ###
93 | # Covers files to be ignored for android development using Android Studio.
94 |
95 | # Built application files
96 |
97 | # Files for the ART/Dalvik VM
98 |
99 | # Java class files
100 |
101 | # Generated files
102 |
103 | # Gradle files
104 | .gradle
105 |
106 | # Signing files
107 | .signing/
108 |
109 | # Local configuration file (sdk path, etc)
110 |
111 | # Proguard folder generated by Eclipse
112 |
113 | # Log Files
114 |
115 | # Android Studio
116 | /*/build/
117 | /*/local.properties
118 | /*/out
119 | /*/*/build
120 | /*/*/production
121 | *.ipr
122 | *~
123 | *.swp
124 |
125 | # Android Patch
126 |
127 | # External native build folder generated in Android Studio 2.2 and later
128 |
129 | # NDK
130 | obj/
131 |
132 | # IntelliJ IDEA
133 | *.iws
134 | /out/
135 |
136 | # User-specific configurations
137 | .idea/caches/
138 | .idea/libraries/
139 | .idea/shelf/
140 | .idea/.name
141 | .idea/compiler.xml
142 | .idea/copyright/profiles_settings.xml
143 | .idea/encodings.xml
144 | .idea/misc.xml
145 | .idea/scopes/scope_settings.xml
146 | .idea/vcs.xml
147 | .idea/jsLibraryMappings.xml
148 | .idea/datasources.xml
149 | .idea/dataSources.ids
150 | .idea/sqlDataSources.xml
151 | .idea/dynamic.xml
152 | .idea/uiDesigner.xml
153 |
154 | # OS-specific files
155 | .DS_Store
156 | .DS_Store?
157 | ._*
158 | .Spotlight-V100
159 | .Trashes
160 | ehthumbs.db
161 | Thumbs.db
162 |
163 | # Legacy Eclipse project files
164 | .classpath
165 | .project
166 | .cproject
167 | .settings/
168 |
169 | # Mobile Tools for Java (J2ME)
170 | .mtj.tmp/
171 |
172 | # Package Files #
173 | *.war
174 | *.ear
175 |
176 | # virtual machine crash logs (Reference: http://www.java.com/en/download/help/error_hotspot.xml)
177 | hs_err_pid*
178 |
179 | ## Plugin-specific files:
180 |
181 | # mpeltonen/sbt-idea plugin
182 | .idea_modules/
183 |
184 | # JIRA plugin
185 | atlassian-ide-plugin.xml
186 |
187 | # Mongo Explorer plugin
188 | .idea/mongoSettings.xml
189 |
190 | # Crashlytics plugin (for Android Studio and IntelliJ)
191 | com_crashlytics_export_strings.xml
192 | crashlytics.properties
193 | crashlytics-build.properties
194 | fabric.properties
195 |
196 | ### AndroidStudio Patch ###
197 |
198 | !/gradle/wrapper/gradle-wrapper.jar
199 |
200 | # End of https://www.gitignore.io/api/android,androidstudio
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 | WhatsAppCleaner
7 |
8 |
9 |
10 | Cleaner for WhatsApp - Clean Redundant Media and Files from Storage
11 |
12 |
13 |
14 |
15 |
16 |
17 |
18 |
19 |
20 |
21 |
22 |
23 |
24 |
25 |
26 |
27 |
28 |
29 |
30 |
31 |
32 |
33 |
38 |
39 |
40 |
41 | 
42 |
43 | ## Enjoying WhatsAppCleaner? Consider Supporting!
44 | Consider donating to support the development! It requires a lot of time and effort to develop the
45 | copylefted libre software app, gratis and ad-free. :D
46 |
47 |
48 |
49 |
50 |
51 |
52 |
53 | ## Join Telegram Group
54 | If you enjoy the app, wish to contribute or simply wish to be with like-minded people, come join us. It is fun :)
55 |
56 |
57 |
58 |
59 |
60 |
61 |
62 | ## Why?
63 |
64 | Hi, I have been developing open-source native Android products for a while now. I have seen many Android devices with storage running out; most of the time all they needed were to clean-up the storage consumed by WhatsApp. I had searched for an application for the same, but I couldn't find any actually working ones on Google Play nor FDroid, but none seemed to work fine, mostly limited due to Android's varying permission requirements for different versions. And I decided to build one! :)
65 |
66 |
70 |
71 |
72 |
73 |
87 |
88 | ## Screenshots
89 |
90 |
91 |
92 |
93 |
94 |
95 |
96 |
97 |
98 | ## Participate in the community
99 |
100 | **Communities keep FOSS projects alive.** Without them, projects usually fade into obscurity when the primary developer loses interest or becomes busy in other parts of their life. That's where you come in! You can:
101 | - Read through the [issues](https://github.com/VishnuSanal/WhatsAppCleaner/issues) and give a star to the ones you care about most.
102 | - Open [new issues](https://github.com/VishnuSanal/WhatsAppCleaner/issues/new/choose) with feedback, feature requests, or bug reports.
103 | - Come chat at [Telegram](https://t.me/QuotesStatusCreator).
104 |
105 |
106 | ## License
107 | [](https://www.gnu.org/licenses/gpl-3.0.en.html)
108 |
109 | Note: WhatsAppCleaner is a third-party application that helps in cleaning files generated by WhatsApp, and is not affiliated to or endorsed by WhatsApp itself.
110 |
--------------------------------------------------------------------------------
/app/.gitignore:
--------------------------------------------------------------------------------
1 | /build
--------------------------------------------------------------------------------
/app/build.gradle.kts:
--------------------------------------------------------------------------------
1 | plugins {
2 | alias(libs.plugins.android.application)
3 | alias(libs.plugins.jetbrains.kotlin.android)
4 | alias(libs.plugins.compose.compiler)
5 | }
6 |
7 | android {
8 | namespace = "com.vishnu.whatsappcleaner"
9 | compileSdk = 35
10 |
11 | defaultConfig {
12 | applicationId = "com.vishnu.whatsappcleaner"
13 | minSdk = 24
14 | targetSdk = 35
15 | versionCode = 8
16 | versionName = "v1.2.0"
17 |
18 | testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner"
19 | vectorDrawables {
20 | useSupportLibrary = true
21 | }
22 | }
23 |
24 | // doesn't make much of a difference, can be enabled when it does
25 | // splits {
26 | // abi {
27 | // isEnable = true
28 | // reset()
29 | // include("x86", "x86_64", "armeabi-v7a", "arm64-v8a")
30 | // isUniversalApk = true
31 | // }
32 | // }
33 |
34 | buildTypes {
35 | release {
36 | isMinifyEnabled = true
37 | isShrinkResources = true
38 | proguardFiles(
39 | getDefaultProguardFile("proguard-android-optimize.txt"),
40 | "proguard-rules.pro"
41 | )
42 | signingConfig = signingConfigs.getByName("debug")
43 | }
44 | debug {
45 | applicationIdSuffix = ".debug"
46 | }
47 | }
48 | compileOptions {
49 | sourceCompatibility = JavaVersion.VERSION_1_8
50 | targetCompatibility = JavaVersion.VERSION_1_8
51 | }
52 | kotlinOptions {
53 | jvmTarget = "1.8"
54 | }
55 | buildFeatures {
56 | compose = true
57 | }
58 | composeOptions {
59 | kotlinCompilerExtensionVersion = "1.5.1"
60 | }
61 | packaging {
62 | resources {
63 | excludes += "/META-INF/{AL2.0,LGPL2.1}"
64 | }
65 | }
66 | // https://gitlab.com/fdroid/fdroidserver/-/issues/1056
67 | dependenciesInfo {
68 | // Disables dependency metadata when building APKs.
69 | includeInApk = false
70 | // Disables dependency metadata when building Android App Bundles.
71 | includeInBundle = false
72 | }
73 | }
74 |
75 | dependencies {
76 |
77 | implementation(libs.androidx.core.ktx)
78 | implementation(libs.androidx.lifecycle.runtime.ktx)
79 | implementation(libs.androidx.activity.compose)
80 | implementation(platform(libs.androidx.compose.bom))
81 | implementation(libs.androidx.ui)
82 | implementation(libs.androidx.ui.graphics)
83 | implementation(libs.androidx.ui.tooling.preview)
84 | implementation(libs.androidx.material3)
85 |
86 | implementation(libs.androidx.navigation.compose)
87 |
88 | implementation(libs.androidx.datastore.core)
89 | implementation(libs.androidx.datastore.preferences)
90 |
91 | implementation(libs.compose.shimmer)
92 | implementation(libs.glide.compose)
93 |
94 | implementation(libs.androidx.runtime)
95 | implementation(libs.androidx.runtime.livedata)
96 |
97 | testImplementation(libs.junit)
98 |
99 | androidTestImplementation(libs.androidx.junit)
100 | androidTestImplementation(libs.androidx.espresso.core)
101 | androidTestImplementation(platform(libs.androidx.compose.bom))
102 | androidTestImplementation(libs.androidx.ui.test.junit4)
103 |
104 | debugImplementation(libs.androidx.ui.tooling)
105 | debugImplementation(libs.androidx.ui.test.manifest)
106 | }
--------------------------------------------------------------------------------
/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/androidTest/java/com/vishnu/whatsappcleaner/ExampleInstrumentedTest.kt:
--------------------------------------------------------------------------------
1 | /*
2 | * Copyright (C) 2025 Vishnu Sanal T
3 | *
4 | * This file is part of WhatsAppCleaner.
5 | *
6 | * Quotes Status Creator is free software: you can redistribute it and/or modify
7 | * it under the terms of the GNU General Public License as published by
8 | * the Free Software Foundation, either version 3 of the License, or
9 | * (at your option) any later version.
10 | *
11 | * This program is distributed in the hope that it will be useful,
12 | * but WITHOUT ANY WARRANTY; without even the implied warranty of
13 | * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
14 | * GNU General Public License for more details.
15 | *
16 | * You should have received a copy of the GNU General Public License
17 | * along with this program. If not, see .
18 | */
19 |
20 | package com.vishnu.whatsappcleaner
21 |
22 | import androidx.test.ext.junit.runners.AndroidJUnit4
23 | import androidx.test.platform.app.InstrumentationRegistry
24 | import org.junit.Assert.assertEquals
25 | import org.junit.Test
26 | import org.junit.runner.RunWith
27 |
28 | /**
29 | * Instrumented test, which will execute on an Android device.
30 | *
31 | * See [testing documentation](http://d.android.com/tools/testing).
32 | */
33 | @RunWith(AndroidJUnit4::class)
34 | class ExampleInstrumentedTest {
35 | @Test
36 | fun useAppContext() {
37 | // Context of the app under test.
38 | val appContext = InstrumentationRegistry.getInstrumentation().targetContext
39 | assertEquals("com.vishnu.whatsappcleaner", appContext.packageName)
40 | }
41 | }
42 |
--------------------------------------------------------------------------------
/app/src/main/AndroidManifest.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
22 |
23 |
25 |
26 |
29 |
30 |
33 |
34 |
37 |
38 |
49 |
55 |
56 |
57 |
58 |
59 |
60 |
61 |
62 |
67 |
70 |
71 |
72 |
73 |
--------------------------------------------------------------------------------
/app/src/main/java/com/vishnu/whatsappcleaner/Constants.kt:
--------------------------------------------------------------------------------
1 | /*
2 | * Copyright (C) 2025 Vishnu Sanal T
3 | *
4 | * This file is part of WhatsAppCleaner.
5 | *
6 | * Quotes Status Creator is free software: you can redistribute it and/or modify
7 | * it under the terms of the GNU General Public License as published by
8 | * the Free Software Foundation, either version 3 of the License, or
9 | * (at your option) any later version.
10 | *
11 | * This program is distributed in the hope that it will be useful,
12 | * but WITHOUT ANY WARRANTY; without even the implied warranty of
13 | * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
14 | * GNU General Public License for more details.
15 | *
16 | * You should have received a copy of the GNU General Public License
17 | * along with this program. If not, see .
18 | */
19 |
20 | package com.vishnu.whatsappcleaner
21 |
22 | object Constants {
23 | const val WHATSAPP_HOME_URI = "whatsapp_home_uri"
24 |
25 | const val SCREEN_PERMISSION = "permission"
26 | const val SCREEN_HOME = "home"
27 | const val SCREEN_DETAILS = "details"
28 |
29 | const val DETAILS_LIST_ITEM = "details_list_item"
30 | const val FORCE_RELOAD_FILE_LIST = "force_reload_file_list"
31 |
32 | const val REQUEST_PERMISSIONS_CODE_WRITE_STORAGE = 2
33 |
34 | const val LIST_LOADING_INDICATION: String = "com.vishnu.whatsappcleaner.loading"
35 |
36 | final val EXTENSIONS_IMAGE = listOf(
37 | "jpg",
38 | "jpeg",
39 | "bmp",
40 | "raw",
41 | "png_a",
42 | "png",
43 | "webp_a",
44 | "webp",
45 | "animated_webp",
46 | "avif",
47 | "animated_avif",
48 | )
49 |
50 | final val EXTENSIONS_VIDEO = listOf(
51 | "mp4",
52 | "mpeg4",
53 | "webm",
54 | "avi",
55 | "gif",
56 | "3gp",
57 | "avi",
58 | )
59 |
60 | final val EXTENSIONS_DOCS = listOf(
61 | "txt",
62 | "pdf",
63 | "doc",
64 | "odt",
65 | "ppt",
66 | "pptx",
67 | "odp",
68 | "xls",
69 | "xlsx",
70 | "ods",
71 | )
72 |
73 | final val EXTENSIONS_AUDIO = listOf(
74 | "aac",
75 | "mp3",
76 | "flac",
77 | "opus",
78 | "midi",
79 | "wav",
80 | "ogg",
81 | )
82 | }
83 |
--------------------------------------------------------------------------------
/app/src/main/java/com/vishnu/whatsappcleaner/MainActivity.kt:
--------------------------------------------------------------------------------
1 | /*
2 | * Copyright (C) 2025 Vishnu Sanal T
3 | *
4 | * This file is part of WhatsAppCleaner.
5 | *
6 | * Quotes Status Creator is free software: you can redistribute it and/or modify
7 | * it under the terms of the GNU General Public License as published by
8 | * the Free Software Foundation, either version 3 of the License, or
9 | * (at your option) any later version.
10 | *
11 | * This program is distributed in the hope that it will be useful,
12 | * but WITHOUT ANY WARRANTY; without even the implied warranty of
13 | * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
14 | * GNU General Public License for more details.
15 | *
16 | * You should have received a copy of the GNU General Public License
17 | * along with this program. If not, see .
18 | */
19 |
20 | package com.vishnu.whatsappcleaner
21 |
22 | import android.Manifest
23 | import android.content.Intent
24 | import android.content.pm.PackageManager
25 | import android.net.Uri
26 | import android.os.Build
27 | import android.os.Build.VERSION_CODES
28 | import android.os.Bundle
29 | import android.os.Environment
30 | import android.provider.DocumentsContract
31 | import android.provider.Settings
32 | import android.widget.Toast
33 | import androidx.activity.ComponentActivity
34 | import androidx.activity.compose.setContent
35 | import androidx.activity.enableEdgeToEdge
36 | import androidx.activity.result.contract.ActivityResultContracts
37 | import androidx.compose.runtime.MutableState
38 | import androidx.compose.runtime.mutableStateOf
39 | import androidx.compose.runtime.remember
40 | import androidx.compose.ui.Modifier
41 | import androidx.core.app.ActivityCompat
42 | import androidx.lifecycle.Lifecycle
43 | import androidx.lifecycle.ViewModelProvider
44 | import androidx.lifecycle.lifecycleScope
45 | import androidx.lifecycle.repeatOnLifecycle
46 | import androidx.navigation.compose.NavHost
47 | import androidx.navigation.compose.composable
48 | import androidx.navigation.compose.rememberNavController
49 | import com.vishnu.whatsappcleaner.ui.DetailsScreen
50 | import com.vishnu.whatsappcleaner.ui.HomeScreen
51 | import com.vishnu.whatsappcleaner.ui.PermissionScreen
52 | import com.vishnu.whatsappcleaner.ui.theme.WhatsAppCleanerTheme
53 | import kotlinx.coroutines.launch
54 | import java.io.File
55 |
56 | class MainActivity : ComponentActivity() {
57 |
58 | private lateinit var viewModel: MainViewModel
59 |
60 | private lateinit var storagePermissionGranted: MutableState
61 |
62 | override fun onCreate(savedInstanceState: Bundle?) {
63 | super.onCreate(savedInstanceState)
64 | enableEdgeToEdge()
65 |
66 | val resultLauncher =
67 | registerForActivityResult(ActivityResultContracts.StartActivityForResult()) { result ->
68 | if (result.resultCode == RESULT_OK && result.data?.data?.path != null) {
69 | val relativePath = result.data!!.data!!.path!!.split(":")[1]
70 | val absolutePath =
71 | Environment.getExternalStorageDirectory().absolutePath + File.separator + relativePath
72 |
73 | viewModel.listDirectories(absolutePath)
74 |
75 | lifecycleScope.launch {
76 | repeatOnLifecycle(Lifecycle.State.STARTED) {
77 | viewModel.directories.collect { dirList ->
78 | if (dirList.toString().contains("/Media")) {
79 | contentResolver.takePersistableUriPermission(
80 | result.data!!.data!!,
81 | Intent.FLAG_GRANT_READ_URI_PERMISSION or Intent.FLAG_GRANT_WRITE_URI_PERMISSION
82 | )
83 |
84 | viewModel.saveHomeUri(absolutePath)
85 | restartActivity()
86 | } else {
87 | Toast.makeText(
88 | this@MainActivity,
89 | "Wrong directory selected, please select the right directory...",
90 | Toast.LENGTH_SHORT
91 | ).show()
92 | }
93 | }
94 | }
95 | }
96 | } else {
97 | Toast.makeText(this, "Please grant permissions...", Toast.LENGTH_SHORT).show()
98 | }
99 | }
100 |
101 | val storagePermissionResultLauncher =
102 | registerForActivityResult(ActivityResultContracts.StartActivityForResult()) {
103 | if (Build.VERSION.SDK_INT >= VERSION_CODES.R && !Environment.isExternalStorageManager()) {
104 | Toast.makeText(
105 | this,
106 | "Please grant permissions...",
107 | Toast.LENGTH_SHORT
108 | ).show()
109 | }
110 | }
111 |
112 | viewModel = ViewModelProvider(
113 | this,
114 | MainViewModelFactory(application)
115 | ).get(MainViewModel::class.java)
116 |
117 | setContent {
118 | WhatsAppCleanerTheme {
119 | storagePermissionGranted =
120 | remember {
121 | mutableStateOf(
122 | (Build.VERSION.SDK_INT >= VERSION_CODES.R && Environment.isExternalStorageManager()) ||
123 | ActivityCompat.checkSelfPermission(
124 | this@MainActivity,
125 | Manifest.permission.WRITE_EXTERNAL_STORAGE
126 | ) == PackageManager.PERMISSION_GRANTED
127 | )
128 | }
129 |
130 | var startDestination =
131 | if (Build.VERSION.SDK_INT >= VERSION_CODES.R &&
132 | Environment.isExternalStorageManager() &&
133 | contentResolver.persistedUriPermissions.isNotEmpty()
134 | ) Constants.SCREEN_HOME
135 | else if (ActivityCompat.checkSelfPermission(
136 | this@MainActivity,
137 | Manifest.permission.WRITE_EXTERNAL_STORAGE
138 | ) == PackageManager.PERMISSION_GRANTED &&
139 | contentResolver.persistedUriPermissions.isNotEmpty()
140 | ) Constants.SCREEN_HOME
141 | else {
142 | Toast.makeText(
143 | this,
144 | "Please grant all permissions...",
145 | Toast.LENGTH_SHORT
146 | ).show()
147 | Constants.SCREEN_PERMISSION
148 | }
149 |
150 | val navController = rememberNavController()
151 |
152 | NavHost(
153 | modifier = Modifier,
154 | navController = navController,
155 | startDestination = startDestination
156 | ) {
157 | composable(route = Constants.SCREEN_PERMISSION) {
158 | PermissionScreen(
159 | navController = navController,
160 | permissionsGranted = Pair(
161 | storagePermissionGranted.value,
162 | contentResolver.persistedUriPermissions.isNotEmpty()
163 | ),
164 | requestPermission = {
165 | if (Build.VERSION.SDK_INT >= VERSION_CODES.R) {
166 | storagePermissionResultLauncher.launch(
167 | Intent(
168 | Settings.ACTION_MANAGE_APP_ALL_FILES_ACCESS_PERMISSION,
169 | Uri.parse("package:" + packageName)
170 | )
171 | )
172 | } else {
173 | if (ActivityCompat.shouldShowRequestPermissionRationale(
174 | this@MainActivity,
175 | Manifest.permission.WRITE_EXTERNAL_STORAGE
176 | )
177 | ) {
178 | Toast.makeText(
179 | this@MainActivity,
180 | "Storage permission required for the app to work",
181 | Toast.LENGTH_SHORT
182 | ).show()
183 | }
184 |
185 | requestPermissions(
186 | arrayOf(
187 | Manifest.permission.WRITE_EXTERNAL_STORAGE,
188 | Manifest.permission.READ_EXTERNAL_STORAGE
189 | ),
190 | Constants.REQUEST_PERMISSIONS_CODE_WRITE_STORAGE
191 | )
192 | }
193 | },
194 | chooseDirectory = {
195 | resultLauncher.launch(
196 | Intent(Intent.ACTION_OPEN_DOCUMENT_TREE).apply {
197 | if (Build.VERSION.SDK_INT >= VERSION_CODES.O) putExtra(
198 | DocumentsContract.EXTRA_INITIAL_URI,
199 | Uri.parse(Constants.WHATSAPP_HOME_URI)
200 | )
201 | }
202 | )
203 | },
204 | )
205 | }
206 |
207 | composable(route = Constants.SCREEN_HOME) {
208 | HomeScreen(navController, viewModel)
209 | }
210 |
211 | composable(route = Constants.SCREEN_DETAILS) {
212 | DetailsScreen(navController, viewModel)
213 | }
214 | }
215 | }
216 | }
217 | }
218 |
219 | @Deprecated("Deprecated callback")
220 | public override fun onRequestPermissionsResult(
221 | requestCode: Int,
222 | permissions: Array,
223 | grantResults: IntArray
224 | ) {
225 | super.onRequestPermissionsResult(requestCode, permissions, grantResults)
226 |
227 | if (requestCode == Constants.REQUEST_PERMISSIONS_CODE_WRITE_STORAGE) {
228 | for (i in permissions.indices) {
229 | val permission = permissions[i]
230 | val grantResult = grantResults[i]
231 |
232 | if (permission == Manifest.permission.WRITE_EXTERNAL_STORAGE) {
233 | if (grantResult == PackageManager.PERMISSION_GRANTED) {
234 | storagePermissionGranted.value = true
235 | } else {
236 | requestPermissions(
237 | arrayOf(
238 | Manifest.permission.WRITE_EXTERNAL_STORAGE,
239 | Manifest.permission.READ_EXTERNAL_STORAGE
240 | ),
241 | Constants.REQUEST_PERMISSIONS_CODE_WRITE_STORAGE
242 | )
243 | }
244 | }
245 | }
246 | }
247 | }
248 |
249 | override fun onResume() {
250 | super.onResume()
251 | if (Build.VERSION.SDK_INT >= VERSION_CODES.R && ::storagePermissionGranted.isInitialized)
252 | storagePermissionGranted.value = Environment.isExternalStorageManager()
253 | }
254 |
255 | private fun restartActivity() {
256 | // terrible hack!
257 | val intent = intent
258 | finish()
259 | startActivity(intent)
260 | }
261 | }
262 |
--------------------------------------------------------------------------------
/app/src/main/java/com/vishnu/whatsappcleaner/MainViewModel.kt:
--------------------------------------------------------------------------------
1 | /*
2 | * Copyright (C) 2025 Vishnu Sanal T
3 | *
4 | * This file is part of WhatsAppCleaner.
5 | *
6 | * Quotes Status Creator is free software: you can redistribute it and/or modify
7 | * it under the terms of the GNU General Public License as published by
8 | * the Free Software Foundation, either version 3 of the License, or
9 | * (at your option) any later version.
10 | *
11 | * This program is distributed in the hope that it will be useful,
12 | * but WITHOUT ANY WARRANTY; without even the implied warranty of
13 | * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
14 | * GNU General Public License for more details.
15 | *
16 | * You should have received a copy of the GNU General Public License
17 | * along with this program. If not, see .
18 | */
19 |
20 | package com.vishnu.whatsappcleaner
21 |
22 | import android.app.Application
23 | import android.util.Log
24 | import androidx.lifecycle.AndroidViewModel
25 | import androidx.lifecycle.ViewModel
26 | import androidx.lifecycle.ViewModelProvider
27 | import androidx.lifecycle.viewModelScope
28 | import com.vishnu.whatsappcleaner.data.FileRepository
29 | import com.vishnu.whatsappcleaner.data.StoreData
30 | import com.vishnu.whatsappcleaner.model.ListDirectory
31 | import com.vishnu.whatsappcleaner.model.ListFile
32 | import kotlinx.coroutines.Dispatchers
33 | import kotlinx.coroutines.flow.MutableStateFlow
34 | import kotlinx.coroutines.flow.StateFlow
35 | import kotlinx.coroutines.flow.asStateFlow
36 | import kotlinx.coroutines.flow.first
37 | import kotlinx.coroutines.launch
38 | import java.util.Date
39 |
40 | class MainViewModel(private val application: Application) : AndroidViewModel(application) {
41 | private val _fileList = MutableStateFlow>(emptyList())
42 | val fileList: StateFlow> = _fileList.asStateFlow()
43 |
44 | private val _sentList = MutableStateFlow>(emptyList())
45 | val sentList: StateFlow> = _sentList.asStateFlow()
46 |
47 | private val _privateList = MutableStateFlow>(emptyList())
48 | val privateList: StateFlow> = _privateList.asStateFlow()
49 |
50 | private val _isInProgress = MutableStateFlow(false)
51 | val isInProgress: StateFlow = _isInProgress.asStateFlow()
52 |
53 | private val _directories = MutableStateFlow>(emptyList())
54 | val directories: StateFlow> = _directories
55 |
56 | private val _homeUri = MutableStateFlow("")
57 | val homeUri: StateFlow = _homeUri.asStateFlow()
58 |
59 | private val _fileReloadTrigger = MutableStateFlow(false)
60 | val fileReloadTrigger: StateFlow = _fileReloadTrigger.asStateFlow()
61 |
62 | private val storeData = StoreData(application.applicationContext)
63 |
64 | private val _isGridView = MutableStateFlow(false)
65 | val isGridView: StateFlow = _isGridView.asStateFlow()
66 |
67 | private val _directoryItem =
68 | MutableStateFlow>>>(ViewState.Loading)
69 | val directoryItem: StateFlow>>> =
70 | _directoryItem.asStateFlow()
71 |
72 | init {
73 | viewModelScope.launch(Dispatchers.Default) {
74 | storeData.isGridViewFlow.collect {
75 | _isGridView.value = it
76 | }
77 | getDirectoryList()
78 | }
79 | }
80 |
81 | fun toggleViewType() {
82 | viewModelScope.launch {
83 | val current = storeData.isGridViewFlow.first()
84 | val toggled = !current
85 | storeData.setGridViewPreference(toggled)
86 | }
87 | }
88 |
89 | fun saveHomeUri(homePath: String) {
90 | Log.i("vishnu", "saveHomeUri: $homePath")
91 | viewModelScope.launch(Dispatchers.Default) {
92 | storeData.set(
93 | Constants.WHATSAPP_HOME_URI,
94 | homePath
95 | )
96 | }
97 | }
98 |
99 | fun getHomeUri() {
100 | viewModelScope.launch(Dispatchers.Default) {
101 | _homeUri.value = storeData.get(Constants.WHATSAPP_HOME_URI)
102 | }
103 | }
104 |
105 | fun getDirectoryList() {
106 | Log.i("vishnu", "getDirectoryList() called")
107 |
108 | viewModelScope.launch(Dispatchers.Default) {
109 | storeData.get(Constants.WHATSAPP_HOME_URI)
110 | ?.let { homeUri ->
111 | val pair = FileRepository.getDirectoryList(
112 | application,
113 | homeUri
114 | )
115 | Log.e("vishnu", "getDirectoryList: $pair")
116 | _directoryItem.value = ViewState.Success(pair)
117 | }
118 | }
119 | }
120 |
121 | fun getFileList(
122 | target: Target,
123 | path: String,
124 | sortBy: String,
125 | isSortDescending: Boolean,
126 | filterStartDate: Long?,
127 | filterEndDate: Long?
128 | ) {
129 | Log.i("vishnu", "getFileList: $path")
130 |
131 | _isInProgress.value = true
132 | viewModelScope.launch(Dispatchers.Default) {
133 | val fileList = FileRepository.getFileList(application, path)
134 | _isInProgress.value = false
135 |
136 | fileList.sortWith(
137 | when {
138 | sortBy.contains("Name") -> compareBy { it.name }
139 | sortBy.contains("Size") -> compareBy { it.length() }
140 | else -> compareBy { it.lastModified() }
141 | }
142 | )
143 |
144 | if (
145 | sortBy.contains("Date") &&
146 | filterStartDate != null &&
147 | filterEndDate != null
148 | ) {
149 | val filteredList = fileList.filter {
150 | val lastModified = Date(it.lastModified())
151 | lastModified.after(Date(filterStartDate)) &&
152 | lastModified.before(
153 | Date(
154 | filterEndDate
155 | )
156 | )
157 | }
158 | fileList.clear()
159 | fileList.addAll(filteredList)
160 | }
161 |
162 | if (isSortDescending) fileList.reverse()
163 |
164 | when (target) {
165 | Target.Received -> _fileList.value = fileList
166 | Target.Sent -> _sentList.value = fileList
167 | Target.Private -> _privateList.value = fileList
168 | }
169 | }
170 | }
171 |
172 | fun listDirectories(path: String) {
173 | Log.i("vishnu", "listDirectories: $path")
174 |
175 | viewModelScope.launch(Dispatchers.Default) {
176 | val dirList = FileRepository.getDirectoryList(path)
177 | _directories.value = dirList
178 | }
179 | }
180 |
181 | fun delete(fileList: List) {
182 | Log.i("vishnu", "delete() called with: fileList = $fileList")
183 |
184 | _isInProgress.value = true
185 | viewModelScope.launch(Dispatchers.IO) {
186 | FileRepository.deleteFiles(fileList)
187 | _isInProgress.value = false
188 | _fileReloadTrigger.value = !_fileReloadTrigger.value
189 | }
190 | }
191 | }
192 |
193 | sealed class Target {
194 | data object Received : Target()
195 | data object Sent : Target()
196 | data object Private : Target()
197 | }
198 |
199 | sealed class ViewState {
200 | data object Loading : ViewState()
201 | data class Success(val data: T) : ViewState()
202 | data class Error(val message: String) : ViewState()
203 | }
204 |
205 | class MainViewModelFactory(private val application: Application) : ViewModelProvider.Factory {
206 | override fun create(modelClass: Class): T {
207 | if (modelClass.isAssignableFrom(MainViewModel::class.java)) {
208 | @Suppress("UNCHECKED_CAST")
209 | return MainViewModel(application) as T
210 | }
211 | throw IllegalArgumentException("Unknown ViewModel class")
212 | }
213 | }
214 |
--------------------------------------------------------------------------------
/app/src/main/java/com/vishnu/whatsappcleaner/data/FileRepository.kt:
--------------------------------------------------------------------------------
1 | /*
2 | * Copyright (C) 2025 Vishnu Sanal T
3 | *
4 | * This file is part of WhatsAppCleaner.
5 | *
6 | * Quotes Status Creator is free software: you can redistribute it and/or modify
7 | * it under the terms of the GNU General Public License as published by
8 | * the Free Software Foundation, either version 3 of the License, or
9 | * (at your option) any later version.
10 | *
11 | * This program is distributed in the hope that it will be useful,
12 | * but WITHOUT ANY WARRANTY; without even the implied warranty of
13 | * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
14 | * GNU General Public License for more details.
15 | *
16 | * You should have received a copy of the GNU General Public License
17 | * along with this program. If not, see .
18 | */
19 |
20 | package com.vishnu.whatsappcleaner.data
21 |
22 | import android.content.Context
23 | import android.text.format.Formatter.formatFileSize
24 | import android.util.Log
25 | import com.vishnu.whatsappcleaner.Constants
26 | import com.vishnu.whatsappcleaner.model.ListDirectory
27 | import com.vishnu.whatsappcleaner.model.ListFile
28 | import java.io.File
29 |
30 | class FileRepository {
31 | companion object {
32 |
33 | @JvmStatic
34 | public suspend fun getDirectoryList(
35 | context: Context,
36 | homePath: String
37 | ): Pair> {
38 | Log.i(
39 | "vishnu",
40 | "FileRepository#getDirectoryList: $homePath"
41 | )
42 |
43 | var totalSize = 0L
44 |
45 | val directoryList = ListDirectory.getDirectoryList(homePath)
46 |
47 | directoryList.forEach { directoryItem ->
48 |
49 | // since this contains expensive operations :)
50 | var size = File(directoryItem.path)
51 | .walkBottomUp()
52 | .filter { f -> f.isFile() }
53 | .map {
54 | if (it.name == ".nomedia")
55 | 0
56 | else
57 | it.length()
58 | }.sum()
59 |
60 | directoryItem.size = formatFileSize(context, size)
61 |
62 | totalSize += size
63 | }
64 |
65 | return Pair(
66 | formatFileSize(context, totalSize),
67 | directoryList
68 | )
69 | }
70 |
71 | @JvmStatic
72 | public suspend fun getFileList(context: Context, path: String): ArrayList {
73 | Log.i("vishnu", "FileRepository#getFileList: $path")
74 |
75 | val list = ArrayList()
76 |
77 | // flattening...
78 | if (path.contains("Media/WhatsApp Voice Notes") or path.contains("Media/WhatsApp Video Notes")) File(
79 | path
80 | ).walkTopDown().forEach { f ->
81 | if (!f.isDirectory && f.name != ".nomedia") list.add(
82 | ListFile(
83 | f.path,
84 | formatFileSize(context, getSize(f.path))
85 | )
86 | )
87 | }
88 | else File(path).listFiles { dir, name ->
89 |
90 | val f = File("$dir/$name")
91 |
92 | if (!f.isDirectory && f.name != ".nomedia") list.add(
93 | ListFile(
94 | f.path,
95 | formatFileSize(context, getSize(f.path))
96 | )
97 | )
98 |
99 | true
100 | }
101 |
102 | return list
103 | }
104 |
105 | @JvmStatic
106 | public suspend fun getDirectoryList(path: String): ArrayList {
107 | Log.i("vishnu", "FileRepository#getDirectoryList: $path")
108 |
109 | val list = ArrayList()
110 |
111 | File(path).listFiles { dir, name ->
112 |
113 | val f = File("$dir/$name")
114 |
115 | if (f.isDirectory) list.add(f.path)
116 |
117 | true
118 | }
119 |
120 | return list
121 | }
122 |
123 | @JvmStatic
124 | public fun getLoadingList(): ArrayList {
125 | val loadingList = ArrayList()
126 |
127 | for (i in 0 until 10) {
128 | loadingList.add(
129 | ListFile(
130 | Constants.LIST_LOADING_INDICATION,
131 | "0 B"
132 | )
133 | )
134 | }
135 |
136 | return loadingList
137 | }
138 |
139 | @JvmStatic
140 | public fun deleteFiles(fileList: List): Boolean {
141 | Log.i("vishnu", "FileRepository#deleteFiles: $fileList")
142 |
143 | fileList.forEach { file ->
144 | file.delete()
145 | }
146 |
147 | return false
148 | }
149 |
150 | private fun getSize(path: String): Long {
151 | // Log.i("vishnu", "getSize() called with: path = $path")
152 | return File(path).walkTopDown().map { it.length() }.sum()
153 | }
154 | }
155 | }
156 |
--------------------------------------------------------------------------------
/app/src/main/java/com/vishnu/whatsappcleaner/data/StoreData.kt:
--------------------------------------------------------------------------------
1 | /*
2 | * Copyright (C) 2025 Vishnu Sanal T
3 | *
4 | * This file is part of WhatsAppCleaner.
5 | *
6 | * Quotes Status Creator is free software: you can redistribute it and/or modify
7 | * it under the terms of the GNU General Public License as published by
8 | * the Free Software Foundation, either version 3 of the License, or
9 | * (at your option) any later version.
10 | *
11 | * This program is distributed in the hope that it will be useful,
12 | * but WITHOUT ANY WARRANTY; without even the implied warranty of
13 | * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
14 | * GNU General Public License for more details.
15 | *
16 | * You should have received a copy of the GNU General Public License
17 | * along with this program. If not, see .
18 | */
19 |
20 | package com.vishnu.whatsappcleaner.data
21 |
22 | import android.content.Context
23 | import androidx.datastore.preferences.core.booleanPreferencesKey
24 | import androidx.datastore.preferences.core.edit
25 | import androidx.datastore.preferences.core.stringPreferencesKey
26 | import androidx.datastore.preferences.preferencesDataStore
27 | import kotlinx.coroutines.flow.Flow
28 | import kotlinx.coroutines.flow.first
29 | import kotlinx.coroutines.flow.map
30 |
31 | class StoreData(val context: Context) {
32 |
33 | companion object {
34 | private val Context.dataStore by preferencesDataStore(name = "store_data")
35 | val IS_GRID_VIEW_KEY = booleanPreferencesKey("is_grid_view")
36 | }
37 |
38 | suspend fun set(key: String, value: String) {
39 | context.dataStore.edit { preferences ->
40 | preferences[stringPreferencesKey(key)] = value
41 | }
42 | }
43 |
44 | suspend fun get(key: String): String? = context.dataStore.data.first().get(
45 | stringPreferencesKey(key)
46 | )
47 |
48 | suspend fun setGridViewPreference(isGridView: Boolean) {
49 | context.dataStore.edit { preferences ->
50 | preferences[IS_GRID_VIEW_KEY] = isGridView
51 | }
52 | }
53 |
54 | val isGridViewFlow: Flow = context.dataStore.data
55 | .map { preferences ->
56 | preferences[IS_GRID_VIEW_KEY] ?: true
57 | }
58 | }
59 |
--------------------------------------------------------------------------------
/app/src/main/java/com/vishnu/whatsappcleaner/model/ListDirectory.kt:
--------------------------------------------------------------------------------
1 | /*
2 | * Copyright (C) 2025 Vishnu Sanal T
3 | *
4 | * This file is part of WhatsAppCleaner.
5 | *
6 | * Quotes Status Creator is free software: you can redistribute it and/or modify
7 | * it under the terms of the GNU General Public License as published by
8 | * the Free Software Foundation, either version 3 of the License, or
9 | * (at your option) any later version.
10 | *
11 | * This program is distributed in the hope that it will be useful,
12 | * but WITHOUT ANY WARRANTY; without even the implied warranty of
13 | * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
14 | * GNU General Public License for more details.
15 | *
16 | * You should have received a copy of the GNU General Public License
17 | * along with this program. If not, see .
18 | */
19 |
20 | package com.vishnu.whatsappcleaner.model
21 |
22 | import com.vishnu.whatsappcleaner.R
23 | import java.io.Serializable
24 |
25 | data class ListDirectory(
26 | val name: String,
27 | val path: String,
28 | val icon: Int,
29 | val hasSent: Boolean = true,
30 | val hasPrivate: Boolean = true,
31 | var size: String = "0 B"
32 | ) : Serializable {
33 | companion object {
34 | private const val serialVersionUID: Long = -5435756175248173106L
35 |
36 | fun getDirectoryList(homePath: String): List = listOf(
37 | ListDirectory(
38 | "Images",
39 | "$homePath/Media/WhatsApp Images",
40 | R.drawable.image
41 | ),
42 | ListDirectory(
43 | "Videos",
44 | "$homePath/Media/WhatsApp Video",
45 | R.drawable.video
46 | ),
47 | ListDirectory(
48 | "Documents",
49 | "$homePath/Media/WhatsApp Documents",
50 | R.drawable.document
51 | ),
52 |
53 | ListDirectory(
54 | "Audios",
55 | "$homePath/Media/WhatsApp Audio",
56 | R.drawable.audio
57 | ),
58 | ListDirectory(
59 | "Statuses",
60 | "$homePath/Media/.Statuses",
61 | R.drawable.status,
62 | false,
63 | false
64 | ),
65 |
66 | ListDirectory(
67 | "Voice Notes",
68 | "$homePath/Media/WhatsApp Voice Notes",
69 | R.drawable.voice,
70 | false,
71 | false
72 | ),
73 | ListDirectory(
74 | "Video Notes",
75 | "$homePath/Media/WhatsApp Video Notes",
76 | R.drawable.video_notes,
77 | false,
78 | false
79 | ),
80 |
81 | ListDirectory(
82 | "GIFs",
83 | "$homePath/Media/WhatsApp Animated Gifs",
84 | R.drawable.gif
85 | ),
86 | ListDirectory(
87 | "Wallpapers",
88 | "$homePath/Media/WallPaper",
89 | R.drawable.wallpaper,
90 | false,
91 | false
92 | ),
93 | ListDirectory(
94 | "Stickers",
95 | "$homePath/Media/WhatsApp Stickers",
96 | R.drawable.sticker,
97 | false,
98 | false
99 | ),
100 | ListDirectory(
101 | "Profile Photos",
102 | "$homePath/Media/WhatsApp Profile Photos",
103 | R.drawable.profile,
104 | false,
105 | false
106 | ),
107 | )
108 | }
109 | }
110 |
--------------------------------------------------------------------------------
/app/src/main/java/com/vishnu/whatsappcleaner/model/ListFile.kt:
--------------------------------------------------------------------------------
1 | /*
2 | * Copyright (C) 2025 Vishnu Sanal T
3 | *
4 | * This file is part of WhatsAppCleaner.
5 | *
6 | * Quotes Status Creator is free software: you can redistribute it and/or modify
7 | * it under the terms of the GNU General Public License as published by
8 | * the Free Software Foundation, either version 3 of the License, or
9 | * (at your option) any later version.
10 | *
11 | * This program is distributed in the hope that it will be useful,
12 | * but WITHOUT ANY WARRANTY; without even the implied warranty of
13 | * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
14 | * GNU General Public License for more details.
15 | *
16 | * You should have received a copy of the GNU General Public License
17 | * along with this program. If not, see .
18 | */
19 |
20 | package com.vishnu.whatsappcleaner.model
21 |
22 | import java.io.File
23 |
24 | data class ListFile(
25 | val filePath: String,
26 | var size: String = "0 B",
27 | var isSelected: Boolean = false,
28 | ) : File(filePath) {
29 | companion object {
30 | private const val serialVersionUID: Long = 8425722975465458623L
31 | }
32 |
33 | override fun equals(other: Any?): Boolean {
34 | if (javaClass != other?.javaClass) return false
35 | if (!super.equals(other)) return false
36 |
37 | other as ListFile
38 |
39 | if (filePath != other.filePath) return false
40 | if (size != other.size) return false
41 | if (isSelected != other.isSelected) return false
42 |
43 | return true
44 | }
45 | }
46 |
--------------------------------------------------------------------------------
/app/src/main/java/com/vishnu/whatsappcleaner/ui/Components.kt:
--------------------------------------------------------------------------------
1 | /*
2 | * Copyright (C) 2025 Vishnu Sanal T
3 | *
4 | * This file is part of WhatsAppCleaner.
5 | *
6 | * Quotes Status Creator is free software: you can redistribute it and/or modify
7 | * it under the terms of the GNU General Public License as published by
8 | * the Free Software Foundation, either version 3 of the License, or
9 | * (at your option) any later version.
10 | *
11 | * This program is distributed in the hope that it will be useful,
12 | * but WITHOUT ANY WARRANTY; without even the implied warranty of
13 | * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
14 | * GNU General Public License for more details.
15 | *
16 | * You should have received a copy of the GNU General Public License
17 | * along with this program. If not, see .
18 | */
19 |
20 | package com.vishnu.whatsappcleaner.ui
21 |
22 | import android.content.ActivityNotFoundException
23 | import android.content.Context
24 | import android.content.Intent
25 | import android.widget.Toast
26 | import androidx.compose.animation.animateColorAsState
27 | import androidx.compose.foundation.BorderStroke
28 | import androidx.compose.foundation.background
29 | import androidx.compose.foundation.basicMarquee
30 | import androidx.compose.foundation.border
31 | import androidx.compose.foundation.clickable
32 | import androidx.compose.foundation.gestures.detectTapGestures
33 | import androidx.compose.foundation.layout.Arrangement
34 | import androidx.compose.foundation.layout.Box
35 | import androidx.compose.foundation.layout.Column
36 | import androidx.compose.foundation.layout.PaddingValues
37 | import androidx.compose.foundation.layout.Row
38 | import androidx.compose.foundation.layout.Spacer
39 | import androidx.compose.foundation.layout.aspectRatio
40 | import androidx.compose.foundation.layout.fillMaxHeight
41 | import androidx.compose.foundation.layout.fillMaxSize
42 | import androidx.compose.foundation.layout.fillMaxWidth
43 | import androidx.compose.foundation.layout.padding
44 | import androidx.compose.foundation.layout.size
45 | import androidx.compose.foundation.layout.width
46 | import androidx.compose.foundation.layout.wrapContentHeight
47 | import androidx.compose.foundation.shape.CircleShape
48 | import androidx.compose.foundation.shape.RoundedCornerShape
49 | import androidx.compose.material3.ButtonDefaults
50 | import androidx.compose.material3.Card
51 | import androidx.compose.material3.CardDefaults
52 | import androidx.compose.material3.Icon
53 | import androidx.compose.material3.MaterialTheme
54 | import androidx.compose.material3.Text
55 | import androidx.compose.material3.TextButton
56 | import androidx.compose.material3.VerticalDivider
57 | import androidx.compose.runtime.Composable
58 | import androidx.compose.runtime.LaunchedEffect
59 | import androidx.compose.runtime.getValue
60 | import androidx.compose.runtime.key
61 | import androidx.compose.runtime.mutableStateOf
62 | import androidx.compose.runtime.remember
63 | import androidx.compose.runtime.setValue
64 | import androidx.compose.ui.Alignment
65 | import androidx.compose.ui.Modifier
66 | import androidx.compose.ui.draw.clip
67 | import androidx.compose.ui.draw.shadow
68 | import androidx.compose.ui.graphics.Color
69 | import androidx.compose.ui.graphics.vector.ImageVector
70 | import androidx.compose.ui.input.pointer.pointerInput
71 | import androidx.compose.ui.layout.ContentScale
72 | import androidx.compose.ui.res.painterResource
73 | import androidx.compose.ui.res.vectorResource
74 | import androidx.compose.ui.text.AnnotatedString
75 | import androidx.compose.ui.text.SpanStyle
76 | import androidx.compose.ui.text.buildAnnotatedString
77 | import androidx.compose.ui.text.font.FontWeight
78 | import androidx.compose.ui.text.style.TextAlign
79 | import androidx.compose.ui.text.style.TextOverflow
80 | import androidx.compose.ui.text.withStyle
81 | import androidx.compose.ui.unit.dp
82 | import androidx.compose.ui.unit.sp
83 | import androidx.compose.ui.zIndex
84 | import androidx.core.content.ContextCompat.startActivity
85 | import androidx.core.content.FileProvider
86 | import androidx.navigation.NavHostController
87 | import com.bumptech.glide.integration.compose.ExperimentalGlideComposeApi
88 | import com.bumptech.glide.integration.compose.GlideImage
89 | import com.bumptech.glide.integration.compose.placeholder
90 | import com.valentinilk.shimmer.shimmer
91 | import com.vishnu.whatsappcleaner.Constants
92 | import com.vishnu.whatsappcleaner.R
93 | import com.vishnu.whatsappcleaner.ViewState
94 | import com.vishnu.whatsappcleaner.model.ListDirectory
95 | import com.vishnu.whatsappcleaner.model.ListFile
96 | import java.text.DateFormat
97 |
98 | @Composable
99 | fun Title(modifier: Modifier, text: String) {
100 | Text(
101 | modifier = modifier.padding(8.dp),
102 | text = text,
103 | fontSize = 24.sp,
104 | textAlign = TextAlign.Start,
105 | fontWeight = FontWeight.Bold,
106 | )
107 | }
108 |
109 | @Composable
110 | fun Banner(modifier: Modifier, directoryItem: ViewState>>) {
111 | val bgColor = MaterialTheme.colorScheme.primaryContainer
112 | val textColor = MaterialTheme.colorScheme.onPrimaryContainer
113 |
114 | Column(
115 | modifier = modifier,
116 | verticalArrangement = Arrangement.Center,
117 | horizontalAlignment = Alignment.CenterHorizontally
118 | ) {
119 | Box(
120 | modifier
121 | .padding(12.dp)
122 | .fillMaxWidth(0.4f)
123 | .aspectRatio(1f)
124 | .shadow(elevation = 16.dp, shape = CircleShape)
125 | .background(bgColor, shape = CircleShape),
126 | contentAlignment = Alignment.Center
127 | ) {
128 | Text(
129 | text = buildAnnotatedString {
130 | when (directoryItem) {
131 | is ViewState.Success -> {
132 | var size = directoryItem.data.first
133 |
134 | if (size.contains(" ")) {
135 | val split = size.split(" ")
136 | withStyle(SpanStyle(fontSize = 32.sp)) {
137 | append(split.get(0))
138 | }
139 | withStyle(SpanStyle(fontSize = 18.sp)) {
140 | append(" ${split.get(1)}")
141 | }
142 | } else {
143 | withStyle(SpanStyle(fontSize = 28.sp)) {
144 | append(size)
145 | }
146 | }
147 | }
148 |
149 | is ViewState.Loading -> withStyle(SpanStyle(fontSize = 18.sp)) {
150 | append("Loading...")
151 | }
152 |
153 | is ViewState.Error -> withStyle(SpanStyle(fontSize = 18.sp)) {
154 | append("Error")
155 | }
156 | }
157 | },
158 | style = MaterialTheme.typography.titleLarge,
159 | color = textColor,
160 | fontWeight = FontWeight.Bold,
161 | textAlign = TextAlign.Center,
162 | )
163 | }
164 | }
165 | }
166 |
167 | @Composable
168 | fun Banner(modifier: Modifier, text: AnnotatedString) {
169 | val bgColor = MaterialTheme.colorScheme.primaryContainer
170 | val textColor = MaterialTheme.colorScheme.onPrimaryContainer
171 |
172 | Column(
173 | modifier = modifier,
174 | verticalArrangement = Arrangement.Center,
175 | horizontalAlignment = Alignment.CenterHorizontally
176 | ) {
177 | Box(
178 | Modifier
179 | .padding(12.dp)
180 | .fillMaxWidth(0.4f)
181 | .aspectRatio(1f)
182 | .shadow(elevation = 16.dp, shape = CircleShape)
183 | .background(bgColor, shape = CircleShape),
184 | contentAlignment = Alignment.Center
185 | ) {
186 | Text(
187 | text = text,
188 | style = MaterialTheme.typography.titleLarge,
189 | color = textColor,
190 | fontWeight = FontWeight.Bold,
191 | textAlign = TextAlign.Center,
192 | )
193 | }
194 | }
195 | }
196 |
197 | @Composable
198 | fun SingleCard(
199 | listDirectory: ListDirectory,
200 | navController: NavHostController,
201 | ) {
202 | val bgColor = MaterialTheme.colorScheme.secondaryContainer
203 | val textColor = MaterialTheme.colorScheme.onSecondaryContainer
204 |
205 | var onClick: () -> Unit
206 | var modifier: Modifier
207 |
208 | if (listDirectory.path.contains(Constants.LIST_LOADING_INDICATION)) {
209 | modifier = Modifier.shimmer()
210 | onClick = { }
211 | } else {
212 | modifier = Modifier
213 | onClick = {
214 | navController.currentBackStackEntry?.savedStateHandle?.apply {
215 | set(Constants.DETAILS_LIST_ITEM, listDirectory)
216 | }
217 | navController.navigate(Constants.SCREEN_DETAILS)
218 | }
219 | }
220 |
221 | Card(
222 | modifier = modifier
223 | .fillMaxWidth()
224 | .padding(horizontal = 16.dp, vertical = 8.dp),
225 | colors = CardDefaults.cardColors(containerColor = bgColor),
226 | onClick = onClick
227 | ) {
228 | Row(verticalAlignment = Alignment.CenterVertically) {
229 | Box(
230 | Modifier
231 | .padding(16.dp)
232 | .fillMaxWidth(0.2f)
233 | .aspectRatio(1f)
234 | .shadow(elevation = 8.dp, shape = CircleShape)
235 | .background(bgColor, shape = CircleShape),
236 | contentAlignment = Alignment.Center
237 | ) {
238 | Icon(
239 | modifier = Modifier.padding(8.dp),
240 | imageVector = ImageVector.vectorResource(id = listDirectory.icon),
241 | contentDescription = "icon",
242 | tint = textColor
243 | )
244 | }
245 |
246 | Column(
247 | Modifier
248 | .align(Alignment.CenterVertically)
249 | .fillMaxWidth(0.75f),
250 | verticalArrangement = Arrangement.SpaceEvenly
251 | ) {
252 | Text(
253 | modifier = Modifier
254 | .fillMaxWidth()
255 | .align(Alignment.Start)
256 | .padding(2.dp),
257 | text = listDirectory.name,
258 | fontWeight = FontWeight.Medium,
259 | style = MaterialTheme.typography.titleLarge,
260 | color = textColor,
261 | )
262 |
263 | Text(
264 | modifier = Modifier
265 | .fillMaxWidth()
266 | .align(Alignment.Start)
267 | .padding(2.dp),
268 | text = listDirectory.size,
269 | fontWeight = FontWeight.Medium,
270 | style = MaterialTheme.typography.titleSmall,
271 | color = textColor,
272 | )
273 | }
274 | }
275 | }
276 | }
277 |
278 | @OptIn(ExperimentalGlideComposeApi::class)
279 | @Composable
280 | fun ItemGridCard(
281 | listFile: ListFile,
282 | navController: NavHostController,
283 | isSelected: Boolean = false,
284 | selectionEnabled: Boolean = true,
285 | toggleSelection: () -> Unit,
286 | ) {
287 | key(listFile) {
288 | // only for keeping track of the UI
289 | var selected by remember { mutableStateOf(isSelected) }
290 |
291 | var modifier = if (listFile.filePath.toString()
292 | .contains(Constants.LIST_LOADING_INDICATION)
293 | ) Modifier.shimmer()
294 | else Modifier
295 |
296 | LaunchedEffect(isSelected) {
297 | selected = isSelected
298 | }
299 |
300 | Card(
301 | modifier = modifier
302 | .fillMaxWidth()
303 | .aspectRatio(1f)
304 | .padding(
305 | if (selected) 16.dp else 8.dp
306 | ),
307 | ) {
308 | Box(
309 | Modifier
310 | .fillMaxSize()
311 | .clip(shape = RoundedCornerShape(8.dp))
312 | .pointerInput(Unit) {
313 | detectTapGestures(onLongPress = {
314 | if (!selectionEnabled) return@detectTapGestures
315 |
316 | selected = !selected
317 |
318 | if (!listFile.filePath.toString()
319 | .contains(Constants.LIST_LOADING_INDICATION)
320 | ) toggleSelection()
321 | }, onTap = {
322 | if (selectionEnabled &&
323 | !listFile.filePath.toString()
324 | .contains(Constants.LIST_LOADING_INDICATION)
325 | ) openFile(
326 | navController.context,
327 | listFile
328 | )
329 | })
330 | }
331 | ) {
332 | if (selectionEnabled) Box(
333 | Modifier
334 | .padding(8.dp)
335 | .size(24.dp)
336 | .align(Alignment.TopStart)
337 | .clip(CircleShape)
338 | .border(
339 | BorderStroke(
340 | 2.dp,
341 | if (selected) Color.Unspecified else Color.White,
342 | ),
343 | CircleShape
344 | )
345 | .aspectRatio(1f)
346 | .zIndex(4f)
347 | .clickable {
348 | selected = !selected
349 |
350 | if (!listFile.filePath.toString()
351 | .contains(Constants.LIST_LOADING_INDICATION)
352 | ) toggleSelection()
353 | }
354 | ) {
355 | if (selected) {
356 | CheckedIcon(
357 | modifier = Modifier
358 | .align(Alignment.Center)
359 | .size(24.dp),
360 | )
361 | }
362 | }
363 |
364 | if (listFile.extension.lowercase() in Constants.EXTENSIONS_IMAGE) GlideImage(
365 | model = listFile,
366 | contentScale = ContentScale.Crop,
367 | loading = placeholder(R.drawable.image),
368 | failure = placeholder(R.drawable.error),
369 | contentDescription = "details list item"
370 | )
371 | else if (listFile.extension.lowercase() in Constants.EXTENSIONS_VIDEO) {
372 | GlideImage(
373 | model = listFile,
374 | contentScale = ContentScale.Crop,
375 | loading = placeholder(R.drawable.image),
376 | failure = placeholder(R.drawable.error),
377 | contentDescription = "details list item"
378 | )
379 |
380 | Icon(
381 | modifier = Modifier
382 | .size(32.dp)
383 | .align(Alignment.Center)
384 | .clip(CircleShape)
385 | .background(MaterialTheme.colorScheme.background.copy(alpha = 0.6f))
386 | .padding(8.dp)
387 | .aspectRatio(1f)
388 | .zIndex(2f),
389 | painter = painterResource(id = R.drawable.video),
390 | contentDescription = "video",
391 | )
392 | } else if (listFile.extension.lowercase() in Constants.EXTENSIONS_DOCS) {
393 | Column {
394 | Icon(
395 | modifier = Modifier
396 | .weight(1f)
397 | .fillMaxWidth()
398 | .fillMaxHeight()
399 | .background(MaterialTheme.colorScheme.background.copy(alpha = 0.6f))
400 | .padding(8.dp),
401 | painter = painterResource(id = R.drawable.document),
402 | contentDescription = "doc",
403 | )
404 |
405 | Text(
406 | modifier = Modifier
407 | .fillMaxWidth()
408 | .wrapContentHeight()
409 | .padding(8.dp),
410 | text = listFile.name,
411 | textAlign = TextAlign.Center,
412 | maxLines = 2,
413 | minLines = 2,
414 | overflow = TextOverflow.Ellipsis
415 | )
416 | }
417 | } else if (listFile.extension.lowercase() in Constants.EXTENSIONS_AUDIO) {
418 | Column {
419 | Icon(
420 | modifier = Modifier
421 | .weight(1f)
422 | .fillMaxWidth()
423 | .fillMaxHeight()
424 | .background(MaterialTheme.colorScheme.background.copy(alpha = 0.6f))
425 | .padding(8.dp),
426 | painter = painterResource(id = R.drawable.audio),
427 | contentDescription = "audio",
428 | )
429 |
430 | Text(
431 | modifier = Modifier
432 | .fillMaxWidth()
433 | .wrapContentHeight()
434 | .padding(8.dp),
435 | text = listFile.name,
436 | textAlign = TextAlign.Center,
437 | maxLines = 2,
438 | minLines = 2,
439 | overflow = TextOverflow.Ellipsis
440 | )
441 | }
442 | } else {
443 | Column {
444 | Icon(
445 | modifier = Modifier
446 | .weight(1f)
447 | .fillMaxWidth()
448 | .fillMaxHeight()
449 | .background(MaterialTheme.colorScheme.background.copy(alpha = 0.6f))
450 | .padding(8.dp),
451 | painter = painterResource(id = R.drawable.unknown),
452 | contentDescription = "unknown",
453 | )
454 |
455 | Text(
456 | modifier = Modifier
457 | .fillMaxWidth()
458 | .wrapContentHeight()
459 | .padding(8.dp),
460 | text = listFile.name,
461 | textAlign = TextAlign.Center,
462 | maxLines = 2,
463 | minLines = 2,
464 | overflow = TextOverflow.Ellipsis
465 | )
466 | }
467 | }
468 | }
469 | }
470 | }
471 | }
472 |
473 | @OptIn(ExperimentalGlideComposeApi::class)
474 | @Composable
475 | fun ItemListCard(
476 | listFile: ListFile,
477 | navController: NavHostController,
478 | isSelected: Boolean = false,
479 | selectionEnabled: Boolean = true,
480 | toggleSelection: () -> Unit,
481 | ) {
482 | var selected by remember { mutableStateOf(isSelected) }
483 |
484 | val modifier = if (listFile.filePath.toString().contains(Constants.LIST_LOADING_INDICATION))
485 | Modifier.shimmer() else Modifier
486 |
487 | LaunchedEffect(isSelected) {
488 | selected = isSelected
489 | }
490 |
491 | Card(
492 | modifier = modifier
493 | .fillMaxWidth()
494 | .padding(vertical = 4.dp, horizontal = 8.dp),
495 | shape = RoundedCornerShape(8.dp),
496 | elevation = CardDefaults.cardElevation(defaultElevation = 4.dp)
497 | ) {
498 | Row(
499 | modifier = Modifier
500 | .fillMaxWidth()
501 | .padding(16.dp)
502 | .pointerInput(Unit) {
503 | detectTapGestures(onLongPress = {
504 | if (!selectionEnabled) return@detectTapGestures
505 |
506 | selected = !selected
507 |
508 | if (!listFile.filePath.toString()
509 | .contains(Constants.LIST_LOADING_INDICATION)
510 | ) toggleSelection()
511 | }, onTap = {
512 | if (selectionEnabled &&
513 | !listFile.filePath.toString()
514 | .contains(Constants.LIST_LOADING_INDICATION)
515 | ) openFile(
516 | navController.context,
517 | listFile
518 | )
519 | })
520 | },
521 | verticalAlignment = Alignment.CenterVertically
522 | ) {
523 | if (selectionEnabled) {
524 | Box(
525 | Modifier
526 | .size(48.dp)
527 | .clip(CircleShape)
528 | .clickable {
529 | selected = !selected
530 | if (!listFile.filePath.toString()
531 | .contains(Constants.LIST_LOADING_INDICATION)
532 | ) {
533 | toggleSelection()
534 | }
535 | }
536 | ) {
537 | when {
538 | listFile.extension.lowercase() in Constants.EXTENSIONS_IMAGE -> {
539 | GlideImage(
540 | model = listFile,
541 | contentScale = ContentScale.Crop,
542 | loading = placeholder(R.drawable.image),
543 | failure = placeholder(R.drawable.error),
544 | contentDescription = "image preview",
545 | modifier = Modifier.size(48.dp)
546 | )
547 | }
548 |
549 | listFile.extension.lowercase() in Constants.EXTENSIONS_VIDEO -> {
550 | Icon(
551 | modifier = Modifier
552 | .size(32.dp)
553 | .align(Alignment.Center)
554 | .clip(CircleShape)
555 | .background(MaterialTheme.colorScheme.background.copy(alpha = 0.6f))
556 | .padding(8.dp)
557 | .aspectRatio(1f)
558 | .zIndex(2f),
559 | painter = painterResource(id = R.drawable.video),
560 | contentDescription = "video",
561 | )
562 |
563 | GlideImage(
564 | model = listFile,
565 | contentScale = ContentScale.Crop,
566 | loading = placeholder(R.drawable.image),
567 | failure = placeholder(R.drawable.error),
568 | contentDescription = "details list item"
569 | )
570 | }
571 |
572 | listFile.extension.lowercase() in Constants.EXTENSIONS_DOCS -> {
573 | Icon(
574 | painter = painterResource(id = R.drawable.document),
575 | contentDescription = "document",
576 | modifier = Modifier.size(48.dp),
577 | tint = MaterialTheme.colorScheme.onSurface
578 | )
579 | }
580 |
581 | listFile.extension.lowercase() in Constants.EXTENSIONS_AUDIO -> {
582 | Icon(
583 | painter = painterResource(id = R.drawable.audio),
584 | contentDescription = "audio file",
585 | modifier = Modifier.size(48.dp),
586 | tint = MaterialTheme.colorScheme.onSurface
587 | )
588 | }
589 |
590 | else -> {
591 | Icon(
592 | painter = painterResource(id = R.drawable.unknown),
593 | contentDescription = "unknown file",
594 | modifier = Modifier.size(48.dp),
595 | tint = MaterialTheme.colorScheme.onSurface
596 | )
597 | }
598 | }
599 |
600 | if (selected) {
601 | CheckedIcon(
602 | modifier = Modifier
603 | .align(Alignment.Center)
604 | .size(24.dp)
605 | .zIndex(3f),
606 | )
607 | }
608 | }
609 | }
610 |
611 | Spacer(modifier = Modifier.width(8.dp))
612 |
613 | Column(
614 | modifier = Modifier.weight(1f)
615 | ) {
616 | Text(
617 | modifier = Modifier
618 | .padding(vertical = 4.dp, horizontal = 8.dp)
619 | .fillMaxWidth()
620 | .basicMarquee(),
621 | text = listFile.name,
622 | style = MaterialTheme.typography.bodyLarge,
623 | maxLines = 1,
624 | overflow = TextOverflow.Clip
625 | )
626 | Row(Modifier.padding(vertical = 4.dp, horizontal = 8.dp)) {
627 | Text(
628 | text = listFile.extension.uppercase(),
629 | style = MaterialTheme.typography.bodySmall,
630 | color = MaterialTheme.colorScheme.onSurfaceVariant
631 | )
632 |
633 | VerticalDivider(
634 | modifier = Modifier.padding(2.dp),
635 | thickness = 1.dp,
636 | color = MaterialTheme.colorScheme.onSurfaceVariant
637 | )
638 |
639 | Text(
640 | text = listFile.size,
641 | style = MaterialTheme.typography.bodySmall,
642 | color = MaterialTheme.colorScheme.onSurfaceVariant
643 | )
644 |
645 | VerticalDivider(
646 | modifier = Modifier.padding(2.dp),
647 | thickness = 1.dp,
648 | color = MaterialTheme.colorScheme.onSurfaceVariant
649 | )
650 |
651 | Text(
652 | text = DateFormat.getDateInstance().format(listFile.lastModified()),
653 | style = MaterialTheme.typography.bodySmall,
654 | color = MaterialTheme.colorScheme.onSurfaceVariant
655 | )
656 | }
657 | }
658 | }
659 | }
660 | }
661 |
662 | @Composable
663 | fun CheckedIcon(modifier: Modifier = Modifier) {
664 | Box(
665 | modifier = modifier
666 | .background(MaterialTheme.colorScheme.onPrimary, shape = CircleShape),
667 | contentAlignment = Alignment.Center
668 | ) {
669 | Icon(
670 | modifier = Modifier.matchParentSize(),
671 | painter = painterResource(id = R.drawable.check_circle_filled),
672 | tint = MaterialTheme.colorScheme.primary,
673 | contentDescription = "checkbox"
674 | )
675 | }
676 | }
677 |
678 | @Composable
679 | fun CleanUpButton(
680 | modifier: Modifier = Modifier,
681 | selectedItems: List,
682 | onShowDialog: () -> Unit
683 | ) {
684 | val isEnabled = selectedItems.isNotEmpty()
685 |
686 | val containerColor by animateColorAsState(
687 | targetValue = if (isEnabled)
688 | MaterialTheme.colorScheme.primary
689 | else
690 | MaterialTheme.colorScheme.surfaceVariant,
691 | label = "ContainerColorAnimation"
692 | )
693 |
694 | val contentColor by animateColorAsState(
695 | targetValue = if (isEnabled)
696 | MaterialTheme.colorScheme.onPrimary
697 | else
698 | MaterialTheme.colorScheme.onSurfaceVariant,
699 | label = "ContentColorAnimation"
700 | )
701 |
702 | TextButton(
703 | modifier = modifier.padding(2.dp),
704 | colors = ButtonDefaults.textButtonColors(
705 | containerColor = containerColor,
706 | contentColor = contentColor,
707 | disabledContainerColor = containerColor,
708 | disabledContentColor = contentColor
709 | ),
710 | shape = RoundedCornerShape(64.dp),
711 | contentPadding = PaddingValues(8.dp),
712 | enabled = isEnabled,
713 | onClick = {
714 | if (selectedItems.isNotEmpty()) onShowDialog()
715 | }
716 | ) {
717 | Text(
718 | text = "Cleanup",
719 | fontWeight = FontWeight.Medium,
720 | style = MaterialTheme.typography.titleLarge,
721 | fontSize = 18.sp,
722 | letterSpacing = 1.sp
723 | )
724 | }
725 | }
726 |
727 | fun openFile(context: Context, listFile: ListFile) {
728 | try {
729 | startActivity(
730 | context,
731 | Intent(
732 | Intent.ACTION_VIEW,
733 | FileProvider.getUriForFile(
734 | context,
735 | context.packageName + ".provider",
736 | listFile
737 | )
738 | ).addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION),
739 | null
740 | )
741 | } catch (e: ActivityNotFoundException) {
742 | e.printStackTrace()
743 | Toast.makeText(
744 | context,
745 | "No application found to open this file.",
746 | Toast.LENGTH_SHORT
747 | ).show()
748 | } catch (e: IllegalArgumentException) {
749 | e.printStackTrace()
750 | Toast.makeText(
751 | context,
752 | "Something went wrong...",
753 | Toast.LENGTH_SHORT
754 | ).show()
755 | }
756 | }
757 |
--------------------------------------------------------------------------------
/app/src/main/java/com/vishnu/whatsappcleaner/ui/CustomTabLayout.kt:
--------------------------------------------------------------------------------
1 | /*
2 | * Copyright (C) 2025 Vishnu Sanal T
3 | *
4 | * This file is part of WhatsAppCleaner.
5 | *
6 | * Quotes Status Creator is free software: you can redistribute it and/or modify
7 | * it under the terms of the GNU General Public License as published by
8 | * the Free Software Foundation, either version 3 of the License, or
9 | * (at your option) any later version.
10 | *
11 | * This program is distributed in the hope that it will be useful,
12 | * but WITHOUT ANY WARRANTY; without even the implied warranty of
13 | * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
14 | * GNU General Public License for more details.
15 | *
16 | * You should have received a copy of the GNU General Public License
17 | * along with this program. If not, see .
18 | */
19 |
20 | package com.vishnu.whatsappcleaner.ui
21 |
22 | import androidx.compose.animation.animateColorAsState
23 | import androidx.compose.animation.core.LinearEasing
24 | import androidx.compose.animation.core.animateDpAsState
25 | import androidx.compose.animation.core.tween
26 | import androidx.compose.foundation.background
27 | import androidx.compose.foundation.clickable
28 | import androidx.compose.foundation.layout.Arrangement
29 | import androidx.compose.foundation.layout.Box
30 | import androidx.compose.foundation.layout.IntrinsicSize
31 | import androidx.compose.foundation.layout.Row
32 | import androidx.compose.foundation.layout.fillMaxHeight
33 | import androidx.compose.foundation.layout.height
34 | import androidx.compose.foundation.layout.offset
35 | import androidx.compose.foundation.layout.padding
36 | import androidx.compose.foundation.layout.width
37 | import androidx.compose.foundation.shape.CircleShape
38 | import androidx.compose.material3.MaterialTheme
39 | import androidx.compose.material3.Text
40 | import androidx.compose.runtime.Composable
41 | import androidx.compose.runtime.getValue
42 | import androidx.compose.ui.Modifier
43 | import androidx.compose.ui.draw.clip
44 | import androidx.compose.ui.graphics.Color
45 | import androidx.compose.ui.platform.LocalConfiguration
46 | import androidx.compose.ui.text.style.TextAlign
47 | import androidx.compose.ui.tooling.preview.Preview
48 | import androidx.compose.ui.unit.Dp
49 | import androidx.compose.ui.unit.dp
50 | import com.vishnu.whatsappcleaner.ui.theme.WhatsAppCleanerTheme
51 |
52 | @Composable
53 | fun CustomTabLayout(
54 | modifier: Modifier = Modifier,
55 | selectedItemIndex: Int,
56 | items: List,
57 | onTabSelected: (index: Int) -> Unit,
58 | ) {
59 | val screenWidth = LocalConfiguration.current.screenWidthDp.dp
60 | val tabWidth = (screenWidth - 32.dp) / items.size
61 |
62 | val indicatorOffset: Dp by animateDpAsState(
63 | targetValue = tabWidth * selectedItemIndex,
64 | animationSpec = tween(easing = LinearEasing),
65 | label = "indicator-offset"
66 | )
67 |
68 | Box(
69 | modifier = modifier
70 | .clip(CircleShape)
71 | .height(intrinsicSize = IntrinsicSize.Min),
72 | ) {
73 | TabIndicator(
74 | indicatorWidth = tabWidth,
75 | indicatorOffset = indicatorOffset,
76 | indicatorColor = MaterialTheme.colorScheme.primary,
77 | )
78 | Row(
79 | modifier = Modifier.clip(CircleShape),
80 | horizontalArrangement = Arrangement.Center,
81 | ) {
82 | items.mapIndexed { index, text ->
83 | val isSelected = index == selectedItemIndex
84 |
85 | TabItem(
86 | isSelected = isSelected,
87 | onClick = {
88 | onTabSelected(index)
89 | },
90 | tabWidth = tabWidth,
91 | text = text,
92 | )
93 | }
94 | }
95 | }
96 | }
97 |
98 | @Composable
99 | private fun TabItem(
100 | isSelected: Boolean,
101 | onClick: () -> Unit,
102 | tabWidth: Dp,
103 | text: String,
104 | ) {
105 | val tabTextColor: Color by animateColorAsState(
106 | targetValue = if (isSelected) {
107 | MaterialTheme.colorScheme.onPrimary
108 | } else {
109 | MaterialTheme.colorScheme.onSurfaceVariant
110 | },
111 | animationSpec = tween(easing = LinearEasing),
112 | )
113 |
114 | Text(
115 | modifier = Modifier
116 | .clip(CircleShape)
117 | .clickable {
118 | onClick()
119 | }
120 | .width(tabWidth)
121 | .padding(12.dp),
122 | text = text,
123 | color = tabTextColor,
124 | textAlign = TextAlign.Center,
125 | )
126 | }
127 |
128 | @Composable
129 | private fun TabIndicator(
130 | indicatorWidth: Dp,
131 | indicatorOffset: Dp,
132 | indicatorColor: Color,
133 | ) {
134 | Box(
135 | modifier = Modifier
136 | .fillMaxHeight()
137 | .width(
138 | width = indicatorWidth,
139 | )
140 | .offset(
141 | x = indicatorOffset,
142 | )
143 | .clip(
144 | shape = CircleShape,
145 | )
146 | .background(
147 | color = indicatorColor,
148 | ),
149 | )
150 | }
151 |
152 | @Preview(showBackground = true, widthDp = 400)
153 | @Composable
154 | private fun CustomTabPreview() {
155 | WhatsAppCleanerTheme {
156 | CustomTabLayout(
157 | modifier = Modifier.padding(12.dp),
158 | items = listOf("Tab 1", "Tab 2", "Tab 3"),
159 | selectedItemIndex = 0,
160 | onTabSelected = {},
161 | )
162 | }
163 | }
164 |
--------------------------------------------------------------------------------
/app/src/main/java/com/vishnu/whatsappcleaner/ui/DetailsScreen.kt:
--------------------------------------------------------------------------------
1 | /*
2 | * Copyright (C) 2025 Vishnu Sanal T
3 | *
4 | * This file is part of WhatsAppCleaner.
5 | *
6 | * Quotes Status Creator is free software: you can redistribute it and/or modify
7 | * it under the terms of the GNU General Public License as published by
8 | * the Free Software Foundation, either version 3 of the License, or
9 | * (at your option) any later version.
10 | *
11 | * This program is distributed in the hope that it will be useful,
12 | * but WITHOUT ANY WARRANTY; without even the implied warranty of
13 | * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
14 | * GNU General Public License for more details.
15 | *
16 | * You should have received a copy of the GNU General Public License
17 | * along with this program. If not, see .
18 | */
19 |
20 | package com.vishnu.whatsappcleaner.ui
21 |
22 | import androidx.compose.animation.AnimatedVisibility
23 | import androidx.compose.foundation.background
24 | import androidx.compose.foundation.clickable
25 | import androidx.compose.foundation.gestures.awaitEachGesture
26 | import androidx.compose.foundation.gestures.awaitFirstDown
27 | import androidx.compose.foundation.gestures.waitForUpOrCancellation
28 | import androidx.compose.foundation.layout.Arrangement
29 | import androidx.compose.foundation.layout.Column
30 | import androidx.compose.foundation.layout.PaddingValues
31 | import androidx.compose.foundation.layout.Row
32 | import androidx.compose.foundation.layout.Spacer
33 | import androidx.compose.foundation.layout.fillMaxSize
34 | import androidx.compose.foundation.layout.fillMaxWidth
35 | import androidx.compose.foundation.layout.padding
36 | import androidx.compose.foundation.layout.size
37 | import androidx.compose.foundation.layout.width
38 | import androidx.compose.foundation.layout.wrapContentHeight
39 | import androidx.compose.foundation.lazy.LazyColumn
40 | import androidx.compose.foundation.lazy.LazyListState
41 | import androidx.compose.foundation.lazy.grid.GridCells
42 | import androidx.compose.foundation.lazy.grid.LazyGridState
43 | import androidx.compose.foundation.lazy.grid.LazyVerticalGrid
44 | import androidx.compose.foundation.lazy.grid.items
45 | import androidx.compose.foundation.lazy.items
46 | import androidx.compose.foundation.pager.HorizontalPager
47 | import androidx.compose.foundation.pager.rememberPagerState
48 | import androidx.compose.foundation.shape.RoundedCornerShape
49 | import androidx.compose.material3.ButtonDefaults
50 | import androidx.compose.material3.Card
51 | import androidx.compose.material3.CardDefaults
52 | import androidx.compose.material3.DatePickerDialog
53 | import androidx.compose.material3.DateRangePicker
54 | import androidx.compose.material3.DateRangePickerState
55 | import androidx.compose.material3.ExperimentalMaterial3Api
56 | import androidx.compose.material3.Icon
57 | import androidx.compose.material3.IconButton
58 | import androidx.compose.material3.LinearProgressIndicator
59 | import androidx.compose.material3.MaterialTheme
60 | import androidx.compose.material3.OutlinedTextField
61 | import androidx.compose.material3.RadioButton
62 | import androidx.compose.material3.RadioButtonDefaults
63 | import androidx.compose.material3.Scaffold
64 | import androidx.compose.material3.Surface
65 | import androidx.compose.material3.Switch
66 | import androidx.compose.material3.Text
67 | import androidx.compose.material3.TextButton
68 | import androidx.compose.material3.TopAppBar
69 | import androidx.compose.material3.rememberDateRangePickerState
70 | import androidx.compose.runtime.Composable
71 | import androidx.compose.runtime.LaunchedEffect
72 | import androidx.compose.runtime.MutableState
73 | import androidx.compose.runtime.collectAsState
74 | import androidx.compose.runtime.derivedStateOf
75 | import androidx.compose.runtime.getValue
76 | import androidx.compose.runtime.mutableStateListOf
77 | import androidx.compose.runtime.mutableStateOf
78 | import androidx.compose.runtime.remember
79 | import androidx.compose.runtime.setValue
80 | import androidx.compose.ui.Alignment
81 | import androidx.compose.ui.Modifier
82 | import androidx.compose.ui.input.pointer.PointerEventPass
83 | import androidx.compose.ui.input.pointer.pointerInput
84 | import androidx.compose.ui.res.painterResource
85 | import androidx.compose.ui.text.SpanStyle
86 | import androidx.compose.ui.text.buildAnnotatedString
87 | import androidx.compose.ui.text.font.FontWeight
88 | import androidx.compose.ui.text.withStyle
89 | import androidx.compose.ui.unit.dp
90 | import androidx.compose.ui.unit.sp
91 | import androidx.compose.ui.window.Dialog
92 | import androidx.compose.ui.window.DialogProperties
93 | import androidx.navigation.NavHostController
94 | import com.vishnu.whatsappcleaner.Constants
95 | import com.vishnu.whatsappcleaner.MainViewModel
96 | import com.vishnu.whatsappcleaner.R
97 | import com.vishnu.whatsappcleaner.Target
98 | import com.vishnu.whatsappcleaner.model.ListDirectory
99 | import com.vishnu.whatsappcleaner.model.ListFile
100 | import java.text.DateFormat
101 |
102 | @OptIn(ExperimentalMaterial3Api::class)
103 | @Composable
104 | fun DetailsScreen(navController: NavHostController, viewModel: MainViewModel) {
105 | val listDirectory = navController.previousBackStackEntry?.savedStateHandle?.get(
106 | Constants.DETAILS_LIST_ITEM
107 | )
108 |
109 | if (listDirectory == null) return Surface {}
110 |
111 | val fileList by viewModel.fileList.collectAsState()
112 | val sentList by viewModel.sentList.collectAsState()
113 | val privateList by viewModel.privateList.collectAsState()
114 | val isInProgress by viewModel.isInProgress.collectAsState()
115 | val isGridView by viewModel.isGridView.collectAsState()
116 | val fileReloadTrigger by viewModel.fileReloadTrigger.collectAsState()
117 |
118 | var selectedItems = remember { mutableStateListOf() }
119 | var sortBy = remember { mutableStateOf("Date") }
120 | var isSortDescending = remember { mutableStateOf(true) }
121 |
122 | val dateRangePickerState = rememberDateRangePickerState()
123 |
124 | var showConfirmationDialog by remember { mutableStateOf(false) }
125 | var showSortDialog by remember { mutableStateOf(false) }
126 | var isAllSelected by remember { mutableStateOf(false) }
127 |
128 | val tabs = listOf("Received", "Sent", "Private")
129 |
130 | val pagerState = rememberPagerState(
131 | initialPage = 0,
132 | pageCount = {
133 | if (listDirectory.hasSent) {
134 | if (listDirectory.hasPrivate) 3
135 | else 2
136 | } else 1
137 | }
138 | )
139 |
140 | var selectedTabIndex by remember { mutableStateOf(pagerState.currentPage) }
141 |
142 | val gridStates = remember {
143 | List(3) { LazyGridState() }
144 | }
145 | val listStates = remember {
146 | List(3) { LazyListState() }
147 | }
148 |
149 | val showHeader by remember {
150 | derivedStateOf {
151 | if (isGridView) {
152 | gridStates[pagerState.currentPage].firstVisibleItemIndex < 1
153 | } else {
154 | listStates[pagerState.currentPage].firstVisibleItemIndex < 1
155 | }
156 | }
157 | }
158 |
159 | LaunchedEffect(
160 | fileReloadTrigger,
161 | sortBy.value,
162 | isSortDescending.value,
163 | dateRangePickerState.selectedStartDateMillis,
164 | dateRangePickerState.selectedEndDateMillis
165 | ) {
166 | viewModel.getFileList(
167 | Target.Received,
168 | listDirectory.path,
169 | sortBy.value,
170 | isSortDescending.value,
171 | dateRangePickerState.selectedStartDateMillis,
172 | dateRangePickerState.selectedEndDateMillis
173 | )
174 |
175 | if (listDirectory.hasSent) {
176 | viewModel.getFileList(
177 | Target.Sent,
178 | "${listDirectory.path}/Sent",
179 | sortBy.value,
180 | isSortDescending.value,
181 | dateRangePickerState.selectedStartDateMillis,
182 | dateRangePickerState.selectedEndDateMillis
183 | )
184 | }
185 |
186 | if (listDirectory.hasPrivate) {
187 | viewModel.getFileList(
188 | Target.Private,
189 | "${listDirectory.path}/Private",
190 | sortBy.value,
191 | isSortDescending.value,
192 | dateRangePickerState.selectedStartDateMillis,
193 | dateRangePickerState.selectedEndDateMillis
194 | )
195 | }
196 | }
197 |
198 | LaunchedEffect(pagerState.currentPage) {
199 | if (selectedItems.isNotEmpty() || isAllSelected) {
200 | selectedItems.clear()
201 | isAllSelected = false
202 | }
203 | }
204 |
205 | LaunchedEffect(selectedTabIndex) {
206 | if (selectedTabIndex != pagerState.currentPage) {
207 | pagerState.animateScrollToPage(selectedTabIndex)
208 | }
209 | }
210 |
211 | LaunchedEffect(pagerState.currentPage) {
212 | selectedTabIndex = pagerState.currentPage
213 | }
214 |
215 | Scaffold(
216 | topBar = {
217 | DetailScreenTopBar(
218 | title = listDirectory.name,
219 | toggleGridView = {
220 | viewModel.toggleViewType()
221 | },
222 | isGridView = isGridView,
223 | onSortClick = {
224 | showSortDialog = true
225 | dateRangePickerState.setSelection(null, null)
226 | selectedItems.clear()
227 | isAllSelected = false
228 | }
229 | )
230 | }
231 | ) { innerPadding ->
232 | Column(
233 | modifier = Modifier
234 | .fillMaxSize()
235 | .padding(innerPadding),
236 | horizontalAlignment = Alignment.CenterHorizontally
237 | ) {
238 | if (isInProgress) LinearProgressIndicator(
239 | modifier = Modifier
240 | .fillMaxWidth()
241 | .wrapContentHeight()
242 | .padding(8.dp),
243 | )
244 |
245 | AnimatedVisibility(
246 | visible = showHeader,
247 | ) {
248 | Banner(
249 | Modifier.padding(16.dp),
250 | buildAnnotatedString {
251 | val size = listDirectory.size
252 | val parts = size.split(" ")
253 | if (parts.size == 2) {
254 | withStyle(SpanStyle(fontSize = 24.sp)) { append(parts[0]) }
255 | withStyle(SpanStyle(fontSize = 18.sp)) { append(" ${parts[1]}") }
256 | } else {
257 | withStyle(SpanStyle(fontSize = 24.sp)) { append(size) }
258 | }
259 | }
260 | )
261 | }
262 |
263 | if (listDirectory.hasSent || listDirectory.hasPrivate) {
264 | CustomTabLayout(
265 | modifier = Modifier
266 | .fillMaxWidth()
267 | .padding(horizontal = 16.dp, vertical = 8.dp),
268 | selectedItemIndex = pagerState.currentPage,
269 | items = tabs,
270 | onTabSelected = { index ->
271 | selectedTabIndex = index
272 | }
273 | )
274 | }
275 |
276 | HorizontalPager(
277 | state = pagerState,
278 | modifier = Modifier
279 | .weight(1f)
280 | .fillMaxWidth()
281 | ) { page ->
282 | val list = when (page) {
283 | 0 -> fileList
284 | 1 -> sentList
285 | else -> privateList
286 | }
287 |
288 | Column(
289 | Modifier
290 | .fillMaxSize()
291 | ) {
292 | if (list.isNotEmpty()) {
293 | IconButton(
294 | modifier = Modifier
295 | .align(Alignment.End)
296 | .padding(8.dp)
297 | .size(32.dp),
298 | onClick = {
299 | isAllSelected = !isAllSelected
300 | if (isAllSelected) selectedItems.addAll(list)
301 | else selectedItems.clear()
302 | }
303 | ) {
304 | Icon(
305 | modifier = Modifier.size(32.dp),
306 | painter = painterResource(id = if (isAllSelected) R.drawable.check_circle_filled else R.drawable.check_circle),
307 | tint = MaterialTheme.colorScheme.primary,
308 | contentDescription = "select all"
309 | )
310 | }
311 | }
312 |
313 | if (list.isNotEmpty()) {
314 | if (isGridView) {
315 | LazyVerticalGrid(
316 | state = gridStates[page],
317 | modifier = Modifier
318 | .fillMaxSize()
319 | .padding(8.dp),
320 | columns = GridCells.Fixed(3)
321 | ) {
322 | items(list) {
323 | ItemGridCard(it, navController, selectedItems.contains(it)) {
324 | if (selectedItems.contains(it)) selectedItems.remove(it)
325 | else selectedItems.add(it)
326 | }
327 | }
328 | }
329 | } else {
330 | LazyColumn(
331 | state = listStates[page],
332 | modifier = Modifier
333 | .fillMaxSize()
334 | .padding(8.dp)
335 | ) {
336 | items(list) {
337 | ItemListCard(it, navController, selectedItems.contains(it)) {
338 | if (selectedItems.contains(it)) selectedItems.remove(it)
339 | else selectedItems.add(it)
340 | }
341 | }
342 | }
343 | }
344 | } else {
345 | Column(
346 | modifier = Modifier.fillMaxSize(),
347 | verticalArrangement = Arrangement.Center,
348 | horizontalAlignment = Alignment.CenterHorizontally
349 | ) {
350 | Icon(
351 | modifier = Modifier
352 | .fillMaxSize(0.4f)
353 | .padding(8.dp),
354 | painter = painterResource(id = R.drawable.clean),
355 | contentDescription = "empty",
356 | tint = MaterialTheme.colorScheme.secondaryContainer
357 | )
358 | Text(
359 | text = "Nothing to clean",
360 | fontSize = 24.sp,
361 | fontWeight = FontWeight.Medium
362 | )
363 | }
364 | }
365 | }
366 | }
367 |
368 | CleanUpButton(
369 | modifier = Modifier
370 | .fillMaxWidth()
371 | .padding(horizontal = 16.dp, vertical = 4.dp),
372 | selectedItems = selectedItems,
373 | onShowDialog = { showConfirmationDialog = true }
374 | )
375 | }
376 | }
377 |
378 | if (showSortDialog) {
379 | SortDialog(
380 | navController,
381 | onDismissRequest = {
382 | showSortDialog = false
383 | },
384 | sortBy,
385 | isSortDescending,
386 | dateRangePickerState
387 | )
388 | }
389 |
390 | if (showConfirmationDialog) {
391 | ConfirmationDialog(
392 | onDismissRequest = {
393 | showConfirmationDialog = false
394 | selectedItems.clear()
395 | },
396 | onConfirmation = {
397 | viewModel.delete(selectedItems.toList())
398 | showConfirmationDialog = false
399 | selectedItems.clear()
400 | },
401 | selectedItems,
402 | navController
403 | )
404 | }
405 | }
406 |
407 | @OptIn(ExperimentalMaterial3Api::class)
408 | @Composable
409 | fun DetailScreenTopBar(
410 | modifier: Modifier = Modifier,
411 | title: String = "",
412 | toggleGridView: () -> Unit,
413 | isGridView: Boolean,
414 | onSortClick: () -> Unit
415 | ) {
416 | TopAppBar(
417 | modifier = modifier,
418 | title = {
419 | Title(text = title, modifier = Modifier)
420 | },
421 | actions = {
422 | IconButton(
423 | modifier = Modifier
424 | .size(32.dp),
425 | onClick = {
426 | toggleGridView()
427 | }
428 | ) {
429 | Icon(
430 | modifier = Modifier
431 | .size(32.dp),
432 | painter =
433 | if (isGridView)
434 | painterResource(id = R.drawable.ic_view_list)
435 | else
436 | painterResource(id = R.drawable.ic_grid_view),
437 | tint = MaterialTheme.colorScheme.primary,
438 | contentDescription = "grid list view",
439 | )
440 | }
441 |
442 | Spacer(modifier = Modifier.width(12.dp))
443 |
444 | IconButton(
445 | modifier = Modifier
446 | .size(32.dp),
447 | onClick = { onSortClick() }
448 | ) {
449 | Icon(
450 | modifier = Modifier.size(32.dp),
451 | painter = painterResource(id = R.drawable.ic_sort),
452 | tint = MaterialTheme.colorScheme.primary,
453 | contentDescription = "sort",
454 | )
455 | }
456 | Spacer(modifier = Modifier.width(8.dp))
457 | }
458 | )
459 | }
460 |
461 | @OptIn(ExperimentalMaterial3Api::class)
462 | @Composable
463 | fun SortDialog(
464 | navController: NavHostController,
465 | onDismissRequest: () -> Unit,
466 | sortBy: MutableState,
467 | isSortDescending: MutableState,
468 | dateRangePickerState: DateRangePickerState,
469 | ) {
470 | var showDatePicker by remember { mutableStateOf(false) }
471 |
472 | Dialog(
473 | onDismissRequest = onDismissRequest,
474 | properties = DialogProperties(
475 | usePlatformDefaultWidth = false,
476 | dismissOnClickOutside = true,
477 | dismissOnBackPress = true,
478 | decorFitsSystemWindows = true
479 | ),
480 | ) {
481 | var isDescending by remember { mutableStateOf(isSortDescending) }
482 | var selectedItem by remember { mutableStateOf(sortBy) }
483 |
484 | Card(
485 | modifier = Modifier
486 | .fillMaxWidth()
487 | .wrapContentHeight()
488 | .padding(vertical = 64.dp, horizontal = 32.dp),
489 | shape = RoundedCornerShape(16.dp),
490 | colors = CardDefaults.cardColors(containerColor = MaterialTheme.colorScheme.background)
491 | ) {
492 | Column(
493 | modifier = Modifier
494 | .fillMaxWidth()
495 | .wrapContentHeight()
496 | .padding(16.dp),
497 | ) {
498 | Text(
499 | modifier = Modifier
500 | .wrapContentHeight()
501 | .padding(8.dp),
502 | text = "Sort Criteria",
503 | style = MaterialTheme.typography.headlineLarge,
504 | )
505 |
506 | listOf(
507 | "Date",
508 | "Size",
509 | "Name",
510 | ).forEach { item ->
511 | Row(
512 | modifier = Modifier
513 | .fillMaxWidth()
514 | .clickable {
515 | selectedItem.value = item
516 | },
517 | verticalAlignment = Alignment.CenterVertically,
518 | ) {
519 | RadioButton(
520 | selected = sortBy.value == item,
521 | onClick = {
522 | selectedItem.value = item
523 | },
524 | enabled = true,
525 | colors = RadioButtonDefaults.colors(
526 | selectedColor = MaterialTheme.colorScheme.primary
527 | )
528 | )
529 | Text(text = item, modifier = Modifier.padding(start = 8.dp))
530 | }
531 | }
532 |
533 | if (showDatePicker) DatePickerDialog(
534 | onDismissRequest = { showDatePicker = false },
535 | confirmButton = {
536 | TextButton(
537 | onClick = {
538 | showDatePicker = false
539 | }
540 | ) { Text("OK") }
541 | },
542 | dismissButton = {
543 | TextButton(
544 | onClick = {
545 | showDatePicker = false
546 | }
547 | ) { Text("Cancel") }
548 | }
549 | ) {
550 | DateRangePicker(state = dateRangePickerState)
551 | }
552 |
553 | Row(
554 | modifier = Modifier
555 | .padding(8.dp),
556 | verticalAlignment = Alignment.CenterVertically
557 | ) {
558 | when (selectedItem.value) {
559 | "Date" -> {
560 | OutlinedTextField(
561 | modifier = Modifier
562 | .fillMaxWidth()
563 | .weight(1f)
564 | .pointerInput(Unit) {
565 | awaitEachGesture {
566 | awaitFirstDown(pass = PointerEventPass.Initial)
567 | val upEvent =
568 | waitForUpOrCancellation(pass = PointerEventPass.Initial)
569 | if (upEvent != null) {
570 | showDatePicker = true
571 | }
572 | }
573 | },
574 | readOnly = true,
575 | value = if (dateRangePickerState.selectedEndDateMillis != null)
576 | DateFormat.getDateInstance()
577 | .format(dateRangePickerState.selectedStartDateMillis)
578 | else
579 | "N/A",
580 | onValueChange = {},
581 | label = { Text("From Date") },
582 | )
583 |
584 | Spacer(modifier = Modifier.width(8.dp))
585 |
586 | OutlinedTextField(
587 | modifier = Modifier
588 | .fillMaxWidth()
589 | .weight(1f)
590 | .pointerInput(Unit) {
591 | awaitEachGesture {
592 | awaitFirstDown(pass = PointerEventPass.Initial)
593 | val upEvent =
594 | waitForUpOrCancellation(pass = PointerEventPass.Initial)
595 | if (upEvent != null) {
596 | showDatePicker = true
597 | }
598 | }
599 | },
600 | readOnly = true,
601 | value = if (dateRangePickerState.selectedEndDateMillis != null)
602 | DateFormat.getDateInstance()
603 | .format(dateRangePickerState.selectedEndDateMillis)
604 | else
605 | "N/A",
606 | onValueChange = {},
607 | label = { Text("To Date") },
608 | )
609 | }
610 | }
611 | }
612 |
613 | Row(
614 | modifier = Modifier
615 | .padding(8.dp),
616 | verticalAlignment = Alignment.CenterVertically
617 | ) {
618 | Switch(
619 | checked = isDescending.value,
620 | onCheckedChange = { isDescending.value = it }
621 | )
622 |
623 | Text(text = "Descending", modifier = Modifier.padding(start = 8.dp))
624 | }
625 |
626 | TextButton(
627 | modifier = Modifier
628 | .fillMaxWidth()
629 | .padding(4.dp),
630 | colors = ButtonDefaults.outlinedButtonColors(containerColor = MaterialTheme.colorScheme.primaryContainer),
631 | shape = RoundedCornerShape(16.dp),
632 | contentPadding = PaddingValues(4.dp),
633 | onClick = {
634 | sortBy.value = selectedItem.value
635 | isSortDescending.value = isDescending.value
636 | onDismissRequest()
637 | }
638 | ) {
639 | Text(
640 | text = buildAnnotatedString {
641 | withStyle(
642 | SpanStyle(
643 | color = MaterialTheme.colorScheme.onPrimaryContainer,
644 | fontWeight = FontWeight.Medium,
645 | letterSpacing = 1.sp
646 | )
647 | ) {
648 | append("Apply")
649 | }
650 | },
651 | fontWeight = FontWeight.Medium,
652 | style = MaterialTheme.typography.bodyLarge
653 | )
654 | }
655 | }
656 | }
657 | }
658 | }
659 |
660 | @Composable
661 | fun ConfirmationDialog(
662 | onDismissRequest: () -> Unit,
663 | onConfirmation: () -> Unit,
664 | list: List,
665 | navController: NavHostController
666 | ) {
667 | Dialog(
668 | onDismissRequest = onDismissRequest,
669 | properties = DialogProperties(
670 | usePlatformDefaultWidth = false,
671 | dismissOnClickOutside = true,
672 | dismissOnBackPress = true,
673 | decorFitsSystemWindows = true
674 | ),
675 | ) {
676 | Card(
677 | modifier = Modifier
678 | .fillMaxWidth()
679 | .wrapContentHeight()
680 | .padding(vertical = 64.dp, horizontal = 32.dp),
681 | shape = RoundedCornerShape(16.dp),
682 | colors = CardDefaults.cardColors(containerColor = MaterialTheme.colorScheme.background)
683 | ) {
684 | Column(
685 | modifier = Modifier
686 | .fillMaxWidth()
687 | .wrapContentHeight()
688 | .padding(16.dp),
689 | ) {
690 | Row(
691 | modifier = Modifier.wrapContentHeight(),
692 | horizontalArrangement = Arrangement.Center,
693 | ) {
694 | Column(
695 | Modifier
696 | .weight(0.6f)
697 | .fillMaxWidth()
698 | ) {
699 | Text(
700 | modifier = Modifier
701 | .wrapContentHeight()
702 | .padding(vertical = 4.dp)
703 | .align(Alignment.Start),
704 | text = "Confirm Cleanup",
705 | style = MaterialTheme.typography.titleLarge,
706 | )
707 |
708 | Text(
709 | modifier = Modifier
710 | .wrapContentHeight()
711 | .padding(vertical = 2.dp)
712 | .align(Alignment.Start),
713 | text = "The following files will be deleted.",
714 | style = MaterialTheme.typography.bodyMedium,
715 | )
716 | }
717 |
718 | TextButton(
719 | modifier = Modifier
720 | .weight(0.4f)
721 | .fillMaxWidth()
722 | .padding(8.dp),
723 | colors = ButtonDefaults.outlinedButtonColors(
724 | containerColor = MaterialTheme.colorScheme.primaryContainer,
725 | contentColor = MaterialTheme.colorScheme.onPrimaryContainer
726 | ),
727 | shape = RoundedCornerShape(16.dp),
728 | contentPadding = PaddingValues(vertical = 16.dp, horizontal = 16.dp),
729 | onClick = onConfirmation,
730 | content = {
731 | Text(
732 | text = "Confirm",
733 | style = MaterialTheme.typography.titleMedium,
734 | fontWeight = FontWeight.Medium
735 | )
736 | },
737 | )
738 | }
739 |
740 | // todo: no preview & replace it with count + red colored CTA
741 | LazyVerticalGrid(
742 | modifier = Modifier
743 | .wrapContentHeight(),
744 | columns = GridCells.Fixed(3),
745 | ) {
746 | items(list) { ItemGridCard(it, navController, selectionEnabled = false) {} }
747 | }
748 | }
749 | }
750 | }
751 | }
752 |
--------------------------------------------------------------------------------
/app/src/main/java/com/vishnu/whatsappcleaner/ui/HomeScreen.kt:
--------------------------------------------------------------------------------
1 | /*
2 | * Copyright (C) 2025 Vishnu Sanal T
3 | *
4 | * This file is part of WhatsAppCleaner.
5 | *
6 | * Quotes Status Creator is free software: you can redistribute it and/or modify
7 | * it under the terms of the GNU General Public License as published by
8 | * the Free Software Foundation, either version 3 of the License, or
9 | * (at your option) any later version.
10 | *
11 | * This program is distributed in the hope that it will be useful,
12 | * but WITHOUT ANY WARRANTY; without even the implied warranty of
13 | * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
14 | * GNU General Public License for more details.
15 | *
16 | * You should have received a copy of the GNU General Public License
17 | * along with this program. If not, see .
18 | */
19 |
20 | package com.vishnu.whatsappcleaner.ui
21 |
22 | import androidx.compose.foundation.layout.Arrangement
23 | import androidx.compose.foundation.layout.Column
24 | import androidx.compose.foundation.layout.fillMaxSize
25 | import androidx.compose.foundation.layout.padding
26 | import androidx.compose.foundation.lazy.LazyColumn
27 | import androidx.compose.foundation.lazy.items
28 | import androidx.compose.material3.ExperimentalMaterial3Api
29 | import androidx.compose.material3.MaterialTheme
30 | import androidx.compose.material3.Scaffold
31 | import androidx.compose.material3.Text
32 | import androidx.compose.material3.TopAppBar
33 | import androidx.compose.runtime.Composable
34 | import androidx.compose.runtime.LaunchedEffect
35 | import androidx.compose.runtime.State
36 | import androidx.compose.runtime.collectAsState
37 | import androidx.compose.runtime.getValue
38 | import androidx.compose.runtime.mutableStateOf
39 | import androidx.compose.runtime.remember
40 | import androidx.compose.runtime.setValue
41 | import androidx.compose.ui.Alignment
42 | import androidx.compose.ui.Modifier
43 | import androidx.compose.ui.res.stringResource
44 | import androidx.compose.ui.text.SpanStyle
45 | import androidx.compose.ui.text.buildAnnotatedString
46 | import androidx.compose.ui.text.withStyle
47 | import androidx.compose.ui.unit.dp
48 | import androidx.compose.ui.unit.sp
49 | import androidx.navigation.NavHostController
50 | import com.valentinilk.shimmer.shimmer
51 | import com.vishnu.whatsappcleaner.BuildConfig
52 | import com.vishnu.whatsappcleaner.Constants
53 | import com.vishnu.whatsappcleaner.MainViewModel
54 | import com.vishnu.whatsappcleaner.R
55 | import com.vishnu.whatsappcleaner.ViewState
56 | import com.vishnu.whatsappcleaner.model.ListDirectory
57 |
58 | @Composable
59 | fun HomeScreen(navController: NavHostController, viewModel: MainViewModel) {
60 | var forceReload by remember { mutableStateOf(false) }
61 |
62 | forceReload = navController.currentBackStackEntry?.savedStateHandle?.get(
63 | Constants.FORCE_RELOAD_FILE_LIST
64 | ) ?: false
65 |
66 | val directoryItem: State>>> =
67 | viewModel.directoryItem.collectAsState()
68 |
69 | if (forceReload) {
70 | viewModel.getDirectoryList()
71 | }
72 |
73 | // trigger when moving from permission screen to home screen on Android 15
74 | LaunchedEffect(key1 = null) {
75 | viewModel.getDirectoryList()
76 | }
77 |
78 | Scaffold(
79 | topBar = {
80 | HomeTopBar(modifier = Modifier)
81 | }
82 | ) { contentPadding ->
83 | Column(
84 | Modifier.padding(contentPadding),
85 | horizontalAlignment = Alignment.CenterHorizontally,
86 | verticalArrangement = Arrangement.Center
87 | ) {
88 | val modifier = if (directoryItem.value is ViewState.Success) Modifier
89 | else Modifier.shimmer()
90 |
91 | when (directoryItem.value) {
92 | is ViewState.Success -> {
93 | LazyColumn(
94 | Modifier.weight(1f),
95 | horizontalAlignment = Alignment.CenterHorizontally
96 | ) {
97 | item {
98 | ListSizeHeader(
99 | modifier,
100 | directoryItem
101 | )
102 | }
103 | items((directoryItem.value as ViewState.Success>>).data.second) {
104 | SingleCard(it, navController)
105 | }
106 | }
107 | }
108 |
109 | is ViewState.Loading -> LazyColumn(
110 | Modifier.weight(1f),
111 | horizontalAlignment = Alignment.CenterHorizontally
112 | ) {
113 | item {
114 | ListSizeHeader(
115 | modifier,
116 | directoryItem
117 | )
118 | }
119 | items(ListDirectory.getDirectoryList(Constants.LIST_LOADING_INDICATION)) {
120 | SingleCard(it, navController)
121 | }
122 | }
123 |
124 | is ViewState.Error -> Text(
125 | modifier = Modifier
126 | .fillMaxSize()
127 | .align(Alignment.CenterHorizontally)
128 | .padding(16.dp),
129 | text = "Error loading directory list\n${(directoryItem.value as ViewState.Error).message}",
130 | style = MaterialTheme.typography.labelSmall,
131 | )
132 | }
133 |
134 | if (BuildConfig.DEBUG)
135 | Text(
136 | modifier = Modifier
137 | .align(Alignment.CenterHorizontally)
138 | .padding(4.dp),
139 | text = "Debug Build ${BuildConfig.VERSION_NAME}",
140 | style = MaterialTheme.typography.labelSmall,
141 | )
142 | }
143 | }
144 | }
145 |
146 | @Composable
147 | fun ListSizeHeader(
148 | modifier: Modifier = Modifier,
149 | viewState: State>>>
150 | ) = Banner(
151 | modifier.padding(16.dp),
152 | buildAnnotatedString {
153 | when (viewState.value) {
154 | is ViewState.Success -> {
155 | var size =
156 | (viewState.value as ViewState.Success>>).data.first
157 |
158 | if (size.contains(" ")) {
159 | val split = size.split(" ")
160 | withStyle(SpanStyle(fontSize = 24.sp)) {
161 | append(split.get(0))
162 | }
163 | withStyle(SpanStyle(fontSize = 18.sp)) {
164 | append(" ${split.get(1)}")
165 | }
166 | } else {
167 | withStyle(SpanStyle(fontSize = 24.sp)) {
168 | append(size)
169 | }
170 | }
171 | }
172 |
173 | is ViewState.Loading -> withStyle(SpanStyle(fontSize = 18.sp)) {
174 | append("Loading...")
175 | }
176 |
177 | is ViewState.Error -> withStyle(SpanStyle(fontSize = 18.sp)) {
178 | append("Error")
179 | }
180 | }
181 | }
182 | )
183 |
184 | @OptIn(ExperimentalMaterial3Api::class)
185 | @Composable
186 | fun HomeTopBar(modifier: Modifier = Modifier) {
187 | TopAppBar(
188 | modifier = modifier,
189 | title = {
190 | Title(
191 | modifier = Modifier,
192 | text = stringResource(R.string.app_name)
193 | )
194 | }
195 | )
196 | }
197 |
--------------------------------------------------------------------------------
/app/src/main/java/com/vishnu/whatsappcleaner/ui/PermissionScreen.kt:
--------------------------------------------------------------------------------
1 | /*
2 | * Copyright (C) 2025 Vishnu Sanal T
3 | *
4 | * This file is part of WhatsAppCleaner.
5 | *
6 | * Quotes Status Creator is free software: you can redistribute it and/or modify
7 | * it under the terms of the GNU General Public License as published by
8 | * the Free Software Foundation, either version 3 of the License, or
9 | * (at your option) any later version.
10 | *
11 | * This program is distributed in the hope that it will be useful,
12 | * but WITHOUT ANY WARRANTY; without even the implied warranty of
13 | * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
14 | * GNU General Public License for more details.
15 | *
16 | * You should have received a copy of the GNU General Public License
17 | * along with this program. If not, see .
18 | */
19 |
20 | package com.vishnu.whatsappcleaner.ui
21 |
22 | import androidx.compose.foundation.background
23 | import androidx.compose.foundation.layout.Column
24 | import androidx.compose.foundation.layout.Spacer
25 | import androidx.compose.foundation.layout.fillMaxHeight
26 | import androidx.compose.foundation.layout.fillMaxWidth
27 | import androidx.compose.foundation.layout.padding
28 | import androidx.compose.material3.MaterialTheme
29 | import androidx.compose.material3.OutlinedButton
30 | import androidx.compose.material3.Scaffold
31 | import androidx.compose.material3.Text
32 | import androidx.compose.runtime.Composable
33 | import androidx.compose.ui.Alignment
34 | import androidx.compose.ui.Modifier
35 | import androidx.compose.ui.layout.ContentScale
36 | import androidx.compose.ui.text.style.TextAlign
37 | import androidx.compose.ui.unit.dp
38 | import androidx.navigation.NavHostController
39 | import com.bumptech.glide.integration.compose.ExperimentalGlideComposeApi
40 | import com.bumptech.glide.integration.compose.GlideImage
41 | import com.bumptech.glide.integration.compose.placeholder
42 | import com.vishnu.whatsappcleaner.R
43 |
44 | @OptIn(ExperimentalGlideComposeApi::class)
45 | @Composable
46 | fun PermissionScreen(
47 | navController: NavHostController,
48 | permissionsGranted: Pair,
49 | requestPermission: () -> Unit,
50 | chooseDirectory: () -> Unit
51 | ) {
52 | Scaffold(
53 | Modifier.background(MaterialTheme.colorScheme.background)
54 | ) { contentPadding ->
55 | Column(Modifier.padding(contentPadding)) {
56 | Text(
57 | modifier = Modifier.padding(16.dp, 32.dp, 16.dp, 0.dp),
58 | text = "Welcome!",
59 | style = MaterialTheme.typography.titleLarge,
60 | textAlign = TextAlign.Start,
61 | color = MaterialTheme.colorScheme.onBackground,
62 | )
63 |
64 | Text(
65 | modifier = Modifier.padding(16.dp),
66 | text = "Thanks for installing WhatsAppCleaner. Please follow this quick setup to get started.",
67 | style = MaterialTheme.typography.bodyMedium,
68 | textAlign = TextAlign.Start,
69 | color = MaterialTheme.colorScheme.onBackground,
70 | )
71 |
72 | Text(
73 | modifier = Modifier
74 | .fillMaxWidth()
75 | .padding(horizontal = 16.dp, vertical = 4.dp),
76 | text = "1. Please grant the required permissions to WhatsAppCleaner",
77 | textAlign = TextAlign.Justify,
78 | style = MaterialTheme.typography.bodyMedium,
79 | )
80 |
81 | OutlinedButton(
82 | modifier = Modifier
83 | .fillMaxWidth()
84 | .padding(16.dp),
85 | enabled = !permissionsGranted.first,
86 | content = {
87 | Text(
88 | if (!permissionsGranted.first)
89 | "Grant storage permissions"
90 | else
91 | "Storage permission granted"
92 | )
93 | },
94 | onClick = {
95 | requestPermission()
96 | }
97 | )
98 |
99 | if (!permissionsGranted.first)
100 | Spacer(Modifier.weight(1f))
101 |
102 | Text(
103 | modifier = Modifier
104 | .fillMaxWidth()
105 | .padding(horizontal = 16.dp, vertical = 4.dp),
106 | text = "2. Please grant access to the WhatsApp directory as shown in the picture below",
107 | textAlign = TextAlign.Justify,
108 | style = MaterialTheme.typography.bodyMedium,
109 | )
110 |
111 | OutlinedButton(
112 | modifier = Modifier
113 | .fillMaxWidth()
114 | .padding(16.dp),
115 | enabled = permissionsGranted.first && !permissionsGranted.second,
116 | content = {
117 | Text(
118 | if (!permissionsGranted.first)
119 | "Grant storage permissions"
120 | else
121 | "Choose WhatsApp directory"
122 | )
123 | },
124 | onClick = {
125 | chooseDirectory()
126 | },
127 | )
128 |
129 | if (permissionsGranted.first && !permissionsGranted.second)
130 | GlideImage(
131 | modifier = Modifier
132 | .padding(16.dp)
133 | .align(Alignment.CenterHorizontally)
134 | .fillMaxWidth()
135 | .fillMaxHeight(),
136 | model = R.drawable.permission_hint,
137 | contentScale = ContentScale.Inside,
138 | loading = placeholder(R.drawable.image),
139 | failure = placeholder(R.drawable.error),
140 | contentDescription = "permission hint"
141 | )
142 | }
143 | }
144 | }
145 |
--------------------------------------------------------------------------------
/app/src/main/java/com/vishnu/whatsappcleaner/ui/theme/Color.kt:
--------------------------------------------------------------------------------
1 | /*
2 | * Copyright (C) 2025 Vishnu Sanal T
3 | *
4 | * This file is part of WhatsAppCleaner.
5 | *
6 | * Quotes Status Creator is free software: you can redistribute it and/or modify
7 | * it under the terms of the GNU General Public License as published by
8 | * the Free Software Foundation, either version 3 of the License, or
9 | * (at your option) any later version.
10 | *
11 | * This program is distributed in the hope that it will be useful,
12 | * but WITHOUT ANY WARRANTY; without even the implied warranty of
13 | * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
14 | * GNU General Public License for more details.
15 | *
16 | * You should have received a copy of the GNU General Public License
17 | * along with this program. If not, see .
18 | */
19 |
20 | package com.vishnu.whatsappcleaner.ui.theme
21 |
22 | import androidx.compose.ui.graphics.Color
23 |
24 | val primaryLight = Color(0xFF296A47)
25 | val onPrimaryLight = Color(0xFFFFFFFF)
26 | val primaryContainerLight = Color(0xFFAEF2C5)
27 | val onPrimaryContainerLight = Color(0xFF002110)
28 | val secondaryLight = Color(0xFF4E6355)
29 | val onSecondaryLight = Color(0xFFFFFFFF)
30 | val secondaryContainerLight = Color(0xFFD1E8D6)
31 | val onSecondaryContainerLight = Color(0xFF0C1F14)
32 | val tertiaryLight = Color(0xFF3B6471)
33 | val onTertiaryLight = Color(0xFFFFFFFF)
34 | val tertiaryContainerLight = Color(0xFFBFE9F8)
35 | val onTertiaryContainerLight = Color(0xFF001F27)
36 | val errorLight = Color(0xFFBA1A1A)
37 | val onErrorLight = Color(0xFFFFFFFF)
38 | val errorContainerLight = Color(0xFFFFDAD6)
39 | val onErrorContainerLight = Color(0xFF410002)
40 | val backgroundLight = Color(0xFFF6FBF4)
41 | val onBackgroundLight = Color(0xFF171D19)
42 | val surfaceLight = Color(0xFFF6FBF4)
43 | val onSurfaceLight = Color(0xFF171D19)
44 | val surfaceVariantLight = Color(0xFFDCE5DC)
45 | val onSurfaceVariantLight = Color(0xFF404942)
46 | val outlineLight = Color(0xFF717972)
47 | val outlineVariantLight = Color(0xFFC0C9C0)
48 | val scrimLight = Color(0xFF000000)
49 | val inverseSurfaceLight = Color(0xFF2C322D)
50 | val inverseOnSurfaceLight = Color(0xFFEDF2EB)
51 | val inversePrimaryLight = Color(0xFF93D5AA)
52 | val surfaceDimLight = Color(0xFFD6DBD5)
53 | val surfaceBrightLight = Color(0xFFF6FBF4)
54 | val surfaceContainerLowestLight = Color(0xFFFFFFFF)
55 | val surfaceContainerLowLight = Color(0xFFF0F5EE)
56 | val surfaceContainerLight = Color(0xFFEAEFE8)
57 | val surfaceContainerHighLight = Color(0xFFE4EAE3)
58 | val surfaceContainerHighestLight = Color(0xFFDFE4DD)
59 |
60 | val primaryDark = Color(0xFF93D5AA)
61 | val onPrimaryDark = Color(0xFF003920)
62 | val primaryContainerDark = Color(0xFF095131)
63 | val onPrimaryContainerDark = Color(0xFFAEF2C5)
64 | val secondaryDark = Color(0xFFB5CCBA)
65 | val onSecondaryDark = Color(0xFF213528)
66 | val secondaryContainerDark = Color(0xFF374B3E)
67 | val onSecondaryContainerDark = Color(0xFFD1E8D6)
68 | val tertiaryDark = Color(0xFFA3CDDB)
69 | val onTertiaryDark = Color(0xFF033541)
70 | val tertiaryContainerDark = Color(0xFF224C58)
71 | val onTertiaryContainerDark = Color(0xFFBFE9F8)
72 | val errorDark = Color(0xFFFFB4AB)
73 | val onErrorDark = Color(0xFF690005)
74 | val errorContainerDark = Color(0xFF93000A)
75 | val onErrorContainerDark = Color(0xFFFFDAD6)
76 | val backgroundDark = Color(0xFF0F1511)
77 | val onBackgroundDark = Color(0xFFDFE4DD)
78 | val surfaceDark = Color(0xFF0F1511)
79 | val onSurfaceDark = Color(0xFFDFE4DD)
80 | val surfaceVariantDark = Color(0xFF404942)
81 | val onSurfaceVariantDark = Color(0xFFC0C9C0)
82 | val outlineDark = Color(0xFF8A938B)
83 | val outlineVariantDark = Color(0xFF404942)
84 | val scrimDark = Color(0xFF000000)
85 | val inverseSurfaceDark = Color(0xFFDFE4DD)
86 | val inverseOnSurfaceDark = Color(0xFF2C322D)
87 | val inversePrimaryDark = Color(0xFF296A47)
88 | val surfaceDimDark = Color(0xFF0F1511)
89 | val surfaceBrightDark = Color(0xFF353B36)
90 | val surfaceContainerLowestDark = Color(0xFF0A0F0C)
91 | val surfaceContainerLowDark = Color(0xFF171D19)
92 | val surfaceContainerDark = Color(0xFF1B211D)
93 | val surfaceContainerHighDark = Color(0xFF262B27)
94 | val surfaceContainerHighestDark = Color(0xFF313632)
95 |
--------------------------------------------------------------------------------
/app/src/main/java/com/vishnu/whatsappcleaner/ui/theme/Theme.kt:
--------------------------------------------------------------------------------
1 | /*
2 | * Copyright (C) 2025 Vishnu Sanal T
3 | *
4 | * This file is part of WhatsAppCleaner.
5 | *
6 | * Quotes Status Creator is free software: you can redistribute it and/or modify
7 | * it under the terms of the GNU General Public License as published by
8 | * the Free Software Foundation, either version 3 of the License, or
9 | * (at your option) any later version.
10 | *
11 | * This program is distributed in the hope that it will be useful,
12 | * but WITHOUT ANY WARRANTY; without even the implied warranty of
13 | * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
14 | * GNU General Public License for more details.
15 | *
16 | * You should have received a copy of the GNU General Public License
17 | * along with this program. If not, see .
18 | */
19 |
20 | package com.vishnu.whatsappcleaner.ui.theme
21 |
22 | import android.graphics.Typeface
23 | import android.os.Build
24 | import android.os.Build.VERSION_CODES
25 | import androidx.compose.foundation.isSystemInDarkTheme
26 | import androidx.compose.material3.MaterialTheme
27 | import androidx.compose.material3.Typography
28 | import androidx.compose.material3.darkColorScheme
29 | import androidx.compose.material3.dynamicDarkColorScheme
30 | import androidx.compose.material3.dynamicLightColorScheme
31 | import androidx.compose.material3.lightColorScheme
32 | import androidx.compose.runtime.Composable
33 | import androidx.compose.ui.platform.LocalContext
34 | import androidx.compose.ui.text.font.Font
35 | import androidx.compose.ui.text.font.FontFamily
36 | import com.vishnu.whatsappcleaner.R
37 |
38 | private val LightColorScheme = lightColorScheme(
39 | primary = primaryLight,
40 | onPrimary = onPrimaryLight,
41 | primaryContainer = primaryContainerLight,
42 | onPrimaryContainer = onPrimaryContainerLight,
43 | secondary = secondaryLight,
44 | onSecondary = onSecondaryLight,
45 | secondaryContainer = secondaryContainerLight,
46 | onSecondaryContainer = onSecondaryContainerLight,
47 | tertiary = tertiaryLight,
48 | onTertiary = onTertiaryLight,
49 | tertiaryContainer = tertiaryContainerLight,
50 | onTertiaryContainer = onTertiaryContainerLight,
51 | error = errorLight,
52 | onError = onErrorLight,
53 | errorContainer = errorContainerLight,
54 | onErrorContainer = onErrorContainerLight,
55 | background = backgroundLight,
56 | onBackground = onBackgroundLight,
57 | surface = surfaceLight,
58 | onSurface = onSurfaceLight,
59 | surfaceVariant = surfaceVariantLight,
60 | onSurfaceVariant = onSurfaceVariantLight,
61 | outline = outlineLight,
62 | outlineVariant = outlineVariantLight,
63 | scrim = scrimLight,
64 | inverseSurface = inverseSurfaceLight,
65 | inverseOnSurface = inverseOnSurfaceLight,
66 | inversePrimary = inversePrimaryLight,
67 | surfaceDim = surfaceDimLight,
68 | surfaceBright = surfaceBrightLight,
69 | surfaceContainerLowest = surfaceContainerLowestLight,
70 | surfaceContainerLow = surfaceContainerLowLight,
71 | surfaceContainer = surfaceContainerLight,
72 | surfaceContainerHigh = surfaceContainerHighLight,
73 | surfaceContainerHighest = surfaceContainerHighestLight,
74 | )
75 |
76 | private val DarkColorScheme = darkColorScheme(
77 | primary = primaryDark,
78 | onPrimary = onPrimaryDark,
79 | primaryContainer = primaryContainerDark,
80 | onPrimaryContainer = onPrimaryContainerDark,
81 | secondary = secondaryDark,
82 | onSecondary = onSecondaryDark,
83 | secondaryContainer = secondaryContainerDark,
84 | onSecondaryContainer = onSecondaryContainerDark,
85 | tertiary = tertiaryDark,
86 | onTertiary = onTertiaryDark,
87 | tertiaryContainer = tertiaryContainerDark,
88 | onTertiaryContainer = onTertiaryContainerDark,
89 | error = errorDark,
90 | onError = onErrorDark,
91 | errorContainer = errorContainerDark,
92 | onErrorContainer = onErrorContainerDark,
93 | background = backgroundDark,
94 | onBackground = onBackgroundDark,
95 | surface = surfaceDark,
96 | onSurface = onSurfaceDark,
97 | surfaceVariant = surfaceVariantDark,
98 | onSurfaceVariant = onSurfaceVariantDark,
99 | outline = outlineDark,
100 | outlineVariant = outlineVariantDark,
101 | scrim = scrimDark,
102 | inverseSurface = inverseSurfaceDark,
103 | inverseOnSurface = inverseOnSurfaceDark,
104 | inversePrimary = inversePrimaryDark,
105 | surfaceDim = surfaceDimDark,
106 | surfaceBright = surfaceBrightDark,
107 | surfaceContainerLowest = surfaceContainerLowestDark,
108 | surfaceContainerLow = surfaceContainerLowDark,
109 | surfaceContainer = surfaceContainerDark,
110 | surfaceContainerHigh = surfaceContainerHighDark,
111 | surfaceContainerHighest = surfaceContainerHighestDark,
112 | )
113 |
114 | private val fontFamily = if (Build.VERSION.SDK_INT > VERSION_CODES.N)
115 | FontFamily(Font(R.font.geist))
116 | else
117 | FontFamily(Typeface.DEFAULT)
118 |
119 | @Composable
120 | fun WhatsAppCleanerTheme(
121 | darkTheme: Boolean = isSystemInDarkTheme(),
122 | // Dynamic color is available on Android 12+
123 | dynamicColor: Boolean = false,
124 | content: @Composable () -> Unit
125 | ) {
126 | val colorScheme = when {
127 | dynamicColor && Build.VERSION.SDK_INT >= VERSION_CODES.S -> {
128 | val context = LocalContext.current
129 | if (darkTheme) dynamicDarkColorScheme(context) else dynamicLightColorScheme(context)
130 | }
131 |
132 | darkTheme -> DarkColorScheme
133 | else -> LightColorScheme
134 | }
135 |
136 | val typography = Typography(
137 | displayLarge = Typography.displayLarge.copy(fontFamily = fontFamily),
138 | displayMedium = Typography.displayMedium.copy(fontFamily = fontFamily),
139 | displaySmall = Typography.displaySmall.copy(fontFamily = fontFamily),
140 |
141 | headlineLarge = Typography.headlineLarge.copy(fontFamily = fontFamily),
142 | headlineMedium = Typography.headlineMedium.copy(fontFamily = fontFamily),
143 | headlineSmall = Typography.headlineSmall.copy(fontFamily = fontFamily),
144 |
145 | titleLarge = Typography.titleLarge.copy(fontFamily = fontFamily),
146 | titleMedium = Typography.titleMedium.copy(fontFamily = fontFamily),
147 | titleSmall = Typography.titleSmall.copy(fontFamily = fontFamily),
148 |
149 | bodyLarge = Typography.bodyLarge.copy(fontFamily = fontFamily),
150 | bodyMedium = Typography.bodyMedium.copy(fontFamily = fontFamily),
151 | bodySmall = Typography.bodySmall.copy(fontFamily = fontFamily),
152 |
153 | labelLarge = Typography.labelLarge.copy(fontFamily = fontFamily),
154 | labelMedium = Typography.labelMedium.copy(fontFamily = fontFamily),
155 | labelSmall = Typography.labelSmall.copy(fontFamily = fontFamily)
156 | )
157 |
158 | MaterialTheme(
159 | colorScheme = colorScheme,
160 | typography = typography,
161 | content = content
162 | )
163 | }
164 |
--------------------------------------------------------------------------------
/app/src/main/java/com/vishnu/whatsappcleaner/ui/theme/Type.kt:
--------------------------------------------------------------------------------
1 | /*
2 | * Copyright (C) 2025 Vishnu Sanal T
3 | *
4 | * This file is part of WhatsAppCleaner.
5 | *
6 | * Quotes Status Creator is free software: you can redistribute it and/or modify
7 | * it under the terms of the GNU General Public License as published by
8 | * the Free Software Foundation, either version 3 of the License, or
9 | * (at your option) any later version.
10 | *
11 | * This program is distributed in the hope that it will be useful,
12 | * but WITHOUT ANY WARRANTY; without even the implied warranty of
13 | * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
14 | * GNU General Public License for more details.
15 | *
16 | * You should have received a copy of the GNU General Public License
17 | * along with this program. If not, see .
18 | */
19 |
20 | package com.vishnu.whatsappcleaner.ui.theme
21 |
22 | import androidx.compose.material3.Typography
23 | import androidx.compose.ui.text.TextStyle
24 | import androidx.compose.ui.text.font.FontFamily
25 | import androidx.compose.ui.text.font.FontWeight
26 | import androidx.compose.ui.unit.sp
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 | /* Other default text styles to override
38 | titleLarge = TextStyle(
39 | fontFamily = FontFamily.Default,
40 | fontWeight = FontWeight.Normal,
41 | fontSize = 22.sp,
42 | lineHeight = 28.sp,
43 | letterSpacing = 0.sp
44 | ),
45 | labelSmall = TextStyle(
46 | fontFamily = FontFamily.Default,
47 | fontWeight = FontWeight.Medium,
48 | fontSize = 11.sp,
49 | lineHeight = 16.sp,
50 | letterSpacing = 0.5.sp
51 | )
52 | */
53 | )
54 |
--------------------------------------------------------------------------------
/app/src/main/res/drawable/audio.xml:
--------------------------------------------------------------------------------
1 |
6 |
9 |
10 |
--------------------------------------------------------------------------------
/app/src/main/res/drawable/check_circle.xml:
--------------------------------------------------------------------------------
1 |
6 |
9 |
10 |
--------------------------------------------------------------------------------
/app/src/main/res/drawable/check_circle_filled.xml:
--------------------------------------------------------------------------------
1 |
7 |
10 |
11 |
--------------------------------------------------------------------------------
/app/src/main/res/drawable/clean.xml:
--------------------------------------------------------------------------------
1 |
7 |
8 |
11 |
12 |
13 |
--------------------------------------------------------------------------------
/app/src/main/res/drawable/document.xml:
--------------------------------------------------------------------------------
1 |
6 |
9 |
10 |
--------------------------------------------------------------------------------
/app/src/main/res/drawable/empty.xml:
--------------------------------------------------------------------------------
1 |
6 |
7 |
10 |
11 |
12 |
--------------------------------------------------------------------------------
/app/src/main/res/drawable/error.xml:
--------------------------------------------------------------------------------
1 |
6 |
7 |
10 |
11 |
12 |
--------------------------------------------------------------------------------
/app/src/main/res/drawable/gif.xml:
--------------------------------------------------------------------------------
1 |
6 |
9 |
10 |
--------------------------------------------------------------------------------
/app/src/main/res/drawable/ic_grid_view.xml:
--------------------------------------------------------------------------------
1 |
6 |
7 |
10 |
11 |
14 |
15 |
18 |
19 |
22 |
23 |
24 |
--------------------------------------------------------------------------------
/app/src/main/res/drawable/ic_select_all.xml:
--------------------------------------------------------------------------------
1 |
6 |
9 |
10 |
--------------------------------------------------------------------------------
/app/src/main/res/drawable/ic_sort.xml:
--------------------------------------------------------------------------------
1 |
7 |
10 |
11 |
--------------------------------------------------------------------------------
/app/src/main/res/drawable/ic_view_list.xml:
--------------------------------------------------------------------------------
1 |
7 |
8 |
11 |
12 |
13 |
--------------------------------------------------------------------------------
/app/src/main/res/drawable/image.xml:
--------------------------------------------------------------------------------
1 |
6 |
9 |
10 |
--------------------------------------------------------------------------------
/app/src/main/res/drawable/open_in.xml:
--------------------------------------------------------------------------------
1 |
8 |
11 |
12 |
13 |
--------------------------------------------------------------------------------
/app/src/main/res/drawable/permission_hint.webp:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/VishnuSanal/WhatsAppCleaner/e1ce32ff5d0a3160938c233fb815d8946be8027a/app/src/main/res/drawable/permission_hint.webp
--------------------------------------------------------------------------------
/app/src/main/res/drawable/profile.xml:
--------------------------------------------------------------------------------
1 |
6 |
9 |
10 |
--------------------------------------------------------------------------------
/app/src/main/res/drawable/recycling.xml:
--------------------------------------------------------------------------------
1 |
6 |
9 |
--------------------------------------------------------------------------------
/app/src/main/res/drawable/recycling_round.xml:
--------------------------------------------------------------------------------
1 |
6 |
9 |
--------------------------------------------------------------------------------
/app/src/main/res/drawable/status.xml:
--------------------------------------------------------------------------------
1 |
6 |
9 |
10 |
--------------------------------------------------------------------------------
/app/src/main/res/drawable/sticker.xml:
--------------------------------------------------------------------------------
1 |
6 |
9 |
10 |
--------------------------------------------------------------------------------
/app/src/main/res/drawable/unknown.xml:
--------------------------------------------------------------------------------
1 |
6 |
7 |
10 |
11 |
12 |
--------------------------------------------------------------------------------
/app/src/main/res/drawable/video.xml:
--------------------------------------------------------------------------------
1 |
6 |
9 |
10 |
--------------------------------------------------------------------------------
/app/src/main/res/drawable/video_notes.xml:
--------------------------------------------------------------------------------
1 |
6 |
9 |
10 |
11 |
--------------------------------------------------------------------------------
/app/src/main/res/drawable/voice.xml:
--------------------------------------------------------------------------------
1 |
6 |
9 |
10 |
--------------------------------------------------------------------------------
/app/src/main/res/drawable/wallpaper.xml:
--------------------------------------------------------------------------------
1 |
6 |
9 |
10 |
--------------------------------------------------------------------------------
/app/src/main/res/font/geist.ttf:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/VishnuSanal/WhatsAppCleaner/e1ce32ff5d0a3160938c233fb815d8946be8027a/app/src/main/res/font/geist.ttf
--------------------------------------------------------------------------------
/app/src/main/res/values/colors.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 | #FFBB86FC
4 | #FF6200EE
5 | #FF3700B3
6 | #FF03DAC5
7 | #FF018786
8 | #FF000000
9 | #FFFFFFFF
10 |
--------------------------------------------------------------------------------
/app/src/main/res/values/strings.xml:
--------------------------------------------------------------------------------
1 |
2 | WhatsAppCleaner
3 | ca-app-pub-9851547532747429~9247958807
4 | ca-app-pub-9851547532747429/4256631784
5 | ca-app-pub-9851547532747429/5956726261
6 |
--------------------------------------------------------------------------------
/app/src/main/res/values/themes.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------
/app/src/main/res/xml/provider_paths.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
6 |
--------------------------------------------------------------------------------
/app/src/test/java/com/vishnu/whatsappcleaner/ExampleUnitTest.kt:
--------------------------------------------------------------------------------
1 | /*
2 | * Copyright (C) 2025 Vishnu Sanal T
3 | *
4 | * This file is part of WhatsAppCleaner.
5 | *
6 | * Quotes Status Creator is free software: you can redistribute it and/or modify
7 | * it under the terms of the GNU General Public License as published by
8 | * the Free Software Foundation, either version 3 of the License, or
9 | * (at your option) any later version.
10 | *
11 | * This program is distributed in the hope that it will be useful,
12 | * but WITHOUT ANY WARRANTY; without even the implied warranty of
13 | * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
14 | * GNU General Public License for more details.
15 | *
16 | * You should have received a copy of the GNU General Public License
17 | * along with this program. If not, see .
18 | */
19 |
20 | package com.vishnu.whatsappcleaner
21 |
22 | import org.junit.Assert.assertEquals
23 | import org.junit.Test
24 |
25 | /**
26 | * Example local unit test, which will execute on the development machine (host).
27 | *
28 | * See [testing documentation](http://d.android.com/tools/testing).
29 | */
30 | class ExampleUnitTest {
31 | @Test
32 | fun addition_isCorrect() {
33 | assertEquals(4, 2 + 2)
34 | }
35 | }
36 |
--------------------------------------------------------------------------------
/build.gradle.kts:
--------------------------------------------------------------------------------
1 | // Top-level build file where you can add configuration options common to all sub-projects/modules.
2 | plugins {
3 | alias(libs.plugins.android.application) apply false
4 | alias(libs.plugins.jetbrains.kotlin.android) apply false
5 | alias(libs.plugins.compose.compiler) apply false
6 | id("com.diffplug.spotless") version "7.0.2"
7 | }
8 |
9 | configure {
10 | java {
11 | licenseHeaderFile("spotless-header")
12 | importOrder("android", "androidx", "com", "java", "")
13 | target("app/src/**/*.java")
14 | googleJavaFormat("1.25.2").aosp()
15 | removeUnusedImports()
16 | }
17 | kotlin {
18 | licenseHeaderFile("spotless-header")
19 | target("app/src/**/*.kt")
20 | ktlint("1.5.0")
21 | .editorConfigOverride(
22 | mapOf(
23 | "ktlint_function_naming_ignore_when_annotated_with" to "Composable",
24 | "ktlint_standard_trailing-comma-on-declaration-site" to "disabled",
25 | "ktlint_standard_trailing-comma-on-call-site" to "disabled",
26 | "ktlint_standard_if-else-bracing" to "disabled",
27 | "ktlint_standard_multiline-if-else" to "disabled",
28 | ),
29 | )
30 | trimTrailingWhitespace()
31 | }
32 | kotlinGradle {
33 | target("*.gradle.kts")
34 | ktlint("1.5.0")
35 | }
36 | }
37 |
--------------------------------------------------------------------------------
/fastlane/metadata/android/en-US/changelogs/8.txt:
--------------------------------------------------------------------------------
1 | - UI/UX updates
2 | - bug fixes
3 |
4 | Full Changelog -> https://github.com/VishnuSanal/WhatsAppCleaner/compare/v1.2.0...v1.1.0
--------------------------------------------------------------------------------
/fastlane/metadata/android/en-US/full_description.txt:
--------------------------------------------------------------------------------
1 | WhatsAppCleaner - Cleaner for WhatsApp
2 |
3 | Cleaner for WhatsApp - Clean Redundant Media and Files from Storage
4 |
5 | Did you know that WhatsApp is cluttering your phone with unnecessary media, old
6 | files, and duplicate images?! WhatsAppCleaner can help you quickly clean up
7 | your WhatsApp storage. With a simple tap, you can delete redundant photos,
8 | videos, documents, and more to reclaim your valuable storage space!
9 |
10 | *Key Features:*
11 |
12 | - Clean Redundant Media: Scan and clean large files and old media that are no
13 | longer needed.
14 | - File Cleanup: Remove unnecessary documents, voice notes, and backups
15 | cluttering your WhatsApp storage.
16 | - Fast & Easy: Declutter storage in just a few taps with an intuitive,
17 | user-friendly interface.
18 | - Preview Before Deleting: Always preview media before deletion to ensure
19 | you're removing only the files you don't need.
20 |
21 | Note: WhatsAppCleaner is a third-party application that helps in cleaning
22 | files generated by WhatsApp, and is not affiliated to or endorsed by WhatsApp itself.
--------------------------------------------------------------------------------
/fastlane/metadata/android/en-US/images/featureGraphic.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/VishnuSanal/WhatsAppCleaner/e1ce32ff5d0a3160938c233fb815d8946be8027a/fastlane/metadata/android/en-US/images/featureGraphic.png
--------------------------------------------------------------------------------
/fastlane/metadata/android/en-US/images/icon.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/VishnuSanal/WhatsAppCleaner/e1ce32ff5d0a3160938c233fb815d8946be8027a/fastlane/metadata/android/en-US/images/icon.png
--------------------------------------------------------------------------------
/fastlane/metadata/android/en-US/images/phoneScreenshots/1.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/VishnuSanal/WhatsAppCleaner/e1ce32ff5d0a3160938c233fb815d8946be8027a/fastlane/metadata/android/en-US/images/phoneScreenshots/1.png
--------------------------------------------------------------------------------
/fastlane/metadata/android/en-US/images/phoneScreenshots/2.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/VishnuSanal/WhatsAppCleaner/e1ce32ff5d0a3160938c233fb815d8946be8027a/fastlane/metadata/android/en-US/images/phoneScreenshots/2.png
--------------------------------------------------------------------------------
/fastlane/metadata/android/en-US/images/phoneScreenshots/3.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/VishnuSanal/WhatsAppCleaner/e1ce32ff5d0a3160938c233fb815d8946be8027a/fastlane/metadata/android/en-US/images/phoneScreenshots/3.png
--------------------------------------------------------------------------------
/fastlane/metadata/android/en-US/short_description.txt:
--------------------------------------------------------------------------------
1 | Cleaner for WhatsApp - Clean Redundant Media and Files from Storage
--------------------------------------------------------------------------------
/fastlane/metadata/android/en-US/title.txt:
--------------------------------------------------------------------------------
1 | WhatsAppCleaner
--------------------------------------------------------------------------------
/gradle.properties:
--------------------------------------------------------------------------------
1 | # Project-wide Gradle settings.
2 | # IDE (e.g. Android Studio) users:
3 | # Gradle settings configured through the IDE *will override*
4 | # any settings specified in this file.
5 | # For more details on how to configure your build environment visit
6 | # http://www.gradle.org/docs/current/userguide/build_environment.html
7 | # Specifies the JVM arguments used for the daemon process.
8 | # The setting is particularly useful for tweaking memory settings.
9 | org.gradle.jvmargs=-Xmx2048m -Dfile.encoding=UTF-8
10 | # When configured, Gradle will run in incubating parallel mode.
11 | # This option should only be used with decoupled projects. For more details, visit
12 | # https://developer.android.com/r/tools/gradle-multi-project-decoupled-projects
13 | # org.gradle.parallel=true
14 | # AndroidX package structure to make it clearer which packages are bundled with the
15 | # Android operating system, and which are packaged with your app's APK
16 | # https://developer.android.com/topic/libraries/support-library/androidx-rn
17 | android.useAndroidX=true
18 | # Kotlin code style for this project: "official" or "obsolete":
19 | kotlin.code.style=official
20 | # Enables namespacing of each library's R class so that its R class includes only the
21 | # resources declared in the library itself and none from the library's dependencies,
22 | # thereby reducing the size of the R class for that library
23 | android.nonTransitiveRClass=true
24 | android.defaults.buildfeatures.buildconfig=true
--------------------------------------------------------------------------------
/gradle/libs.versions.toml:
--------------------------------------------------------------------------------
1 | [versions]
2 | agp = "8.7.2"
3 | glideCompose = "1.0.0-beta01"
4 | composeShimmer = "1.3.0"
5 | datastoreCore = "1.1.1"
6 | datastorePreferences = "1.1.1"
7 | kotlin = "2.0.20"
8 | coreKtx = "1.15.0"
9 | junit = "4.13.2"
10 | junitVersion = "1.2.1"
11 | espressoCore = "3.6.1"
12 | lifecycleRuntimeKtx = "2.8.7"
13 | activityCompose = "1.9.3"
14 | composeBom = "2024.11.00"
15 | navigationCompose = "2.8.4"
16 | runtime = "1.7.5"
17 |
18 | [libraries]
19 | androidx-core-ktx = { group = "androidx.core", name = "core-ktx", version.ref = "coreKtx" }
20 | androidx-datastore-core = { module = "androidx.datastore:datastore-core", version.ref = "datastoreCore" }
21 | androidx-datastore-preferences = { module = "androidx.datastore:datastore-preferences", version.ref = "datastorePreferences" }
22 | androidx-navigation-compose = { module = "androidx.navigation:navigation-compose", version.ref = "navigationCompose" }
23 | androidx-runtime = { module = "androidx.compose.runtime:runtime", version.ref = "runtime" }
24 | androidx-runtime-livedata = { module = "androidx.compose.runtime:runtime-livedata", version.ref = "runtime" }
25 | glide-compose = { module = "com.github.bumptech.glide:compose", version.ref = "glideCompose" }
26 | compose-shimmer = { module = "com.valentinilk.shimmer:compose-shimmer", version.ref = "composeShimmer" }
27 | junit = { group = "junit", name = "junit", version.ref = "junit" }
28 | androidx-junit = { group = "androidx.test.ext", name = "junit", version.ref = "junitVersion" }
29 | androidx-espresso-core = { group = "androidx.test.espresso", name = "espresso-core", version.ref = "espressoCore" }
30 | androidx-lifecycle-runtime-ktx = { group = "androidx.lifecycle", name = "lifecycle-runtime-ktx", version.ref = "lifecycleRuntimeKtx" }
31 | androidx-activity-compose = { group = "androidx.activity", name = "activity-compose", version.ref = "activityCompose" }
32 | androidx-compose-bom = { group = "androidx.compose", name = "compose-bom", version.ref = "composeBom" }
33 | androidx-ui = { group = "androidx.compose.ui", name = "ui" }
34 | androidx-ui-graphics = { group = "androidx.compose.ui", name = "ui-graphics" }
35 | androidx-ui-tooling = { group = "androidx.compose.ui", name = "ui-tooling" }
36 | androidx-ui-tooling-preview = { group = "androidx.compose.ui", name = "ui-tooling-preview" }
37 | androidx-ui-test-manifest = { group = "androidx.compose.ui", name = "ui-test-manifest" }
38 | androidx-ui-test-junit4 = { group = "androidx.compose.ui", name = "ui-test-junit4" }
39 | androidx-material3 = { group = "androidx.compose.material3", name = "material3" }
40 |
41 | [plugins]
42 | android-application = { id = "com.android.application", version.ref = "agp" }
43 | jetbrains-kotlin-android = { id = "org.jetbrains.kotlin.android", version.ref = "kotlin" }
44 | compose-compiler = { id = "org.jetbrains.kotlin.plugin.compose", version.ref = "kotlin" }
45 |
46 |
--------------------------------------------------------------------------------
/gradle/wrapper/gradle-wrapper.jar:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/VishnuSanal/WhatsAppCleaner/e1ce32ff5d0a3160938c233fb815d8946be8027a/gradle/wrapper/gradle-wrapper.jar
--------------------------------------------------------------------------------
/gradle/wrapper/gradle-wrapper.properties:
--------------------------------------------------------------------------------
1 | distributionBase=GRADLE_USER_HOME
2 | distributionPath=wrapper/dists
3 | distributionSha256Sum=d725d707bfabd4dfdc958c624003b3c80accc03f7037b5122c4b1d0ef15cecab
4 | distributionUrl=https\://services.gradle.org/distributions/gradle-8.9-bin.zip
5 | networkTimeout=10000
6 | validateDistributionUrl=true
7 | zipStoreBase=GRADLE_USER_HOME
8 | zipStorePath=wrapper/dists
9 |
--------------------------------------------------------------------------------
/gradlew:
--------------------------------------------------------------------------------
1 | #!/bin/sh
2 |
3 | #
4 | # Copyright © 2015-2021 the original 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 | # SPDX-License-Identifier: Apache-2.0
19 | #
20 |
21 | ##############################################################################
22 | #
23 | # Gradle start up script for POSIX generated by Gradle.
24 | #
25 | # Important for running:
26 | #
27 | # (1) You need a POSIX-compliant shell to run this script. If your /bin/sh is
28 | # noncompliant, but you have some other compliant shell such as ksh or
29 | # bash, then to run this script, type that shell name before the whole
30 | # command line, like:
31 | #
32 | # ksh Gradle
33 | #
34 | # Busybox and similar reduced shells will NOT work, because this script
35 | # requires all of these POSIX shell features:
36 | # * functions;
37 | # * expansions «$var», «${var}», «${var:-default}», «${var+SET}»,
38 | # «${var#prefix}», «${var%suffix}», and «$( cmd )»;
39 | # * compound commands having a testable exit status, especially «case»;
40 | # * various built-in commands including «command», «set», and «ulimit».
41 | #
42 | # Important for patching:
43 | #
44 | # (2) This script targets any POSIX shell, so it avoids extensions provided
45 | # by Bash, Ksh, etc; in particular arrays are avoided.
46 | #
47 | # The "traditional" practice of packing multiple parameters into a
48 | # space-separated string is a well documented source of bugs and security
49 | # problems, so this is (mostly) avoided, by progressively accumulating
50 | # options in "$@", and eventually passing that to Java.
51 | #
52 | # Where the inherited environment variables (DEFAULT_JVM_OPTS, JAVA_OPTS,
53 | # and GRADLE_OPTS) rely on word-splitting, this is performed explicitly;
54 | # see the in-line comments for details.
55 | #
56 | # There are tweaks for specific operating systems such as AIX, CygWin,
57 | # Darwin, MinGW, and NonStop.
58 | #
59 | # (3) This script is generated from the Groovy template
60 | # https://github.com/gradle/gradle/blob/HEAD/platforms/jvm/plugins-application/src/main/resources/org/gradle/api/internal/plugins/unixStartScript.txt
61 | # within the Gradle project.
62 | #
63 | # You can find Gradle at https://github.com/gradle/gradle/.
64 | #
65 | ##############################################################################
66 |
67 | # Attempt to set APP_HOME
68 |
69 | # Resolve links: $0 may be a link
70 | app_path=$0
71 |
72 | # Need this for daisy-chained symlinks.
73 | while
74 | APP_HOME=${app_path%"${app_path##*/}"} # leaves a trailing /; empty if no leading path
75 | [ -h "$app_path" ]
76 | do
77 | ls=$( ls -ld "$app_path" )
78 | link=${ls#*' -> '}
79 | case $link in #(
80 | /*) app_path=$link ;; #(
81 | *) app_path=$APP_HOME$link ;;
82 | esac
83 | done
84 |
85 | # This is normally unused
86 | # shellcheck disable=SC2034
87 | APP_BASE_NAME=${0##*/}
88 | # Discard cd standard output in case $CDPATH is set (https://github.com/gradle/gradle/issues/25036)
89 | APP_HOME=$( cd -P "${APP_HOME:-./}" > /dev/null && printf '%s
90 | ' "$PWD" ) || exit
91 |
92 | # Use the maximum available, or set MAX_FD != -1 to use that value.
93 | MAX_FD=maximum
94 |
95 | warn () {
96 | echo "$*"
97 | } >&2
98 |
99 | die () {
100 | echo
101 | echo "$*"
102 | echo
103 | exit 1
104 | } >&2
105 |
106 | # OS specific support (must be 'true' or 'false').
107 | cygwin=false
108 | msys=false
109 | darwin=false
110 | nonstop=false
111 | case "$( uname )" in #(
112 | CYGWIN* ) cygwin=true ;; #(
113 | Darwin* ) darwin=true ;; #(
114 | MSYS* | MINGW* ) msys=true ;; #(
115 | NONSTOP* ) nonstop=true ;;
116 | esac
117 |
118 | CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar
119 |
120 |
121 | # Determine the Java command to use to start the JVM.
122 | if [ -n "$JAVA_HOME" ] ; then
123 | if [ -x "$JAVA_HOME/jre/sh/java" ] ; then
124 | # IBM's JDK on AIX uses strange locations for the executables
125 | JAVACMD=$JAVA_HOME/jre/sh/java
126 | else
127 | JAVACMD=$JAVA_HOME/bin/java
128 | fi
129 | if [ ! -x "$JAVACMD" ] ; then
130 | die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME
131 |
132 | Please set the JAVA_HOME variable in your environment to match the
133 | location of your Java installation."
134 | fi
135 | else
136 | JAVACMD=java
137 | if ! command -v java >/dev/null 2>&1
138 | then
139 | die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.
140 |
141 | Please set the JAVA_HOME variable in your environment to match the
142 | location of your Java installation."
143 | fi
144 | fi
145 |
146 | # Increase the maximum file descriptors if we can.
147 | if ! "$cygwin" && ! "$darwin" && ! "$nonstop" ; then
148 | case $MAX_FD in #(
149 | max*)
150 | # In POSIX sh, ulimit -H is undefined. That's why the result is checked to see if it worked.
151 | # shellcheck disable=SC2039,SC3045
152 | MAX_FD=$( ulimit -H -n ) ||
153 | warn "Could not query maximum file descriptor limit"
154 | esac
155 | case $MAX_FD in #(
156 | '' | soft) :;; #(
157 | *)
158 | # In POSIX sh, ulimit -n is undefined. That's why the result is checked to see if it worked.
159 | # shellcheck disable=SC2039,SC3045
160 | ulimit -n "$MAX_FD" ||
161 | warn "Could not set maximum file descriptor limit to $MAX_FD"
162 | esac
163 | fi
164 |
165 | # Collect all arguments for the java command, stacking in reverse order:
166 | # * args from the command line
167 | # * the main class name
168 | # * -classpath
169 | # * -D...appname settings
170 | # * --module-path (only if needed)
171 | # * DEFAULT_JVM_OPTS, JAVA_OPTS, and GRADLE_OPTS environment variables.
172 |
173 | # For Cygwin or MSYS, switch paths to Windows format before running java
174 | if "$cygwin" || "$msys" ; then
175 | APP_HOME=$( cygpath --path --mixed "$APP_HOME" )
176 | CLASSPATH=$( cygpath --path --mixed "$CLASSPATH" )
177 |
178 | JAVACMD=$( cygpath --unix "$JAVACMD" )
179 |
180 | # Now convert the arguments - kludge to limit ourselves to /bin/sh
181 | for arg do
182 | if
183 | case $arg in #(
184 | -*) false ;; # don't mess with options #(
185 | /?*) t=${arg#/} t=/${t%%/*} # looks like a POSIX filepath
186 | [ -e "$t" ] ;; #(
187 | *) false ;;
188 | esac
189 | then
190 | arg=$( cygpath --path --ignore --mixed "$arg" )
191 | fi
192 | # Roll the args list around exactly as many times as the number of
193 | # args, so each arg winds up back in the position where it started, but
194 | # possibly modified.
195 | #
196 | # NB: a `for` loop captures its iteration list before it begins, so
197 | # changing the positional parameters here affects neither the number of
198 | # iterations, nor the values presented in `arg`.
199 | shift # remove old arg
200 | set -- "$@" "$arg" # push replacement arg
201 | done
202 | fi
203 |
204 |
205 | # Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
206 | DEFAULT_JVM_OPTS='"-Xmx64m" "-Xms64m"'
207 |
208 | # Collect all arguments for the java command:
209 | # * DEFAULT_JVM_OPTS, JAVA_OPTS, JAVA_OPTS, and optsEnvironmentVar are not allowed to contain shell fragments,
210 | # and any embedded shellness will be escaped.
211 | # * For example: A user cannot expect ${Hostname} to be expanded, as it is an environment variable and will be
212 | # treated as '${Hostname}' itself on the command line.
213 |
214 | set -- \
215 | "-Dorg.gradle.appname=$APP_BASE_NAME" \
216 | -classpath "$CLASSPATH" \
217 | org.gradle.wrapper.GradleWrapperMain \
218 | "$@"
219 |
220 | # Stop when "xargs" is not available.
221 | if ! command -v xargs >/dev/null 2>&1
222 | then
223 | die "xargs is not available"
224 | fi
225 |
226 | # Use "xargs" to parse quoted args.
227 | #
228 | # With -n1 it outputs one arg per line, with the quotes and backslashes removed.
229 | #
230 | # In Bash we could simply go:
231 | #
232 | # readarray ARGS < <( xargs -n1 <<<"$var" ) &&
233 | # set -- "${ARGS[@]}" "$@"
234 | #
235 | # but POSIX shell has neither arrays nor command substitution, so instead we
236 | # post-process each arg (as a line of input to sed) to backslash-escape any
237 | # character that might be a shell metacharacter, then use eval to reverse
238 | # that process (while maintaining the separation between arguments), and wrap
239 | # the whole thing up as a single "set" statement.
240 | #
241 | # This will of course break if any of these variables contains a newline or
242 | # an unmatched quote.
243 | #
244 |
245 | eval "set -- $(
246 | printf '%s\n' "$DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS" |
247 | xargs -n1 |
248 | sed ' s~[^-[:alnum:]+,./:=@_]~\\&~g; ' |
249 | tr '\n' ' '
250 | )" '"$@"'
251 |
252 | exec "$JAVACMD" "$@"
253 |
--------------------------------------------------------------------------------
/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 | @rem SPDX-License-Identifier: Apache-2.0
17 | @rem
18 |
19 | @if "%DEBUG%"=="" @echo off
20 | @rem ##########################################################################
21 | @rem
22 | @rem Gradle startup script for Windows
23 | @rem
24 | @rem ##########################################################################
25 |
26 | @rem Set local scope for the variables with windows NT shell
27 | if "%OS%"=="Windows_NT" setlocal
28 |
29 | set DIRNAME=%~dp0
30 | if "%DIRNAME%"=="" set DIRNAME=.
31 | @rem This is normally unused
32 | set APP_BASE_NAME=%~n0
33 | set APP_HOME=%DIRNAME%
34 |
35 | @rem Resolve any "." and ".." in APP_HOME to make it shorter.
36 | for %%i in ("%APP_HOME%") do set APP_HOME=%%~fi
37 |
38 | @rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
39 | set DEFAULT_JVM_OPTS="-Xmx64m" "-Xms64m"
40 |
41 | @rem Find java.exe
42 | if defined JAVA_HOME goto findJavaFromJavaHome
43 |
44 | set JAVA_EXE=java.exe
45 | %JAVA_EXE% -version >NUL 2>&1
46 | if %ERRORLEVEL% equ 0 goto execute
47 |
48 | echo. 1>&2
49 | echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. 1>&2
50 | echo. 1>&2
51 | echo Please set the JAVA_HOME variable in your environment to match the 1>&2
52 | echo location of your Java installation. 1>&2
53 |
54 | goto fail
55 |
56 | :findJavaFromJavaHome
57 | set JAVA_HOME=%JAVA_HOME:"=%
58 | set JAVA_EXE=%JAVA_HOME%/bin/java.exe
59 |
60 | if exist "%JAVA_EXE%" goto execute
61 |
62 | echo. 1>&2
63 | echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% 1>&2
64 | echo. 1>&2
65 | echo Please set the JAVA_HOME variable in your environment to match the 1>&2
66 | echo location of your Java installation. 1>&2
67 |
68 | goto fail
69 |
70 | :execute
71 | @rem Setup the command line
72 |
73 | set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar
74 |
75 |
76 | @rem Execute Gradle
77 | "%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %*
78 |
79 | :end
80 | @rem End local scope for the variables with windows NT shell
81 | if %ERRORLEVEL% equ 0 goto mainEnd
82 |
83 | :fail
84 | rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of
85 | rem the _cmd.exe /c_ return code!
86 | set EXIT_CODE=%ERRORLEVEL%
87 | if %EXIT_CODE% equ 0 set EXIT_CODE=1
88 | if not ""=="%GRADLE_EXIT_CONSOLE%" exit %EXIT_CODE%
89 | exit /b %EXIT_CODE%
90 |
91 | :mainEnd
92 | if "%OS%"=="Windows_NT" endlocal
93 |
94 | :omega
95 |
--------------------------------------------------------------------------------
/settings.gradle.kts:
--------------------------------------------------------------------------------
1 | pluginManagement {
2 | repositories {
3 | google {
4 | content {
5 | includeGroupByRegex("com\\.android.*")
6 | includeGroupByRegex("com\\.google.*")
7 | includeGroupByRegex("androidx.*")
8 | }
9 | }
10 | mavenCentral()
11 | gradlePluginPortal()
12 | }
13 | }
14 | dependencyResolutionManagement {
15 | repositoriesMode.set(RepositoriesMode.FAIL_ON_PROJECT_REPOS)
16 | repositories {
17 | google()
18 | mavenCentral()
19 | }
20 | }
21 |
22 | rootProject.name = "WhatsAppCleaner"
23 | include(":app")
24 |
--------------------------------------------------------------------------------
/spotless-header:
--------------------------------------------------------------------------------
1 | /*
2 | * Copyright (C) 2025 Vishnu Sanal T
3 | *
4 | * This file is part of WhatsAppCleaner.
5 | *
6 | * Quotes Status Creator is free software: you can redistribute it and/or modify
7 | * it under the terms of the GNU General Public License as published by
8 | * the Free Software Foundation, either version 3 of the License, or
9 | * (at your option) any later version.
10 | *
11 | * This program is distributed in the hope that it will be useful,
12 | * but WITHOUT ANY WARRANTY; without even the implied warranty of
13 | * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
14 | * GNU General Public License for more details.
15 | *
16 | * You should have received a copy of the GNU General Public License
17 | * along with this program. If not, see .
18 | */
19 |
20 |
--------------------------------------------------------------------------------