diff --git a/.idea/shelf/Uncommitted_changes_before_Update_at_10_1_2024_11_20_AM_[Changes]/shelved.patch b/.idea/shelf/Uncommitted_changes_before_Update_at_10_1_2024_11_20_AM_[Changes]/shelved.patch deleted file mode 100644 index 07986b1..0000000 --- a/.idea/shelf/Uncommitted_changes_before_Update_at_10_1_2024_11_20_AM_[Changes]/shelved.patch +++ /dev/null @@ -1,193 +0,0 @@ -Index: app/src/main/res/values/strings.xml -IDEA additional info: -Subsystem: com.intellij.openapi.diff.impl.patch.BaseRevisionTextPatchEP -<+>\r\n The last Android Cleaner you will ever need for every day use!\r\n A new update available!\r\n A new version of the app is available. Click to update!\r\n It\\'s been a while!\r\n Let\\'s free up some space! We are happy you are back!\r\n\r\n Welcome\r\n Browse the Terms of Service and Privacy Policy\r\n Read and agree to our Terms of Service and Privacy Policy to continue\r\n Agree\r\n\r\n Open navigation drawer\r\n\r\n Home\r\n %1$s/%2$s GB\\nUsed\r\n Analyze\r\n Clean\r\n Status: Selected %1$d files\r\n Status: No files selected\r\n Select All\r\n Rescan?\r\n Are you sure you want to scan again?\r\n File not found.\r\n No files found\r\n Activity not found.\r\n A security error occurred. Please try again later.\r\n An input/output error occurred. Please check your network connection or storage and try again.\r\n Invalid input or argument provided. Please check the information and try again.\r\n An unknown error occurred.\r\n\r\n App Manager\r\n Installed apps\r\n System apps\r\n App install files (APKs)\r\n Uninstall\r\n Share\r\n App info\r\n Installed\"\r\n Install\"\r\n Share APK\r\n No apps installed\r\n No APK found\r\n Error loading apps.\r\n\r\n Memory Manager\r\n Storage Information\r\n Used:\r\n Free:\r\n Total:\r\n RAM Information\r\n Used RAM:\r\n Free RAM:\r\n Total RAM:\r\n Categories\r\n System\r\n Music\r\n Images\r\n Documents\r\n Downloads\r\n Other Files\r\n\r\n Image optimizer\r\n Select the image you want to optimize\r\n Choose image\r\n Optimize image\r\n Image saved to: \r\n Quick Compress\r\n Select image\r\n Low\r\n Medium\r\n High\r\n File Size (KB)\r\n Enter a value\r\n Manual\r\n Width (px)\r\n Height (px)\r\n Quality\r\n\r\n Settings\r\n\r\n Display\r\n Personalize your app\\'s look and feel\r\n\r\n Appearance\r\n Dark theme\r\n Will turn on automatically by the system\r\n Will never turn on automatically\r\n AMOLED mode\r\n Follow System Mode\r\n Light Mode\r\n Dark Mode\r\n Dark theme uses a deep background to help keep your battery alive longer\r\n\r\n Dynamic colors\r\n Apply colors from wallpapers to the app theme\r\n\r\n App behavior\r\n Bounce click\r\n Applies a subtle bounce animation to all clickable elements when clicked\r\n\r\n Navigation\r\n Startup page\r\n Choose the screen to be displayed when the app starts.\r\n Choose your preferred startup page:\r\n Configures the initial screen presented to users upon launching the appli0cation. This setting influences the user\\'s first interaction with the app and can be tailored to specific user segments or product goals.\r\n\r\n Language\r\n Changes the language used in the app\r\n Select your preferred language\r\n Personalize your experience with your preferred language. Any language changes you make take effect right away, ensuring a seamless experience in your chosen language\r\n\r\n Cleaning\r\n Customize your cleaning experience\r\n Filters\r\n Generic filter\r\n Delete temp, log, logs, & other useless files & folders\r\n Delete empty folders\r\n Delete archives\r\n Delete archives from your device\r\n Delete corpse files\r\n Delete files left over from uninstalled/removed apps\r\n Delete APK files\r\n Delete installer files (.apk) files for Android\r\n Media\r\n Delete audio\r\n Delete the audio files from the device\r\n Delete video\r\n Delete the video files from the device\r\n Delete images\r\n Delete the image files from the device\r\n Delete invalid media\r\n Delete corrupted images\r\n Scanner\r\n Clipboard clean\r\n\r\n Notifications\r\n Manage app notifications\r\n\r\n App Usage Notifications\r\n Update Notifications\r\n\r\n Advanced\r\n Explore more advanced settings\r\n Error reporting\r\n Bug report\r\n Send bug reports and feature requests to the app\\'s GitHub repository issues page\r\n\r\n Security & privacy\r\n Manage your privacy settings\r\n Privacy\r\n Privacy Policy\r\n View the policy that governs how we handle your data\r\n Terms of Service\r\n Review the terms you agree to when using our service\r\n Code of Conduct\r\n Understand the rules and guidelines for behavior within our service\r\n\r\n Permissions\r\n Manage the permissions granted to our service\r\n Normal\r\n Ad id [AD_ID]\r\n Allows the app to retrieve and use the advertising identifier associated with the user\\'s device, providing personalized ads, measuring ad effectiveness, and showing ads on Android 13 or later devices.\r\n Internet [INTERNET]\r\n Allows the app to establish an internet connection to send error reports or check for updates.\r\n Post notifications [POST_NOTIFICATIONS]\r\n Allows the app to display notifications on the devices with Android 13 or later.\r\n Runtime\r\n Access network state [ACCESS_NETWORK_STATE]\r\n Allows the app to check network connectivity and retrieve information about Wi-Fi, including enabled status and connected Wi-Fi device names.\r\n Access notification policy [ACCESS_NOTIFICATION_POLICY]\r\n Allows the app to access and modify the device\\'s notification policy, controlling how and when notifications are displayed to the user and providing custom notification management features.\r\n Billing [BILLING]\r\n Allows the app to use the Google Play Billing Library to handle in-app purchases and donations\r\n Check license [CHECK_LICENSE]\r\n Allows the app to verify its compliance with the license agreement and enforce licensing terms to protect intellectual property.\r\n Foreground service [FOREGROUND_SERVICE]\r\n Allows the app to create and use services that run in the foreground, giving them priority over other background processes and improving performance and reliability.\r\n Request delete packages [REQUEST_DELETE_PACKAGES]\r\n Allows the app to request the deletion of packages. This can be used to uninstall other apps from the device\r\n Storage\r\n Access media location [ACCESS_MEDIA_LOCATION]\r\n Allows the app to access the device\\'s media files location\r\n Action open document tree [ACTION_OPEN_DOCUMENT_TREE]\r\n Allows the app to access to a tree of documents, including the ability to read and write files in the tree\r\n Manage external storage [MANAGE_EXTERNAL_STORAGE]\r\n Allows the app to perform actions like deleting and renaming files on the device\\'s storage, including both internal and external storage\r\n Package usage stats [PACKAGE_USAGE_STATS]\r\n Allows the app to access information about the user\\'s app usage, including which apps have been used, how long they have been used, and how frequently they have been used\r\n Query all packages [QUERY_ALL_PACKAGES]\r\n Allows the app to query for information about all installed packages on the device. The app uses this information to determine which files to keep or delete\r\n Read external storage [READ_EXTERNAL_STORAGE]\r\n Allows the app to read files from the device\\'s storage, including both internal and external storage\r\n Read media audio [READ_MEDIA_AUDIO]\r\n Allows the app to read audio files from the device\\'s storage\r\n Read media images [READ_MEDIA_IMAGES]\r\n Allows the app to read image files from the device\\'s storage\r\n Read media video [READ_MEDIA_VIDEO]\r\n Allows the app to read video files from the device\\'s storage\r\n Write external storage [WRITE_EXTERNAL_STORAGE]\r\n Allows the app to write files to the device\\'s storage, including both internal and external storage\r\n\r\n Ads\r\n Control how we use your information to show you relevant ads\r\n Display ads\r\n Personalized ads\r\n Choose how we personalize ads for you based on your interests\r\n See ads that are relevant to you. Manage the information used to show personalized ads based on your app activity. You can always turn off personalization here.\\n\r\n\r\n Usage and diagnostics\r\n Share data to help improve Cleaner for Android\r\n Help improve your experience by automatically sending diagnostic, device, and app usage data to us. This will help improve app performance, stability, and other enhancements. Some aggregate data will also help our apps.\\n\\nThis is general information about your device and how you use our apps (such as battery level, system and app activity, and errors). The data will be used to improve our apps\\n\\nTurning off this feature doesn\\'t affect your device\\'s ability to send the information needed for essential services such as app updates and security.\\n\r\n\r\n Legal\r\n Legal notices\r\n View legal information about our service\r\n License\r\n\r\n About\r\n Learn more about Cleaner\r\n\r\n Application build version\r\n License details for open source software\r\n Device Info\r\n App Build: Release\\n%1$s\\n%2$s\\n%3$s\\n%4$s\\n%5$s\r\n Manufacturer:\r\n Device Model:\r\n Android Version:\r\n API Level:\r\n Device info copied to clipboard\r\n\r\n Help & feedback\r\n Help\r\n FAQ\r\n View in Google Play Store\r\n Version info\r\n Version %1$s\r\n Beta program\r\n What is Cleaner?\r\n Cleaner is a free and open-source Android app that helps you optimize your device\\'s performance and free up storage space. It can scan your device for unnecessary files and delete them, as well clear your cache to improve your device\\'s speed\r\n Is Cleaner safe to use?\r\n Yes, Cleaner is safe to use. It only removes files that are not essential to the operation of your device or apps, and it does not delete any of your personal data or important system files\r\n How do I use Cleaner?\r\n To use Cleaner, simply download and install the app from the Google Play Store or a reputable third-party app store. Then, open the app and follow the prompts to scan your device and delete unnecessary files. You can also access the app\\'s settings to customize the types of files that Cleaner looks for and deletes\r\n Can Cleaner delete app cache?\r\n Yes, Cleaner can delete app cache. However, it is important to note that deleting cache may cause the app to take longer to load or perform certain tasks the next time you use it\r\n Does Cleaner require root access?\r\n No, Cleaner does not require root access to work. You can use it on any Android device without rooting your device\r\n Is Cleaner available for iOS?\r\n No, Cleaner is currently only available for Android devices\r\n Does Cleaner improve battery life?\r\n Using Cleaner to clear your cache may help improve your device\\'s battery life by reducing the amount of power and resources that are used by unnecessary processes. However, the extent to which Cleaner can improve your battery life will depend on various factors such as your device\\'s make and model, the apps you use, and your usage habits\r\n How often should I use Cleaner?\r\n There is no set rule for how often you should use Cleaner, as it will depend on your device\\'s usage and the amount of unnecessary files that accumulate over time. Some users may find it helpful to run Cleaner weekly or monthly, while others may only need to use it occasionally\r\n Is Cleaner compatible with all Android devices?\r\n Cleaner should be compatible with most Android devices that meet the app\\'s minimum system requirements. However, it is always a good idea to check the app\\'s page on the Google Play Store or the app store you are using to confirm compatibility with your specific device. If you encounter any issues while using Cleaner, you can try troubleshooting the problem or contacting the app\\'s support team for assistance\r\n Feedback\r\n\r\n Updates\r\n\r\n Support Us\r\n Paid Support\r\n No matter how much you donate, you will help us keep our app running and improve our features. We appreciate your generosity and kindness!\r\n Non-Paid Support\r\n Web Ad\r\n\r\n The app has been successfully updated\r\n The update process encountered an issue\r\n Try again\r\n Check out this amazing app that I\\'m using! It has some really cool features that you might find interesting. You can download it from the Play Store at: %1$s\r\n Learn more\r\n\r\n Close?\r\n Require restart!\r\n Network error occurred while checking for updates\r\n An error occurred while checking for updates\r\n Are you sure you want to exit?\r\n To take effect please restart the app!\r\n\r\n No image provided\r\n Please select a valid file size\r\n -Subsystem: com.intellij.openapi.diff.impl.patch.CharsetEP -<+>UTF-8 -=================================================================== -diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml ---- a/app/src/main/res/values/strings.xml (revision 57c1e1bfa29ea2909c3b8e44202c38eb1a5c37c7) -+++ b/app/src/main/res/values/strings.xml (date 1727770683044) -@@ -16,6 +16,11 @@ - %1$s/%2$s GB\nUsed - Analyze - Clean -+ APKs -+ Archives -+ Videos -+ Audios -+ Other - Status: Selected %1$d files - Status: No files selected - Select All -Index: gradle/libs.versions.toml -IDEA additional info: -Subsystem: com.intellij.openapi.diff.impl.patch.BaseRevisionTextPatchEP -<+>[versions]\r\nagp = \"8.6.1\"\r\nappcompat = \"1.7.0\"\r\nappUpdateKtx = \"2.1.0\"\r\nbilling = \"7.1.0\"\r\ncoilCompose = \"2.7.0\"\r\ncoilVideo = \"2.7.0\"\r\ncomposeBom = \"2024.09.02\"\r\ncompressor = \"3.0.1\"\r\nconstraintlayoutCompose = \"1.0.1\"\r\ncoreSplashscreen = \"1.0.1\"\r\ndatastoreCore = \"1.1.1\"\r\nfirebaseBom = \"33.3.0\"\r\nlifecycle = \"2.8.6\"\r\nvolley = \"1.2.1\"\r\nkotlin = \"2.0.10\"\r\ncoreKtx = \"1.13.1\"\r\njunit = \"4.13.2\"\r\njunitVersion = \"1.2.1\"\r\nespressoCore = \"3.6.1\"\r\nkotlinxCoroutinesAndroid = \"1.9.0\"\r\nactivityCompose = \"1.9.2\"\r\ncomposeUi = \"1.7.2\"\r\ncomposeMaterial3 = \"1.3.0\"\r\ngoogle-services = \"4.4.2\"\r\ngoogle-oss-services = \"0.10.6\"\r\ngoogle-firebase-crashlytics = \"3.0.2\"\r\nmaterial = \"1.12.0\"\r\nmultidex = \"2.0.1\"\r\nnavigationCompose = \"2.8.1\"\r\nplayServicesAds = \"23.3.0\"\r\nplayServicesOssLicenses = \"17.1.0\"\r\nreviewKtx = \"2.0.1\"\r\nworkRuntimeKtx = \"2.9.1\"\r\n\r\n\r\n[libraries]\r\nandroidx-appcompat = { module = \"androidx.appcompat:appcompat\", version.ref = \"appcompat\" }\r\nandroidx-compose-bom = { module = \"androidx.compose:compose-bom\", version.ref = \"composeBom\" }\r\nandroidx-constraintlayout-compose = { module = \"androidx.constraintlayout:constraintlayout-compose\", version.ref = \"constraintlayoutCompose\" }\r\nandroidx-core-ktx = { group = \"androidx.core\", name = \"core-ktx\", version.ref = \"coreKtx\" }\r\nandroidx-core-splashscreen = { module = \"androidx.core:core-splashscreen\", version.ref = \"coreSplashscreen\" }\r\nandroidx-datastore-preferences = { module = \"androidx.datastore:datastore-core\", version.ref = \"datastoreCore\" }\r\nandroidx-foundation = { module = \"androidx.compose.foundation:foundation\" }\r\nandroidx-compose-runtime = { module = \"androidx.compose.runtime:runtime\" }\r\nandroidx-lifecycle-common-java8 = { module = \"androidx.lifecycle:lifecycle-common-java8\", version.ref = \"lifecycle\" }\r\nandroidx-lifecycle-livedata-ktx = { module = \"androidx.lifecycle:lifecycle-livedata-ktx\", version.ref = \"lifecycle\" }\r\nandroidx-lifecycle-process = { module = \"androidx.lifecycle:lifecycle-process\", version.ref = \"lifecycle\" }\r\nandroidx-lifecycle-runtime-compose = { module = \"androidx.lifecycle:lifecycle-runtime-compose\", version.ref = \"lifecycle\" }\r\nandroidx-lifecycle-viewmodel-compose = { module = \"androidx.lifecycle:lifecycle-viewmodel-compose\", version.ref = \"lifecycle\" }\r\nandroidx-lifecycle-viewmodel-ktx = { module = \"androidx.lifecycle:lifecycle-viewmodel-ktx\", version.ref = \"lifecycle\" }\r\nandroidx-multidex = { module = \"androidx.multidex:multidex\", version.ref = \"multidex\" }\r\nandroidx-material-icons-extended = { module = \"androidx.compose.material:material-icons-extended\" }\r\nandroidx-runtime-livedata = { module = \"androidx.compose.runtime:runtime-livedata\" }\r\nandroidx-runtime-rxjava2 = { module = \"androidx.compose.runtime:runtime-rxjava2\" }\r\nandroidx-work-runtime-ktx = { module = \"androidx.work:work-runtime-ktx\", version.ref = \"workRuntimeKtx\" }\r\napp-update-ktx = { module = \"com.google.android.play:app-update-ktx\", version.ref = \"appUpdateKtx\" }\r\nbilling = { module = \"com.android.billingclient:billing\", version.ref = \"billing\" }\r\ncoil-compose = { module = \"io.coil-kt:coil-compose\", version.ref = \"coilCompose\" }\r\ncoil-video = { module = \"io.coil-kt:coil-video\", version.ref = \"coilVideo\" }\r\ncompressor = { module = \"id.zelory:compressor\", version.ref = \"compressor\" }\r\ndatastore-preferences = { module = \"androidx.datastore:datastore-preferences\", version.ref = \"datastoreCore\" }\r\nfirebase-analytics-ktx = { module = \"com.google.firebase:firebase-analytics-ktx\" }\r\nfirebase-bom = { module = \"com.google.firebase:firebase-bom\", version.ref = \"firebaseBom\" }\r\nfirebase-crashlytics-ktx = { module = \"com.google.firebase:firebase-crashlytics-ktx\" }\r\nfirebase-perf = { module = \"com.google.firebase:firebase-perf\" }\r\njunit = { group = \"junit\", name = \"junit\", version.ref = \"junit\" }\r\nandroidx-junit = { group = \"androidx.test.ext\", name = \"junit\", version.ref = \"junitVersion\" }\r\nandroidx-espresso-core = { group = \"androidx.test.espresso\", name = \"espresso-core\", version.ref = \"espressoCore\" }\r\nandroidx-lifecycle-runtime-ktx = { group = \"androidx.lifecycle\", name = \"lifecycle-runtime-ktx\", version.ref = \"lifecycle\" }\r\nandroidx-activity-compose = { group = \"androidx.activity\", name = \"activity-compose\", version.ref = \"activityCompose\" }\r\nandroidx-ui = { group = \"androidx.compose.ui\", name = \"ui\", version.ref = \"composeUi\" }\r\nandroidx-ui-graphics = { group = \"androidx.compose.ui\", name = \"ui-graphics\" }\r\nandroidx-ui-tooling = { group = \"androidx.compose.ui\", name = \"ui-tooling\" }\r\nandroidx-ui-tooling-preview = { group = \"androidx.compose.ui\", name = \"ui-tooling-preview\" }\r\nandroidx-ui-test-manifest = { group = \"androidx.compose.ui\", name = \"ui-test-manifest\", version.ref = \"composeUi\" }\r\nui-test-junit4 = { module = \"androidx.compose.ui:ui-test-junit4\", version.ref = \"composeUi\" }\r\nandroidx-material3 = { group = \"androidx.compose.material3\", name = \"material3\", version.ref = \"composeMaterial3\" }\r\nandroidx-navigation-compose = { group = \"androidx.navigation\", name = \"navigation-compose\", version.ref = \"navigationCompose\" }\r\nkotlinx-coroutines-android = { module = \"org.jetbrains.kotlinx:kotlinx-coroutines-android\", version.ref = \"kotlinxCoroutinesAndroid\" }\r\nmaterial = { module = \"com.google.android.material:material\", version.ref = \"material\" }\r\nplay-services-ads = { module = \"com.google.android.gms:play-services-ads\", version.ref = \"playServicesAds\" }\r\nplay-services-oss-licenses = { module = \"com.google.android.gms:play-services-oss-licenses\", version.ref = \"playServicesOssLicenses\" }\r\nreview-ktx = { module = \"com.google.android.play:review-ktx\", version.ref = \"reviewKtx\" }\r\nvolley = { module = \"com.android.volley:volley\", version.ref = \"volley\" }\r\n\r\n[plugins]\r\nandroidApplication = { id = \"com.android.application\", version.ref = \"agp\" }\r\nandroidLibrary = { id = \"com.android.library\", version.ref = \"agp\" }\r\njetbrainsKotlinAndroid = { id = \"org.jetbrains.kotlin.android\", version.ref = \"kotlin\" }\r\ngooglePlayServices = { id = \"com.google.gms.google-services\", version.ref = \"google-services\" }\r\ngoogleOssServices = { id = \"com.google.android.gms.oss-licenses-plugin\", version.ref = \"google-oss-services\" }\r\ngoogleFirebase = { id = \"com.google.firebase.crashlytics\", version.ref = \"google-firebase-crashlytics\" }\r\ncompose-compiler = { id = \"org.jetbrains.kotlin.plugin.compose\", version.ref = \"kotlin\" } -Subsystem: com.intellij.openapi.diff.impl.patch.CharsetEP -<+>UTF-8 -=================================================================== -diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml ---- a/gradle/libs.versions.toml (revision 57c1e1bfa29ea2909c3b8e44202c38eb1a5c37c7) -+++ b/gradle/libs.versions.toml (date 1727770813792) -@@ -28,12 +28,11 @@ - material = "1.12.0" - multidex = "2.0.1" - navigationCompose = "2.8.1" --playServicesAds = "23.3.0" -+playServicesAds = "23.4.0" - playServicesOssLicenses = "17.1.0" - reviewKtx = "2.0.1" - workRuntimeKtx = "2.9.1" - -- - [libraries] - androidx-appcompat = { module = "androidx.appcompat:appcompat", version.ref = "appcompat" } - androidx-compose-bom = { module = "androidx.compose:compose-bom", version.ref = "composeBom" } -Index: app/src/main/kotlin/com/d4rk/cleaner/ui/screens/home/HomeScreen.kt -IDEA additional info: -Subsystem: com.intellij.openapi.diff.impl.patch.BaseRevisionTextPatchEP -<+>package com.d4rk.cleaner.ui.screens.home\r\n\r\nimport android.app.Activity\r\nimport android.content.Context\r\nimport androidx.compose.animation.AnimatedContent\r\nimport androidx.compose.animation.Crossfade\r\nimport androidx.compose.animation.animateColorAsState\r\nimport androidx.compose.animation.animateContentSize\r\nimport androidx.compose.animation.core.tween\r\nimport androidx.compose.foundation.background\r\nimport androidx.compose.foundation.interaction.MutableInteractionSource\r\nimport androidx.compose.foundation.layout.Arrangement\r\nimport androidx.compose.foundation.layout.Box\r\nimport androidx.compose.foundation.layout.Column\r\nimport androidx.compose.foundation.layout.Row\r\nimport androidx.compose.foundation.layout.Spacer\r\nimport androidx.compose.foundation.layout.aspectRatio\r\nimport androidx.compose.foundation.layout.fillMaxSize\r\nimport androidx.compose.foundation.layout.fillMaxWidth\r\nimport androidx.compose.foundation.layout.height\r\nimport androidx.compose.foundation.layout.offset\r\nimport androidx.compose.foundation.layout.padding\r\nimport androidx.compose.foundation.layout.size\r\nimport androidx.compose.foundation.layout.width\r\nimport androidx.compose.foundation.lazy.LazyColumn\r\nimport androidx.compose.foundation.pager.HorizontalPager\r\nimport androidx.compose.foundation.pager.PagerState\r\nimport androidx.compose.foundation.pager.rememberPagerState\r\nimport androidx.compose.foundation.shape.RoundedCornerShape\r\nimport androidx.compose.material.icons.Icons\r\nimport androidx.compose.material.icons.filled.Check\r\nimport androidx.compose.material.icons.outlined.Close\r\nimport androidx.compose.material.icons.outlined.Delete\r\nimport androidx.compose.material.icons.outlined.DeleteForever\r\nimport androidx.compose.material.icons.outlined.FolderOff\r\nimport androidx.compose.material.icons.outlined.Refresh\r\nimport androidx.compose.material3.Button\r\nimport androidx.compose.material3.ButtonDefaults\r\nimport androidx.compose.material3.Card\r\nimport androidx.compose.material3.Checkbox\r\nimport androidx.compose.material3.CircularProgressIndicator\r\nimport androidx.compose.material3.FilterChip\r\nimport androidx.compose.material3.Icon\r\nimport androidx.compose.material3.IconButton\r\nimport androidx.compose.material3.MaterialTheme\r\nimport androidx.compose.material3.OutlinedButton\r\nimport androidx.compose.material3.OutlinedCard\r\nimport androidx.compose.material3.ScrollableTabRow\r\nimport androidx.compose.material3.Tab\r\nimport androidx.compose.material3.TabRowDefaults\r\nimport androidx.compose.material3.TabRowDefaults.tabIndicatorOffset\r\nimport androidx.compose.material3.Text\r\nimport androidx.compose.runtime.Composable\r\nimport androidx.compose.runtime.LaunchedEffect\r\nimport androidx.compose.runtime.collectAsState\r\nimport androidx.compose.runtime.getValue\r\nimport androidx.compose.runtime.mutableStateOf\r\nimport androidx.compose.runtime.remember\r\nimport androidx.compose.runtime.rememberCoroutineScope\r\nimport androidx.compose.runtime.setValue\r\nimport androidx.compose.ui.Alignment\r\nimport androidx.compose.ui.Modifier\r\nimport androidx.compose.ui.graphics.Color\r\nimport androidx.compose.ui.layout.ContentScale\r\nimport androidx.compose.ui.platform.LocalContext\r\nimport androidx.compose.ui.res.painterResource\r\nimport androidx.compose.ui.res.stringResource\r\nimport androidx.compose.ui.text.style.TextOverflow\r\nimport androidx.compose.ui.unit.dp\r\nimport androidx.lifecycle.viewmodel.compose.viewModel\r\nimport coil.ImageLoader\r\nimport coil.compose.AsyncImage\r\nimport coil.decode.VideoFrameDecoder\r\nimport coil.disk.DiskCache\r\nimport coil.memory.MemoryCache\r\nimport coil.request.ImageRequest\r\nimport coil.request.videoFramePercent\r\nimport com.d4rk.cleaner.R\r\nimport com.d4rk.cleaner.data.model.ui.error.UiErrorModel\r\nimport com.d4rk.cleaner.data.model.ui.screens.UiHomeModel\r\nimport com.d4rk.cleaner.ui.components.CircularDeterminateIndicator\r\nimport com.d4rk.cleaner.ui.components.NonLazyGrid\r\nimport com.d4rk.cleaner.ui.components.animations.bounceClick\r\nimport com.d4rk.cleaner.ui.components.animations.hapticPagerSwipe\r\nimport com.d4rk.cleaner.ui.components.dialogs.ErrorAlertDialog\r\nimport com.d4rk.cleaner.ui.components.dialogs.RescanAlertDialog\r\nimport com.d4rk.cleaner.utils.PermissionsUtils\r\nimport com.d4rk.cleaner.utils.TimeHelper\r\nimport com.d4rk.cleaner.utils.cleaning.getFileIcon\r\nimport com.google.common.io.Files.getFileExtension\r\nimport kotlinx.coroutines.CoroutineScope\r\nimport kotlinx.coroutines.launch\r\nimport java.io.File\r\nimport java.text.SimpleDateFormat\r\nimport java.util.Date\r\nimport java.util.Locale\r\n\r\n@Composable\r\nfun HomeScreen() {\r\n val context : Context = LocalContext.current\r\n val viewModel : HomeViewModel = viewModel()\r\n val uiState : UiHomeModel by viewModel.uiState.collectAsState()\r\n val uiErrorModel : UiErrorModel by viewModel.uiErrorModel.collectAsState()\r\n\r\n val imageLoader : ImageLoader = remember {\r\n ImageLoader.Builder(context).memoryCache {\r\n MemoryCache.Builder(context).maxSizePercent(percent = 0.24).build()\r\n }.diskCache {\r\n DiskCache.Builder().directory(context.cacheDir.resolve(relative = \"image_cache\"))\r\n .maxSizePercent(percent = 0.02).build()\r\n }.build()\r\n }\r\n var showRescanDialog : Boolean by remember { mutableStateOf(value = false) }\r\n\r\n LaunchedEffect(Unit) {\r\n if (! PermissionsUtils.hasStoragePermissions(context)) {\r\n PermissionsUtils.requestStoragePermissions(context as Activity)\r\n }\r\n }\r\n\r\n LaunchedEffect(uiState.showRescanDialog) {\r\n showRescanDialog = uiState.showRescanDialog\r\n }\r\n\r\n if (showRescanDialog) {\r\n RescanAlertDialog(onYes = {\r\n viewModel.rescan()\r\n showRescanDialog = false\r\n } , onDismiss = {\r\n showRescanDialog = false\r\n })\r\n }\r\n\r\n if (uiErrorModel.showErrorDialog) {\r\n ErrorAlertDialog(errorMessage = uiErrorModel.errorMessage ,\r\n onDismiss = { viewModel.dismissErrorDialog() })\r\n }\r\n\r\n Column(modifier = Modifier.fillMaxSize()) {\r\n Box(\r\n modifier = Modifier\r\n .weight(4f)\r\n .fillMaxWidth()\r\n ) {\r\n CircularDeterminateIndicator(progress = uiState.progress ,\r\n modifier = Modifier\r\n .align(Alignment.TopCenter)\r\n .offset(y = 98.dp) ,\r\n onClick = { viewModel.analyze() })\r\n\r\n Crossfade(\r\n targetState = uiState.showCleaningComposable ,\r\n animationSpec = tween(durationMillis = 300) ,\r\n label = \"\"\r\n ) { showCleaningComposable ->\r\n if (showCleaningComposable) {\r\n AnalyzeComposable(imageLoader)\r\n }\r\n }\r\n }\r\n }\r\n}\r\n\r\n@Composable\r\nfun AnalyzeComposable(imageLoader : ImageLoader) {\r\n val viewModel : HomeViewModel = viewModel()\r\n val uiState : UiHomeModel by viewModel.uiState.collectAsState()\r\n val context = LocalContext.current\r\n val coroutineScope : CoroutineScope = rememberCoroutineScope()\r\n val enabled = uiState.selectedFileCount > 0\r\n val apkExtensions = remember { context.resources.getStringArray(R.array.apk_extensions) }\r\n val imageExtensions = remember { context.resources.getStringArray(R.array.image_extensions) }\r\n val videoExtensions = remember { context.resources.getStringArray(R.array.video_extensions) }\r\n val audioExtensions = remember { context.resources.getStringArray(R.array.audio_extensions) }\r\n val archiveExtensions =\r\n remember { context.resources.getStringArray(R.array.archive_extensions) }\r\n\r\n val groupedFiles = remember(\r\n uiState.scannedFiles ,\r\n apkExtensions ,\r\n imageExtensions ,\r\n videoExtensions ,\r\n audioExtensions ,\r\n archiveExtensions\r\n ) {\r\n uiState.scannedFiles.groupBy { file ->\r\n when (file.extension.lowercase()) {\r\n in apkExtensions -> {\r\n return@groupBy \"APKs\"\r\n }\r\n\r\n in imageExtensions -> {\r\n return@groupBy \"Images\"\r\n }\r\n\r\n in videoExtensions -> {\r\n return@groupBy \"Videos\"\r\n }\r\n\r\n in audioExtensions -> {\r\n return@groupBy \"Audios\"\r\n }\r\n\r\n in archiveExtensions -> {\r\n return@groupBy \"Archives\"\r\n }\r\n\r\n else -> {\r\n return@groupBy \"Others\"\r\n }\r\n }\r\n }\r\n }\r\n\r\n Column(\r\n modifier = Modifier\r\n .animateContentSize()\r\n .fillMaxWidth()\r\n .padding(16.dp) ,\r\n horizontalAlignment = Alignment.End\r\n ) {\r\n OutlinedCard(\r\n modifier = Modifier\r\n .weight(1f)\r\n .fillMaxWidth() ,\r\n ) {\r\n when {\r\n uiState.scannedFiles.isEmpty() -> {\r\n Box(modifier = Modifier.fillMaxSize() , contentAlignment = Alignment.Center) {\r\n CircularProgressIndicator()\r\n }\r\n }\r\n\r\n uiState.noFilesFound -> {\r\n Box(modifier = Modifier.fillMaxSize() , contentAlignment = Alignment.Center) {\r\n Column(horizontalAlignment = Alignment.CenterHorizontally) {\r\n Icon(\r\n imageVector = Icons.Outlined.FolderOff ,\r\n contentDescription = null ,\r\n modifier = Modifier.size(64.dp) ,\r\n tint = MaterialTheme.colorScheme.onSurface\r\n )\r\n Spacer(modifier = Modifier.height(16.dp))\r\n Text(\r\n text = stringResource(id = R.string.no_files_found) ,\r\n style = MaterialTheme.typography.bodyLarge ,\r\n color = MaterialTheme.colorScheme.onSurface\r\n )\r\n\r\n OutlinedButton(modifier = Modifier.bounceClick() , onClick = {\r\n viewModel.rescanFiles()\r\n }) {\r\n Icon(\r\n modifier = Modifier.size(ButtonDefaults.IconSize) ,\r\n imageVector = Icons.Outlined.Refresh ,\r\n contentDescription = \"Close\"\r\n )\r\n Spacer(modifier = Modifier.size(ButtonDefaults.IconSpacing))\r\n Text(\"Try again\")\r\n }\r\n }\r\n }\r\n }\r\n\r\n else -> {\r\n val tabs = groupedFiles.keys.toList()\r\n val pagerState : PagerState = rememberPagerState(pageCount = { tabs.size })\r\n\r\n Row(\r\n modifier = Modifier.fillMaxWidth() ,\r\n verticalAlignment = Alignment.CenterVertically\r\n ) {\r\n ScrollableTabRow(\r\n selectedTabIndex = pagerState.currentPage ,\r\n modifier = Modifier.weight(1f) ,\r\n edgePadding = 0.dp ,\r\n indicator = { tabPositions ->\r\n TabRowDefaults.PrimaryIndicator(\r\n modifier = Modifier.tabIndicatorOffset(tabPositions[pagerState.currentPage]) ,\r\n shape = RoundedCornerShape(\r\n topStart = 3.dp ,\r\n topEnd = 3.dp ,\r\n bottomEnd = 0.dp ,\r\n bottomStart = 0.dp ,\r\n ) ,\r\n )\r\n } ,\r\n ) {\r\n tabs.forEachIndexed { index , title ->\r\n Tab(modifier = Modifier.bounceClick() ,\r\n selected = pagerState.currentPage == index ,\r\n onClick = {\r\n coroutineScope.launch {\r\n pagerState.animateScrollToPage(index)\r\n }\r\n } ,\r\n text = { Text(text = title) })\r\n }\r\n }\r\n\r\n IconButton(modifier = Modifier.bounceClick() , onClick = {\r\n viewModel.onCloseAnalyzeComposable()\r\n }) {\r\n Icon(imageVector = Icons.Outlined.Close , contentDescription = \"Close\")\r\n }\r\n }\r\n\r\n HorizontalPager(\r\n modifier = Modifier.hapticPagerSwipe(pagerState) ,\r\n state = pagerState ,\r\n ) { page ->\r\n val filesForCurrentPage = groupedFiles[tabs[page]] ?: emptyList()\r\n\r\n val filesByDate = filesForCurrentPage.groupBy { file ->\r\n SimpleDateFormat(\r\n \"yyyy-MM-dd\" , Locale.getDefault()\r\n ).format(Date(file.lastModified()))\r\n }\r\n\r\n LazyColumn(\r\n modifier = Modifier.fillMaxSize() ,\r\n ) {\r\n\r\n val sortedDates = filesByDate.keys.sortedByDescending { dateString ->\r\n return@sortedByDescending SimpleDateFormat(\r\n \"yyyy-MM-dd\" ,\r\n Locale.getDefault()\r\n ).parse(dateString)\r\n }\r\n sortedDates.forEach { date ->\r\n val files = filesByDate[date] ?: emptyList()\r\n item(key = date) {\r\n Row(\r\n modifier = Modifier\r\n .fillMaxWidth()\r\n .padding(horizontal = 8.dp , vertical = 4.dp) ,\r\n verticalAlignment = Alignment.CenterVertically ,\r\n horizontalArrangement = Arrangement.SpaceBetween\r\n ) {\r\n Text(\r\n modifier = Modifier.padding(start = 8.dp) ,\r\n text = TimeHelper.formatDate(Date(files[0].lastModified()))\r\n )\r\n val allFilesForDateSelected =\r\n files.all { uiState.fileSelectionStates[it] == true }\r\n Checkbox(modifier = Modifier.bounceClick() ,\r\n checked = allFilesForDateSelected ,\r\n onCheckedChange = { isChecked ->\r\n files.forEach { file ->\r\n viewModel.onFileSelectionChange(\r\n file , isChecked\r\n )\r\n }\r\n })\r\n }\r\n }\r\n\r\n item(key = \"$date-grid\") {\r\n Box(\r\n modifier = Modifier.fillMaxSize()\r\n ) {\r\n NonLazyGrid(\r\n columns = 3 ,\r\n itemCount = files.size ,\r\n modifier = Modifier.padding(horizontal = 8.dp)\r\n ) { index ->\r\n FileCard(\r\n file = files[index] ,\r\n viewModel = viewModel ,\r\n imageLoader = imageLoader\r\n )\r\n }\r\n }\r\n }\r\n }\r\n }\r\n }\r\n }\r\n }\r\n }\r\n if (uiState.scannedFiles.isNotEmpty()) {\r\n Row(\r\n modifier = Modifier.fillMaxWidth() ,\r\n verticalAlignment = Alignment.CenterVertically ,\r\n horizontalArrangement = Arrangement.SpaceBetween ,\r\n ) {\r\n val statusText : String = if (uiState.selectedFileCount > 0) {\r\n stringResource(id = R.string.status_selected_files , uiState.selectedFileCount)\r\n }\r\n else {\r\n stringResource(id = R.string.status_no_files_selected)\r\n }\r\n val statusColor : Color by animateColorAsState(\r\n targetValue = if (uiState.selectedFileCount > 0) {\r\n MaterialTheme.colorScheme.primary\r\n }\r\n else {\r\n MaterialTheme.colorScheme.secondary\r\n } , animationSpec = tween() , label = \"Selected Files Status Color Animation\"\r\n )\r\n\r\n Text(\r\n text = statusText ,\r\n color = statusColor ,\r\n modifier = Modifier.animateContentSize()\r\n )\r\n SelectAllComposable(viewModel)\r\n }\r\n\r\n Row(\r\n modifier = Modifier.fillMaxWidth() , horizontalArrangement = Arrangement.SpaceAround\r\n ) {\r\n OutlinedButton(\r\n enabled = false , // TODO: Currently false by default because there's no trash\r\n onClick = {\r\n // TODO: add trash\r\n } ,\r\n modifier = Modifier\r\n .weight(1f)\r\n .bounceClick() ,\r\n colors = ButtonDefaults.outlinedButtonColors(contentColor = Color.Black)\r\n ) {\r\n Icon(\r\n imageVector = Icons.Outlined.Delete ,\r\n contentDescription = \"Move to trash\" ,\r\n modifier = Modifier.size(ButtonDefaults.IconSize)\r\n )\r\n Spacer(Modifier.size(ButtonDefaults.IconSpacing))\r\n Text(text = \"Move to trash\")\r\n }\r\n\r\n Spacer(Modifier.width(8.dp))\r\n\r\n Button(\r\n enabled = enabled ,\r\n onClick = {\r\n viewModel.clean()\r\n } ,\r\n modifier = Modifier\r\n .weight(1f)\r\n .bounceClick() ,\r\n colors = ButtonDefaults.buttonColors(contentColor = Color.White)\r\n ) {\r\n Icon(\r\n imageVector = Icons.Outlined.DeleteForever ,\r\n contentDescription = \"Delete forever\" ,\r\n modifier = Modifier.size(ButtonDefaults.IconSize)\r\n )\r\n Spacer(Modifier.size(ButtonDefaults.IconSpacing))\r\n Text(text = \"Delete forever\")\r\n }\r\n }\r\n }\r\n }\r\n}\r\n\r\n@Composable\r\nfun FileCard(file : File , viewModel : HomeViewModel , imageLoader : ImageLoader) {\r\n val context : Context = LocalContext.current\r\n val fileExtension : String = remember(file.name) { getFileExtension(file.name) }\r\n\r\n val imageExtensions =\r\n remember { context.resources.getStringArray(R.array.image_extensions).toList() }\r\n val videoExtensions =\r\n remember { context.resources.getStringArray(R.array.video_extensions).toList() }\r\n\r\n Card(\r\n modifier = Modifier\r\n .fillMaxWidth()\r\n .aspectRatio(ratio = 1f)\r\n .bounceClick() ,\r\n ) {\r\n Box(modifier = Modifier.fillMaxSize()) {\r\n when (fileExtension) {\r\n in imageExtensions -> {\r\n AsyncImage(\r\n model = remember(file) {\r\n ImageRequest.Builder(context).data(file).size(64)\r\n .crossfade(enable = true).build()\r\n } ,\r\n imageLoader = imageLoader ,\r\n contentDescription = file.name ,\r\n contentScale = ContentScale.Crop ,\r\n modifier = Modifier.fillMaxSize() ,\r\n )\r\n }\r\n\r\n in videoExtensions -> {\r\n AsyncImage(\r\n model = remember(file) {\r\n ImageRequest.Builder(context).data(file)\r\n .decoderFactory { result, options, _ ->\r\n VideoFrameDecoder(result.source, options)\r\n }\r\n .videoFramePercent(framePercent = 0.5)\r\n .crossfade(enable = true).build()\r\n },\r\n imageLoader = imageLoader,\r\n contentDescription = file.name,\r\n contentScale = ContentScale.Crop,\r\n modifier = Modifier.fillMaxSize()\r\n )\r\n }\r\n\r\n else -> {\r\n val fileIcon = remember(fileExtension) {\r\n getFileIcon(\r\n fileExtension , context\r\n )\r\n }\r\n Icon(\r\n painter = painterResource(fileIcon) ,\r\n contentDescription = null ,\r\n modifier = Modifier\r\n .size(24.dp)\r\n .align(Alignment.Center)\r\n )\r\n }\r\n }\r\n\r\n Checkbox(checked = viewModel.uiState.value.fileSelectionStates[file] ?: false ,\r\n onCheckedChange = { isChecked ->\r\n viewModel.onFileSelectionChange(file , isChecked)\r\n } ,\r\n modifier = Modifier.align(Alignment.TopEnd))\r\n\r\n Text(\r\n text = file.name ,\r\n maxLines = 1 ,\r\n overflow = TextOverflow.Ellipsis ,\r\n modifier = Modifier\r\n .fillMaxWidth()\r\n .background(\r\n color = Color.Black.copy(alpha = 0.4f)\r\n )\r\n .padding(8.dp)\r\n .align(Alignment.BottomCenter)\r\n )\r\n }\r\n }\r\n}\r\n\r\n/**\r\n * Composable function for selecting or deselecting all items.\r\n *\r\n * This composable displays a filter chip labeled \"Select All\". When tapped, it toggles the\r\n * selection state and invokes the `onCheckedChange` callback.\r\n *\r\n * @param checked A boolean value indicating whether all items are currently selected.\r\n * @param onCheckedChange A callback function that is invoked when the user taps the chip to change the selection state.\r\n */\r\n@Composable\r\nfun SelectAllComposable(\r\n viewModel : HomeViewModel ,\r\n) {\r\n val uiState : UiHomeModel by viewModel.uiState.collectAsState()\r\n\r\n Row(\r\n modifier = Modifier\r\n .fillMaxWidth()\r\n .animateContentSize() ,\r\n verticalAlignment = Alignment.CenterVertically ,\r\n horizontalArrangement = Arrangement.End\r\n ) {\r\n val interactionSource : MutableInteractionSource = remember { MutableInteractionSource() }\r\n FilterChip(\r\n modifier = Modifier.bounceClick() ,\r\n selected = uiState.allFilesSelected ,\r\n onClick = {\r\n viewModel.toggleSelectAllFiles()\r\n } ,\r\n label = { Text(stringResource(id = R.string.select_all)) } ,\r\n leadingIcon = {\r\n AnimatedContent(\r\n targetState = uiState.allFilesSelected , label = \"Checkmark Animation\"\r\n ) { targetChecked ->\r\n if (targetChecked) {\r\n Icon(\r\n imageVector = Icons.Filled.Check ,\r\n contentDescription = null ,\r\n )\r\n }\r\n }\r\n } ,\r\n interactionSource = interactionSource ,\r\n )\r\n }\r\n} -Subsystem: com.intellij.openapi.diff.impl.patch.CharsetEP -<+>UTF-8 -=================================================================== -diff --git a/app/src/main/kotlin/com/d4rk/cleaner/ui/screens/home/HomeScreen.kt b/app/src/main/kotlin/com/d4rk/cleaner/ui/screens/home/HomeScreen.kt ---- a/app/src/main/kotlin/com/d4rk/cleaner/ui/screens/home/HomeScreen.kt (revision 57c1e1bfa29ea2909c3b8e44202c38eb1a5c37c7) -+++ b/app/src/main/kotlin/com/d4rk/cleaner/ui/screens/home/HomeScreen.kt (date 1727770813772) -@@ -64,6 +64,7 @@ - import androidx.compose.ui.layout.ContentScale - import androidx.compose.ui.platform.LocalContext - import androidx.compose.ui.res.painterResource -+import androidx.compose.ui.res.stringArrayResource - import androidx.compose.ui.res.stringResource - import androidx.compose.ui.text.style.TextOverflow - import androidx.compose.ui.unit.dp -@@ -175,38 +176,40 @@ - val archiveExtensions = - remember { context.resources.getStringArray(R.array.archive_extensions) } - -+ val filesTypesStrings: List = stringArrayResource(R.array.file_types_titles).toList() -+ - val groupedFiles = remember( -- uiState.scannedFiles , -- apkExtensions , -- imageExtensions , -- videoExtensions , -- audioExtensions , -+ uiState.scannedFiles, -+ apkExtensions, -+ imageExtensions, -+ videoExtensions, -+ audioExtensions, - archiveExtensions - ) { - uiState.scannedFiles.groupBy { file -> - when (file.extension.lowercase()) { -- in apkExtensions -> { -- return@groupBy "APKs" -- } -- - in imageExtensions -> { -- return@groupBy "Images" -+ return@groupBy filesTypesStrings[0] -+ } -+ -+ in apkExtensions -> { -+ return@groupBy filesTypesStrings[3] - } - - in videoExtensions -> { -- return@groupBy "Videos" -+ return@groupBy filesTypesStrings[2] - } - - in audioExtensions -> { -- return@groupBy "Audios" -+ return@groupBy filesTypesStrings[1] - } - - in archiveExtensions -> { -- return@groupBy "Archives" -+ return@groupBy filesTypesStrings[4] - } - - else -> { -- return@groupBy "Others" -+ return@groupBy filesTypesStrings[5] // Others - } - } - } -Index: app/src/main/res/values/arrays.xml -IDEA additional info: -Subsystem: com.intellij.openapi.diff.impl.patch.BaseRevisionTextPatchEP -<+>\r\n \r\n DS_Store\r\n Spotlight-V100\r\n fseventsd\r\n log\r\n logs\r\n old\r\n tmp\r\n temp\r\n splashad\r\n db\r\n nomedia\r\n \r\n\r\n \r\n apk\r\n apks\r\n apkm\r\n apkx\r\n aab\r\n \r\n\r\n \r\n doc\r\n dot\r\n wbk\r\n docx\r\n docm\r\n dotx\r\n dotm\r\n docb\r\n pdf\r\n wll\r\n wwl\r\n xls\r\n xlt\r\n xlm\r\n xlsx\r\n xlsm\r\n xltx\r\n xltm\r\n xlsb\r\n xla\r\n xlam\r\n xll\r\n xlw\r\n ppt\r\n pot\r\n pps\r\n ppa\r\n pptx\r\n pptm\r\n potx\r\n potm\r\n ppam\r\n ppsx\r\n ppsm\r\n sldx\r\n sldm\r\n ppam\r\n accda\r\n accdb\r\n accde\r\n accdr\r\n accdt\r\n accdu\r\n one\r\n ecf\r\n pub\r\n \r\n\r\n \r\n 7z\r\n ace\r\n arj\r\n bz\r\n bz2\r\n cab\r\n cb7\r\n cbt\r\n cbz\r\n cbr\r\n dgc\r\n dmg\r\n ear\r\n gz\r\n gzip\r\n ha\r\n ice\r\n jar\r\n kgb\r\n lzh\r\n lz\r\n lzma\r\n lzo\r\n pak\r\n partimg\r\n pea\r\n pet\r\n pk3\r\n pk4\r\n rar\r\n rpm\r\n rz\r\n s7z\r\n sda\r\n sea\r\n sen\r\n sfark\r\n sfx\r\n shar\r\n sit\r\n sitx\r\n sqx\r\n tar\r\n tar.bz2\r\n tar.gz\r\n tar.xz\r\n tgz\r\n tlz\r\n txz\r\n udf\r\n utz\r\n uu\r\n uue\r\n war\r\n wim\r\n xar\r\n xp3\r\n xz\r\n z\r\n zip\r\n zipx\r\n zoo\r\n zpaq\r\n \r\n\r\n \r\n mp3\r\n m4a\r\n wav\r\n flac\r\n midi\r\n wma\r\n aac\r\n ogg\r\n opus\r\n aiff\r\n \r\n\r\n \r\n mp4\r\n avi\r\n mov\r\n mkv\r\n flv\r\n webm\r\n vob\r\n ogv\r\n gif\r\n gifv\r\n mng\r\n wmv\r\n yuv\r\n amv\r\n \r\n\r\n \r\n jpg\r\n jpeg\r\n webp\r\n bmp\r\n png\r\n raw\r\n psd\r\n \r\n\r\n \r\n exe\r\n msi\r\n ini\r\n \r\n\r\n \r\n ttf\r\n otf\r\n \r\n\r\n \r\n nomedia\r\n splashad\r\n csv\r\n thumbs?.db\r\n thumb[0–9]\r\n pxr\r\n xd\r\n .xml\r\n .z\r\n .tar\r\n .gz\r\n .iso\r\n .cab\r\n .jar\r\n .dng\r\n .bz2\r\n .lzh\r\n .lzma\r\n .ace\r\n .arj\r\n .deb\r\n .rpm\r\n .tar.gz\r\n .tar.xz\r\n .gz.tar\r\n .bz2tar\r\n .xz.tar\r\n .z\r\n .lz\r\n .txt\r\n .viz\r\n .psxpj\r\n .htm\r\n .html\r\n \r\n\r\n \r\n .Trash\r\n .DS_Store\r\n .estrongs\r\n .facebook_cache\r\n .spotlight-V100\r\n .thumbnails\r\n Ads\r\n AnyDesk\r\n Apk Editor\r\n Apk Editor Plus\r\n Apk Editor Pro\r\n Audiobooks\r\n Bluetoooth\r\n Bluetooth\r\n Bugreport\r\n Cache\r\n Camera\r\n DCIM\r\n Desktop.ini\r\n Discord\r\n Download\r\n Downloads\r\n Fseventsd\r\n GBWhatsApp\r\n GBWhatsApp Animated Gifs\r\n GBWhatsApp Audio\r\n GBWhatsApp Documents\r\n GBWhatsApp Images\r\n GBWhatsApp Stickers\r\n GBWhatsApp Video\r\n Ifont\r\n Jumobile\r\n Kinguserdown\r\n Logs\r\n Lost.dir\r\n Media\r\n Minidump\r\n Mobvista\r\n Notifications\r\n OSSLog\r\n Photoeditor\r\n Photos\r\n Picsart\r\n Podcasts\r\n QQSecureDownload\r\n Ringtones\r\n Screenshots\r\n Temp\r\n Temporary\r\n Thumbnails?\r\n Telegram\r\n Tmp\r\n Tmp\r\n Twrp\r\n Unityadsvideocache\r\n WhatsApp\r\n WhatsApp Business\r\n WhatsApp Animated Gifs\r\n WhatsApp Audio\r\n WhatsApp Documents\r\n WhatsApp Images\r\n WhatsApp Stickers\r\n WhatsApp Video\r\n Xiaoying\r\n \r\n\r\n \r\n .AppleDouble\r\n .DocumentRevisions-V100\r\n .MobileBackups\r\n .Spotlight-V100\r\n .Trashes\r\n .VolumeIcon.icns\r\n .android\r\n .cache\r\n .gradle\r\n .idea\r\n .thumbnails\r\n .vscode\r\n AndroidStudioProjects\r\n Backup\r\n DCIM/.thumbnails\r\n Log\r\n Logs\r\n Podcasts\r\n ads\r\n advertisement\r\n bin\r\n backup\r\n cache\r\n logs\r\n node_modules\r\n obj\r\n temp\r\n temporary\r\n temps\r\n thumbs\r\n thumbnails\r\n target\r\n tmp\r\n venv\r\n \r\n\r\n \r\n Home\r\n App Manager\r\n Memory Manager\r\n \r\n \r\n home\r\n app_manager\r\n memory_manager\r\n \r\n\r\n \r\n @string/bulgarian\r\n @string/english\r\n @string/french\r\n @string/german\r\n @string/hindi\r\n @string/hungarian\r\n @string/indonesian\r\n @string/italian\r\n @string/japanese\r\n @string/polish\r\n @string/romanian\r\n @string/russian\r\n @string/spanish\r\n @string/turkish\r\n @string/ukrainian\r\n @string/traditional_chinese\r\n \r\n \r\n bg\r\n en\r\n fr\r\n de\r\n hi\r\n hu\r\n in\r\n it\r\n ja\r\n pl\r\n ro\r\n ru\r\n es\r\n tr\r\n uk\r\n zh-rTW\r\n \r\n \r\n 200\r\n 400\r\n 600\r\n 800\r\n 900\r\n \r\n -Subsystem: com.intellij.openapi.diff.impl.patch.CharsetEP -<+>UTF-8 -=================================================================== -diff --git a/app/src/main/res/values/arrays.xml b/app/src/main/res/values/arrays.xml ---- a/app/src/main/res/values/arrays.xml (revision 57c1e1bfa29ea2909c3b8e44202c38eb1a5c37c7) -+++ b/app/src/main/res/values/arrays.xml (date 1727770683071) -@@ -1,4 +1,13 @@ - -+ -+ @string/images -+ @string/audios -+ @string/videos -+ @string/apks -+ @string/archives -+ @string/other -+ -+ - - DS_Store - Spotlight-V100 -Index: CHANGELOG.md -IDEA additional info: -Subsystem: com.intellij.openapi.diff.impl.patch.BaseRevisionTextPatchEP -<+># Version 2.0.0:\r\n\r\n- **New**: Added a progress bar to the main screen, offering an approximate visualization of storage\r\n usage.\r\n- **New**: Users can now select specific files for deletion after the scan completes, allowing for\r\n granular control.\r\n- **New**: Enhanced the post-scan screen to display previews of images and videos, aiding in file\r\n selection.\r\n- **New**: Introduced the option to select all files for deletion, streamlining the cleaning\r\n process.\r\n- **New**: Completely overhauled the memory manager, now showcasing storage usage categorized by\r\n file types.\r\n- **New**: Added support for dynamic colors on compatible devices, allowing the app to adapt to\r\n system-wide color palettes.\r\n- **New**: Refined the AMOLED theme for a more immersive dark mode experience.\r\n- **New**: Incorporated updated translations thanks to valuable contributions from the community.\r\n- **New**: Introduced a dedicated section for managing security and privacy settings within the app.\r\n- **New**: Implemented new animations and improved overall app responsiveness for a smoother user\r\n experience.\r\n- **Major**: Migrated the entire app to Jetpack Compose, providing a modern and improved user\r\n interface.\r\n- **Major**: Completely reworked the app's logic using view models and coroutines for enhanced\r\n performance and maintainability.\r\n\r\n# Version 1.1.0:\r\n\r\n- **Minor**: Added GitHub issues templates.\r\n- **Minor**: Updated project dependencies.\r\n- **Minor**: Improved the user experience in Help and Feedback page.\r\n- **Minor**: Made minor under-the-hood improvements for a better overall app experience.\r\n- **Patch**: Fixed an issue that caused the app to crash when the device was rotated during the\r\n cleaning or scanning process.\r\n- **Patch**: Fixed an issue encountered on Android 14 where the application was requesting photo\r\n permissions and initiating the photo picker API.\r\n- **Patch**: Fixed an issue in the Image Optimizer that led to a crash when no gallery application\r\n was installed on the user’s device.\r\n\r\n# Version 1.0.0:\r\n\r\n- **New**: Added multiple languages support for the app.\r\n- **New**: Added a new bottom app bar for the main components of the cleaner to be more accessible\r\n for the user.\r\n- **New**: Added legal notices and more information about permissions.\r\n- **New**: Added a bug report feature to report bugs on GitHub.\r\n- **New**: Added many display customizations for the app.\r\n- **New**: Added a new GDPR message to comply with Google Play policy.\r\n- **New**: Added new custom startup animations.\r\n- **New**: Added support for AMOLED themes.\r\n- **New**: APK sharing functionality has been improved to ensure the proper sharing of APK files.\r\n- **Major**: Migrated the app to Semantic Versioning (SemVer).\r\n- **Major**: Reworked the settings page and organized it way better.\r\n- **Major**: Reworked the file list menu, providing a more intuitive and efficient user experience.\r\n- **Major**: Reworked the Image Optimizer by adding quick compression, manual compression and file\r\n compression.\r\n- **Minor**: Reset the version to 1.0.0 for a fresh start.\r\n- **Minor**: Replaced toasts with snack bars for all notifications.\r\n- **Minor**: Addressed an aesthetic concern by optimizing the display of the temperature icon when\r\n the application operates in dark mode.\r\n- **Patch**: Rectified an issue where the uninstall item was inappropriately located within both the\r\n APKs and System apps tabs, ensuring a more streamlined and intuitive user experience.\r\n- **Patch**: Improved the ads initialization and loading for a better user experience.\r\n- **Patch**: Improved permissions handling logic for improved security and user experience.\r\n- **Patch**: Improved the app's logical parent activities.\r\n- **Patch**: Fixed daily clean running when is turned off from settings.\r\n- **Patch**: Fixed memory manager crashes when trying to exit and enter.\r\n- **Patch**: Fixed the whitelist save issue.\r\n- **Patch**: Fixed uninstall option from the App Manager.\r\n- **Patch**: Fixed app crashes caused by the image optimizer.\r\n- **Patch**: Made various under-the-hood improvements for a better overall app experience. -Subsystem: com.intellij.openapi.diff.impl.patch.CharsetEP -<+>UTF-8 -=================================================================== -diff --git a/CHANGELOG.md b/CHANGELOG.md ---- a/CHANGELOG.md (revision 57c1e1bfa29ea2909c3b8e44202c38eb1a5c37c7) -+++ b/CHANGELOG.md (date 1727541947448) -@@ -1,3 +1,39 @@ -+# Version 3.0.0: -+ -+- **New**: Completely redesigned the Home and Analyze screens for a more intuitive and visually -+ appealing user interface. -+- **New**: Implemented date-based filtering in the Analyze screen, allowing for easier file -+ selection and management. -+- **Patch**:Resolved an issue where APK installation from the App Manager was not functioning -+ correctly. -+- **Patch**:Improved the "Select All" button behavior in the App Manager to ensure accurate state -+ representation. -+- **New**: Introduced loading indicators in the App Manager for a smoother and more informative -+ experience. -+- **New**: Added messages to indicate when no apps or APKs are found. -+- **Patch**:Optimized app loading speed in the App Manager through code refactoring and efficiency -+ improvements. -+- **New**: Enabled real-time RAM usage monitoring in the Memory Manager. -+- **New**: Added a loading animation to the Memory Manager for visual feedback during data -+ retrieval. -+- **New**: Introduced a new setting to customize the app's startup page, providing users with -+ greater control over their initial experience. -+- **New**: Added an option to disable the bounce click effect for users who prefer a more -+ traditional interaction style. -+- **Patch**:Resolved an issue where language selection was not consistently applied. -+- **New**: Added a snackbar notification for older Android versions when users copy device -+ information from the About section. -+- **New**: Integrated haptic feedback for swipe gestures, enhancing the tactile response and user -+ experience. -+- **New**: Implemented sound effects on tap interactions for a more engaging experience. -+- **New**: Redesigned the Settings page, aligning it with the modern aesthetics of the Android 15 -+ design system. -+- **Minor**: Backported the app to support devices running Android 6.0 and above. -+- **Patch**:Fixed visual glitches within the App Manager. -+- **Major**: Restructured the app's code flow for improved organization and future extensibility. -+- **Major**: Implemented a robust error handling mechanism to gracefully manage unexpected -+ situations and provide helpful feedback to users. -+ - # Version 2.0.0: - - - **New**: Added a progress bar to the main screen, offering an approximate visualization of storage diff --git a/app/build.gradle.kts b/app/build.gradle.kts index 2074391..1548a82 100644 --- a/app/build.gradle.kts +++ b/app/build.gradle.kts @@ -14,7 +14,7 @@ android { applicationId = "com.d4rk.cleaner" minSdk = 23 targetSdk = 35 - versionCode = 133 + versionCode = 134 versionName = "3.0.0" testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner" resourceConfigurations += listOf( diff --git a/app/src/main/kotlin/com/d4rk/cleaner/data/datastore/DataStore.kt b/app/src/main/kotlin/com/d4rk/cleaner/data/datastore/DataStore.kt index 4d2f2bf..9add2f1 100644 --- a/app/src/main/kotlin/com/d4rk/cleaner/data/datastore/DataStore.kt +++ b/app/src/main/kotlin/com/d4rk/cleaner/data/datastore/DataStore.kt @@ -46,7 +46,7 @@ class DataStore(context: Context) { private val startupKey = booleanPreferencesKey(name = DataStoreNamesConstants.DATA_STORE_STARTUP) val startup: Flow = dataStore.data.map { preferences -> - preferences[startupKey] ?: true + preferences[startupKey] != false } suspend fun saveStartup(isFirstTime: Boolean) { @@ -72,7 +72,7 @@ class DataStore(context: Context) { private val amoledModeKey = booleanPreferencesKey(name = DataStoreNamesConstants.DATA_STORE_AMOLED_MODE) val amoledMode: Flow = dataStore.data.map { preferences -> - preferences[amoledModeKey] ?: false + preferences[amoledModeKey] == true } suspend fun saveAmoledMode(isChecked: Boolean) { @@ -84,7 +84,7 @@ class DataStore(context: Context) { private val dynamicColorsKey = booleanPreferencesKey(name = DataStoreNamesConstants.DATA_STORE_DYNAMIC_COLORS) val dynamicColors: Flow = dataStore.data.map { preferences -> - preferences[dynamicColorsKey] ?: true + preferences[dynamicColorsKey] != false } suspend fun saveDynamicColors(isChecked: Boolean) { @@ -96,7 +96,7 @@ class DataStore(context: Context) { private val bouncyButtonsKey = booleanPreferencesKey(name = DataStoreNamesConstants.DATA_STORE_BOUNCY_BUTTONS) val bouncyButtons: Flow = dataStore.data.map { preferences -> - preferences[bouncyButtonsKey] ?: true + preferences[bouncyButtonsKey] != false } suspend fun saveBouncyButtons(isChecked: Boolean) { @@ -121,8 +121,7 @@ class DataStore(context: Context) { fun getShowBottomBarLabels(): Flow { return dataStore.data.map { preferences -> - preferences[booleanPreferencesKey(name = DataStoreNamesConstants.DATA_STORE_SHOW_BOTTOM_BAR_LABELS)] - ?: true + preferences[booleanPreferencesKey(name = DataStoreNamesConstants.DATA_STORE_SHOW_BOTTOM_BAR_LABELS)] != false } } @@ -169,7 +168,7 @@ class DataStore(context: Context) { private val genericFilterKey = booleanPreferencesKey(name = DataStoreNamesConstants.DATA_STORE_GENERIC_FILTER) val genericFilter: Flow = dataStore.data.map { preferences -> - preferences[genericFilterKey] ?: false + preferences[genericFilterKey] == true } suspend fun saveGenericFilter(isChecked: Boolean) { @@ -181,7 +180,7 @@ class DataStore(context: Context) { private val deleteEmptyFoldersKey = booleanPreferencesKey(name = DataStoreNamesConstants.DATA_STORE_DELETE_EMPTY_FOLDERS) val deleteEmptyFolders: Flow = dataStore.data.map { preferences -> - preferences[deleteEmptyFoldersKey] ?: true + preferences[deleteEmptyFoldersKey] != false } suspend fun saveDeleteEmptyFolders(isChecked: Boolean) { @@ -193,7 +192,7 @@ class DataStore(context: Context) { private val deleteArchivesKey = booleanPreferencesKey(name = DataStoreNamesConstants.DATA_STORE_DELETE_ARCHIVES) val deleteArchives: Flow = dataStore.data.map { preferences -> - preferences[deleteArchivesKey] ?: false + preferences[deleteArchivesKey] == true } suspend fun saveDeleteArchives(isChecked: Boolean) { @@ -205,7 +204,7 @@ class DataStore(context: Context) { private val deleteInvalidMediaKey = booleanPreferencesKey(name = DataStoreNamesConstants.DATA_STORE_DELETE_INVALID_MEDIA) val deleteInvalidMedia: Flow = dataStore.data.map { preferences -> - preferences[deleteInvalidMediaKey] ?: false + preferences[deleteInvalidMediaKey] == true } suspend fun saveDeleteInvalidMedia(isChecked: Boolean) { @@ -217,7 +216,7 @@ class DataStore(context: Context) { private val deleteCorpseFilesKey = booleanPreferencesKey(name = DataStoreNamesConstants.DATA_STORE_DELETE_CORPSE_FILES) val deleteCorpseFiles: Flow = dataStore.data.map { preferences -> - preferences[deleteCorpseFilesKey] ?: false + preferences[deleteCorpseFilesKey] == true } suspend fun saveDeleteCorpseFiles(isChecked: Boolean) { @@ -229,7 +228,7 @@ class DataStore(context: Context) { private val deleteApkFilesKey = booleanPreferencesKey(name = DataStoreNamesConstants.DATA_STORE_DELETE_APK_FILES) val deleteApkFiles: Flow = dataStore.data.map { preferences -> - preferences[deleteApkFilesKey] ?: true + preferences[deleteApkFilesKey] != false } suspend fun saveDeleteApkFiles(isChecked: Boolean) { @@ -241,7 +240,7 @@ class DataStore(context: Context) { private val deleteAudioFilesKey = booleanPreferencesKey(name = DataStoreNamesConstants.DATA_STORE_DELETE_AUDIO_FILES) val deleteAudioFiles: Flow = dataStore.data.map { preferences -> - preferences[deleteAudioFilesKey] ?: true + preferences[deleteAudioFilesKey] != false } suspend fun saveDeleteAudioFiles(isChecked: Boolean) { @@ -253,7 +252,7 @@ class DataStore(context: Context) { private val deleteVideoFilesKey = booleanPreferencesKey(name = DataStoreNamesConstants.DATA_STORE_DELETE_VIDEO_FILES) val deleteVideoFiles: Flow = dataStore.data.map { preferences -> - preferences[deleteVideoFilesKey] ?: true + preferences[deleteVideoFilesKey] != false } suspend fun saveDeleteVideoFiles(isChecked: Boolean) { @@ -265,7 +264,7 @@ class DataStore(context: Context) { private val deleteImageFilesKey = booleanPreferencesKey(name = DataStoreNamesConstants.DATA_STORE_DELETE_IMAGE_FILES) val deleteImageFiles: Flow = dataStore.data.map { preferences -> - preferences[deleteImageFilesKey] ?: true + preferences[deleteImageFilesKey] != false } suspend fun saveDeleteImageFiles(isChecked: Boolean) { @@ -277,7 +276,7 @@ class DataStore(context: Context) { private val clipboardCleanKey = booleanPreferencesKey(name = DataStoreNamesConstants.DATA_STORE_CLIPBOARD_CLEAN) val clipboardClean: Flow = dataStore.data.map { preferences -> - preferences[clipboardCleanKey] ?: false + preferences[clipboardCleanKey] == true } suspend fun saveClipboardClean(isChecked: Boolean) { @@ -290,7 +289,7 @@ class DataStore(context: Context) { private val usageAndDiagnosticsKey = booleanPreferencesKey(name = DataStoreNamesConstants.DATA_STORE_USAGE_AND_DIAGNOSTICS) val usageAndDiagnostics: Flow = dataStore.data.map { preferences -> - preferences[usageAndDiagnosticsKey] ?: true + preferences[usageAndDiagnosticsKey] != false } suspend fun saveUsageAndDiagnostics(isChecked: Boolean) { @@ -302,7 +301,7 @@ class DataStore(context: Context) { // Ads private val adsKey = booleanPreferencesKey(name = DataStoreNamesConstants.DATA_STORE_ADS) val ads: Flow = dataStore.data.map { preferences -> - preferences[adsKey] ?: true + preferences[adsKey] != false } suspend fun saveAds(isChecked: Boolean) { diff --git a/app/src/main/kotlin/com/d4rk/cleaner/ui/components/navigation/NavigationDrawer.kt b/app/src/main/kotlin/com/d4rk/cleaner/ui/components/navigation/NavigationDrawer.kt index 2be9085..44ca4a6 100644 --- a/app/src/main/kotlin/com/d4rk/cleaner/ui/components/navigation/NavigationDrawer.kt +++ b/app/src/main/kotlin/com/d4rk/cleaner/ui/components/navigation/NavigationDrawer.kt @@ -39,189 +39,189 @@ import androidx.navigation.NavHostController import com.d4rk.cleaner.R import com.d4rk.cleaner.data.datastore.DataStore import com.d4rk.cleaner.data.model.ui.navigation.NavigationDrawerItem +import com.d4rk.cleaner.ui.components.animations.bounceClick +import com.d4rk.cleaner.ui.components.animations.hapticDrawerSwipe import com.d4rk.cleaner.ui.screens.help.HelpActivity import com.d4rk.cleaner.ui.screens.imageoptimizer.imagepicker.ImagePickerActivity import com.d4rk.cleaner.ui.screens.support.SupportActivity -import com.d4rk.cleaner.utils.IntentUtils -import com.d4rk.cleaner.ui.components.animations.bounceClick -import com.d4rk.cleaner.ui.components.animations.hapticDrawerSwipe import com.d4rk.cleaner.ui.screens.trash.TrashActivity +import com.d4rk.cleaner.utils.IntentUtils import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.launch @OptIn(ExperimentalMaterial3Api::class) @Composable fun NavigationDrawer( - navHostController : NavHostController , - drawerState : DrawerState , - view : View , - dataStore : DataStore , - context : Context , + navHostController: NavHostController, + drawerState: DrawerState, + view: View, + dataStore: DataStore, + context: Context, ) { - val scope : CoroutineScope = rememberCoroutineScope() + val scope: CoroutineScope = rememberCoroutineScope() - val drawerItems : List = listOf( + val drawerItems: List = listOf( NavigationDrawerItem( - title = R.string.image_optimizer , selectedIcon = Icons.Outlined.Image - ) , + title = R.string.image_optimizer, selectedIcon = Icons.Outlined.Image + ), NavigationDrawerItem( - title = R.string.trash , selectedIcon = Icons.Outlined.DeleteOutline - ) , + title = R.string.trash, selectedIcon = Icons.Outlined.DeleteOutline + ), NavigationDrawerItem( - title = R.string.settings , - selectedIcon = Icons.Outlined.Settings , - ) , + title = R.string.settings, + selectedIcon = Icons.Outlined.Settings, + ), NavigationDrawerItem( - title = R.string.help_and_feedback , - selectedIcon = Icons.AutoMirrored.Outlined.HelpOutline , - ) , + title = R.string.help_and_feedback, + selectedIcon = Icons.AutoMirrored.Outlined.HelpOutline, + ), NavigationDrawerItem( - title = R.string.updates , - selectedIcon = Icons.AutoMirrored.Outlined.EventNote , - ) , + title = R.string.updates, + selectedIcon = Icons.AutoMirrored.Outlined.EventNote, + ), NavigationDrawerItem( - title = R.string.share , selectedIcon = Icons.Outlined.Share - ) , + title = R.string.share, selectedIcon = Icons.Outlined.Share + ), ) - val selectedItemIndex : Int by rememberSaveable { mutableIntStateOf(value = - 1) } + val selectedItemIndex: Int by rememberSaveable { mutableIntStateOf(value = -1) } - ModalNavigationDrawer(modifier = Modifier.hapticDrawerSwipe(drawerState) , - drawerState = drawerState , - drawerContent = { - ModalDrawerSheet { - Spacer(modifier = Modifier.height(16.dp)) - drawerItems.forEachIndexed { index , item -> - val title : String = stringResource(id = item.title) - NavigationDrawerItem(label = { Text(text = title) } , - selected = index == selectedItemIndex , - onClick = { - when (item.title) { - R.string.image_optimizer -> { - view.playSoundEffect( - SoundEffectConstants.CLICK - ) - IntentUtils.openActivity( - context , - ImagePickerActivity::class.java - ) - } + ModalNavigationDrawer(modifier = Modifier.hapticDrawerSwipe(drawerState), + drawerState = drawerState, + drawerContent = { + ModalDrawerSheet { + Spacer(modifier = Modifier.height(16.dp)) + drawerItems.forEachIndexed { index, item -> + val title: String = stringResource(id = item.title) + NavigationDrawerItem(label = { Text(text = title) }, + selected = index == selectedItemIndex, + onClick = { + when (item.title) { + R.string.image_optimizer -> { + view.playSoundEffect( + SoundEffectConstants.CLICK + ) + IntentUtils.openActivity( + context, + ImagePickerActivity::class.java + ) + } - R.string.trash -> { - view.playSoundEffect( - SoundEffectConstants.CLICK - ) - IntentUtils.openActivity( - context , - TrashActivity::class.java - ) - } + R.string.trash -> { + view.playSoundEffect( + SoundEffectConstants.CLICK + ) + IntentUtils.openActivity( + context, + TrashActivity::class.java + ) + } - R.string.settings -> { - view.playSoundEffect( - SoundEffectConstants.CLICK - ) - IntentUtils.openActivity( - context , - _root_ide_package_.com.d4rk.cleaner.ui.screens.settings.SettingsActivity::class.java - ) - } + R.string.settings -> { + view.playSoundEffect( + SoundEffectConstants.CLICK + ) + IntentUtils.openActivity( + context, + com.d4rk.cleaner.ui.screens.settings.SettingsActivity::class.java + ) + } - R.string.help_and_feedback -> { - view.playSoundEffect( - SoundEffectConstants.CLICK - ) - IntentUtils.openActivity( - context , - HelpActivity::class.java - ) - } + R.string.help_and_feedback -> { + view.playSoundEffect( + SoundEffectConstants.CLICK + ) + IntentUtils.openActivity( + context, + HelpActivity::class.java + ) + } - R.string.updates -> { - view.playSoundEffect( - SoundEffectConstants.CLICK - ) - IntentUtils.openUrl( - context , - url = "https://github.com/D4rK7355608/${context.packageName}/blob/master/CHANGELOG.md" - ) - } + R.string.updates -> { + view.playSoundEffect( + SoundEffectConstants.CLICK + ) + IntentUtils.openUrl( + context, + url = "https://github.com/D4rK7355608/${context.packageName}/blob/master/CHANGELOG.md" + ) + } - R.string.share -> { - view.playSoundEffect( - SoundEffectConstants.CLICK - ) - IntentUtils.shareApp(context) - } - } - scope.launch { drawerState.close() } - } , - icon = { - Icon( - item.selectedIcon , - contentDescription = title - ) - } , - badge = { - item.badgeCount?.let { Text(text = it.toString()) } - } , - modifier = Modifier - .padding( - NavigationDrawerItemDefaults.ItemPadding - ) - .bounceClick()) - if (item.title == R.string.trash) { - HorizontalDivider(modifier = Modifier.padding(8.dp)) - } - } - } - } , - content = { - Scaffold(topBar = { - TopAppBar(title = { Text(text = stringResource(id = R.string.app_name)) } , - navigationIcon = { - IconButton(modifier = Modifier.bounceClick() , - onClick = { - view.playSoundEffect( - SoundEffectConstants.CLICK - ) - scope.launch { - drawerState.apply { - if (isClosed) open() else close() - } - } - }) { - Icon( - imageVector = Icons.Default.Menu , - contentDescription = stringResource(id = R.string.navigation_drawer_open) - ) - } - } , - actions = { - IconButton(modifier = Modifier.bounceClick() , - onClick = { - view.playSoundEffect( - SoundEffectConstants.CLICK - ) - IntentUtils.openActivity( - context , - SupportActivity::class.java - ) - }) { - Icon( - Icons.Outlined.VolunteerActivism , - contentDescription = stringResource(id = R.string.support_us) - ) - } - }) - } , bottomBar = { - BottomNavBar(navHostController , dataStore , view) - }) { paddingValues -> - NavigationHost( - navHostController = navHostController , // FIXME: Cannot find a parameter with this name: navController - dataStore = dataStore , - paddingValues = paddingValues // FIXME: No value passed for parameter 'navHostController' - ) - } - }) + R.string.share -> { + view.playSoundEffect( + SoundEffectConstants.CLICK + ) + IntentUtils.shareApp(context) + } + } + scope.launch { drawerState.close() } + }, + icon = { + Icon( + item.selectedIcon, + contentDescription = title + ) + }, + badge = { + item.badgeCount?.let { Text(text = it.toString()) } + }, + modifier = Modifier + .padding( + NavigationDrawerItemDefaults.ItemPadding + ) + .bounceClick()) + if (item.title == R.string.trash) { + HorizontalDivider(modifier = Modifier.padding(8.dp)) + } + } + } + }, + content = { + Scaffold(topBar = { + TopAppBar(title = { Text(text = stringResource(id = R.string.app_name)) }, + navigationIcon = { + IconButton(modifier = Modifier.bounceClick(), + onClick = { + view.playSoundEffect( + SoundEffectConstants.CLICK + ) + scope.launch { + drawerState.apply { + if (isClosed) open() else close() + } + } + }) { + Icon( + imageVector = Icons.Default.Menu, + contentDescription = stringResource(id = R.string.navigation_drawer_open) + ) + } + }, + actions = { + IconButton(modifier = Modifier.bounceClick(), + onClick = { + view.playSoundEffect( + SoundEffectConstants.CLICK + ) + IntentUtils.openActivity( + context, + SupportActivity::class.java + ) + }) { + Icon( + Icons.Outlined.VolunteerActivism, + contentDescription = stringResource(id = R.string.support_us) + ) + } + }) + }, bottomBar = { + BottomNavBar(navHostController, dataStore, view) + }) { paddingValues -> + NavigationHost( + navHostController = navHostController, + dataStore = dataStore, + paddingValues = paddingValues + ) + } + }) } \ No newline at end of file diff --git a/app/src/main/kotlin/com/d4rk/cleaner/ui/screens/home/HomeScreen.kt b/app/src/main/kotlin/com/d4rk/cleaner/ui/screens/home/HomeScreen.kt index 8132366..56eace2 100644 --- a/app/src/main/kotlin/com/d4rk/cleaner/ui/screens/home/HomeScreen.kt +++ b/app/src/main/kotlin/com/d4rk/cleaner/ui/screens/home/HomeScreen.kt @@ -95,52 +95,52 @@ import java.util.Locale @Composable fun HomeScreen() { - val context : Context = LocalContext.current - val viewModel : HomeViewModel = viewModel() - val uiState : UiHomeModel by viewModel.uiState.collectAsState() - val uiErrorModel : UiErrorModel by viewModel.uiErrorModel.collectAsState() - val imageLoader : ImageLoader = remember { + val context: Context = LocalContext.current + val viewModel: HomeViewModel = viewModel() + val uiState: UiHomeModel by viewModel.uiState.collectAsState() + val uiErrorModel: UiErrorModel by viewModel.uiErrorModel.collectAsState() + val imageLoader: ImageLoader = remember { ImageLoader.Builder(context).memoryCache { MemoryCache.Builder(context).maxSizePercent(percent = 0.24).build() }.diskCache { DiskCache.Builder().directory(context.cacheDir.resolve(relative = "image_cache")) - .maxSizePercent(percent = 0.02).build() + .maxSizePercent(percent = 0.02).build() }.build() } LaunchedEffect(Unit) { - if (! PermissionsUtils.hasStoragePermissions(context)) { + if (!PermissionsUtils.hasStoragePermissions(context)) { PermissionsUtils.requestStoragePermissions(context as Activity) } } if (uiErrorModel.showErrorDialog) { - ErrorAlertDialog(errorMessage = uiErrorModel.errorMessage , - onDismiss = { viewModel.dismissErrorDialog() }) + ErrorAlertDialog(errorMessage = uiErrorModel.errorMessage, + onDismiss = { viewModel.dismissErrorDialog() }) } Column(modifier = Modifier.fillMaxSize()) { Box( modifier = Modifier - .weight(4f) - .fillMaxWidth() + .weight(4f) + .fillMaxWidth() ) { - if (! uiState.showCleaningComposable) { - CircularDeterminateIndicator(progress = uiState.progress , - modifier = Modifier - .align(Alignment.TopCenter) - .offset(y = 98.dp) , - onClick = { viewModel.analyze() }) + if (!uiState.showCleaningComposable) { + CircularDeterminateIndicator(progress = uiState.progress, + modifier = Modifier + .align(Alignment.TopCenter) + .offset(y = 98.dp), + onClick = { viewModel.analyze() }) } Crossfade( - targetState = uiState.showCleaningComposable , - animationSpec = tween(durationMillis = 300) , + targetState = uiState.showCleaningComposable, + animationSpec = tween(durationMillis = 300), label = "" ) { showCleaningComposable -> if (showCleaningComposable) { - AnalyzeComposable(imageLoader = imageLoader , context = context) + AnalyzeComposable(imageLoader = imageLoader, context = context) } } } @@ -148,26 +148,26 @@ fun HomeScreen() { } @Composable -fun AnalyzeComposable(imageLoader : ImageLoader , context : Context) { - val viewModel : HomeViewModel = viewModel() - val uiState : UiHomeModel by viewModel.uiState.collectAsState() - val coroutineScope : CoroutineScope = rememberCoroutineScope() +fun AnalyzeComposable(imageLoader: ImageLoader, context: Context) { + val viewModel: HomeViewModel = viewModel() + val uiState: UiHomeModel by viewModel.uiState.collectAsState() + val coroutineScope: CoroutineScope = rememberCoroutineScope() val enabled = uiState.selectedFileCount > 0 val apkExtensions = remember { context.resources.getStringArray(R.array.apk_extensions) } val imageExtensions = remember { context.resources.getStringArray(R.array.image_extensions) } val videoExtensions = remember { context.resources.getStringArray(R.array.video_extensions) } val audioExtensions = remember { context.resources.getStringArray(R.array.audio_extensions) } val archiveExtensions = - remember { context.resources.getStringArray(R.array.archive_extensions) } + remember { context.resources.getStringArray(R.array.archive_extensions) } - val filesTypesStrings : List = stringArrayResource(R.array.file_types_titles).toList() + val filesTypesStrings: List = stringArrayResource(R.array.file_types_titles).toList() val groupedFiles = remember( - uiState.scannedFiles , - apkExtensions , - imageExtensions , - videoExtensions , - audioExtensions , + uiState.scannedFiles, + apkExtensions, + imageExtensions, + videoExtensions, + audioExtensions, archiveExtensions ) { uiState.scannedFiles.groupBy { file -> @@ -201,45 +201,45 @@ fun AnalyzeComposable(imageLoader : ImageLoader , context : Context) { Column( modifier = Modifier - .animateContentSize() - .fillMaxWidth() - .padding(16.dp) , + .animateContentSize() + .fillMaxWidth() + .padding(16.dp), horizontalAlignment = Alignment.End ) { OutlinedCard( modifier = Modifier - .weight(1f) - .fillMaxWidth() , + .weight(1f) + .fillMaxWidth(), ) { when { uiState.scannedFiles.isEmpty() -> { - Box(modifier = Modifier.fillMaxSize() , contentAlignment = Alignment.Center) { + Box(modifier = Modifier.fillMaxSize(), contentAlignment = Alignment.Center) { CircularProgressIndicator() } } uiState.noFilesFound -> { - Box(modifier = Modifier.fillMaxSize() , contentAlignment = Alignment.Center) { + Box(modifier = Modifier.fillMaxSize(), contentAlignment = Alignment.Center) { Column(horizontalAlignment = Alignment.CenterHorizontally) { Icon( - imageVector = Icons.Outlined.FolderOff , - contentDescription = null , - modifier = Modifier.size(64.dp) , + imageVector = Icons.Outlined.FolderOff, + contentDescription = null, + modifier = Modifier.size(64.dp), tint = MaterialTheme.colorScheme.onSurface ) Spacer(modifier = Modifier.height(16.dp)) Text( - text = stringResource(id = R.string.no_files_found) , - style = MaterialTheme.typography.bodyLarge , + text = stringResource(id = R.string.no_files_found), + style = MaterialTheme.typography.bodyLarge, color = MaterialTheme.colorScheme.onSurface ) - OutlinedButton(modifier = Modifier.bounceClick() , onClick = { + OutlinedButton(modifier = Modifier.bounceClick(), onClick = { viewModel.rescanFiles() }) { Icon( - modifier = Modifier.size(ButtonDefaults.IconSize) , - imageVector = Icons.Outlined.Refresh , + modifier = Modifier.size(ButtonDefaults.IconSize), + imageVector = Icons.Outlined.Refresh, contentDescription = "Close" ) Spacer(modifier = Modifier.size(ButtonDefaults.IconSpacing)) @@ -251,64 +251,64 @@ fun AnalyzeComposable(imageLoader : ImageLoader , context : Context) { else -> { val tabs = groupedFiles.keys.toList() - val pagerState : PagerState = rememberPagerState(pageCount = { tabs.size }) + val pagerState: PagerState = rememberPagerState(pageCount = { tabs.size }) Row( - modifier = Modifier.fillMaxWidth() , + modifier = Modifier.fillMaxWidth(), verticalAlignment = Alignment.CenterVertically ) { ScrollableTabRow( - selectedTabIndex = pagerState.currentPage , - modifier = Modifier.weight(1f) , - edgePadding = 0.dp , + selectedTabIndex = pagerState.currentPage, + modifier = Modifier.weight(1f), + edgePadding = 0.dp, indicator = { tabPositions -> TabRowDefaults.PrimaryIndicator( - modifier = Modifier.tabIndicatorOffset(tabPositions[pagerState.currentPage]) , + modifier = Modifier.tabIndicatorOffset(tabPositions[pagerState.currentPage]), shape = RoundedCornerShape( - topStart = 3.dp , - topEnd = 3.dp , - bottomEnd = 0.dp , - bottomStart = 0.dp , - ) , + topStart = 3.dp, + topEnd = 3.dp, + bottomEnd = 0.dp, + bottomStart = 0.dp, + ), ) - } , + }, ) { - tabs.forEachIndexed { index , title -> - Tab(modifier = Modifier.bounceClick() , - selected = pagerState.currentPage == index , + tabs.forEachIndexed { index, title -> + Tab(modifier = Modifier.bounceClick(), + selected = pagerState.currentPage == index, onClick = { coroutineScope.launch { pagerState.animateScrollToPage(index) } - } , + }, text = { Text(text = title) }) } } - IconButton(modifier = Modifier.bounceClick() , onClick = { + IconButton(modifier = Modifier.bounceClick(), onClick = { viewModel.onCloseAnalyzeComposable() }) { - Icon(imageVector = Icons.Outlined.Close , contentDescription = "Close") + Icon(imageVector = Icons.Outlined.Close, contentDescription = "Close") } } HorizontalPager( - modifier = Modifier.hapticPagerSwipe(pagerState) , - state = pagerState , + modifier = Modifier.hapticPagerSwipe(pagerState), + state = pagerState, ) { page -> val filesForCurrentPage = groupedFiles[tabs[page]] ?: emptyList() val filesByDate = filesForCurrentPage.groupBy { file -> SimpleDateFormat( - "yyyy-MM-dd" , Locale.getDefault() + "yyyy-MM-dd", Locale.getDefault() ).format(Date(file.lastModified())) } FilesByDateSection( modifier = Modifier, - filesByDate = filesByDate , - fileSelectionStates = uiState.fileSelectionStates , - imageLoader = imageLoader , + filesByDate = filesByDate, + fileSelectionStates = uiState.fileSelectionStates, + imageLoader = imageLoader, onFileSelectionChange = viewModel::onFileSelectionChange ) } @@ -317,27 +317,30 @@ fun AnalyzeComposable(imageLoader : ImageLoader , context : Context) { } if (uiState.scannedFiles.isNotEmpty()) { Row( - modifier = Modifier.fillMaxWidth() , - verticalAlignment = Alignment.CenterVertically , - horizontalArrangement = Arrangement.SpaceBetween , + modifier = Modifier.fillMaxWidth(), + verticalAlignment = Alignment.CenterVertically, + horizontalArrangement = Arrangement.SpaceBetween, ) { val statusText: String = if (uiState.selectedFileCount > 0) { - pluralStringResource(id = R.plurals.status_selected_files, count = uiState.selectedFileCount, uiState.selectedFileCount) + pluralStringResource( + id = R.plurals.status_selected_files, + count = uiState.selectedFileCount, + uiState.selectedFileCount + ) } else { stringResource(id = R.string.status_no_files_selected) } - val statusColor : Color by animateColorAsState( + val statusColor: Color by animateColorAsState( targetValue = if (uiState.selectedFileCount > 0) { MaterialTheme.colorScheme.primary - } - else { + } else { MaterialTheme.colorScheme.secondary - } , animationSpec = tween() , label = "Selected Files Status Color Animation" + }, animationSpec = tween(), label = "Selected Files Status Color Animation" ) Text( - text = statusText , - color = statusColor , + text = statusText, + color = statusColor, modifier = Modifier.animateContentSize() ) SelectAllComposable(viewModel) @@ -345,17 +348,17 @@ fun AnalyzeComposable(imageLoader : ImageLoader , context : Context) { TwoRowButtons( modifier = Modifier, - enabled = enabled , + enabled = enabled, onStartButtonClick = { viewModel.moveToTrash() - } , - onStartButtonIcon = Icons.Outlined.Delete , - onStartButtonText = R.string.move_to_trash , + }, + onStartButtonIcon = Icons.Outlined.Delete, + onStartButtonText = R.string.move_to_trash, onEndButtonClick = { viewModel.clean() - } , - onEndButtonIcon = Icons.Outlined.DeleteForever , + }, + onEndButtonIcon = Icons.Outlined.DeleteForever, onEndButtonText = R.string.delete_forever) } } @@ -363,33 +366,33 @@ fun AnalyzeComposable(imageLoader : ImageLoader , context : Context) { @Composable fun FilesByDateSection( - modifier : Modifier, - filesByDate : Map> , - fileSelectionStates : Map , - imageLoader : ImageLoader , - onFileSelectionChange : (File , Boolean) -> Unit , + modifier: Modifier, + filesByDate: Map>, + fileSelectionStates: Map, + imageLoader: ImageLoader, + onFileSelectionChange: (File, Boolean) -> Unit, ) { LazyColumn( modifier = modifier.fillMaxSize() ) { val sortedDates = filesByDate.keys.sortedByDescending { dateString -> - SimpleDateFormat("yyyy-MM-dd" , Locale.getDefault()).parse(dateString) + SimpleDateFormat("yyyy-MM-dd", Locale.getDefault()).parse(dateString) } sortedDates.forEach { date -> val files = filesByDate[date] ?: emptyList() item(key = date) { DateHeader( - files = files , - fileSelectionStates = fileSelectionStates , + files = files, + fileSelectionStates = fileSelectionStates, onFileSelectionChange = onFileSelectionChange ) } item(key = "$date-grid") { FilesGrid( - files = files , - imageLoader = imageLoader , - fileSelectionStates = fileSelectionStates , + files = files, + imageLoader = imageLoader, + fileSelectionStates = fileSelectionStates, onFileSelectionChange = onFileSelectionChange ) } @@ -399,73 +402,73 @@ fun FilesByDateSection( @Composable fun DateHeader( - files : List , - fileSelectionStates : Map , - onFileSelectionChange : (File , Boolean) -> Unit , + files: List, + fileSelectionStates: Map, + onFileSelectionChange: (File, Boolean) -> Unit, ) { Row( modifier = Modifier - .fillMaxWidth() - .padding(horizontal = 8.dp , vertical = 4.dp) , - verticalAlignment = Alignment.CenterVertically , + .fillMaxWidth() + .padding(horizontal = 8.dp, vertical = 4.dp), + verticalAlignment = Alignment.CenterVertically, horizontalArrangement = Arrangement.SpaceBetween ) { Text( - modifier = Modifier.padding(start = 8.dp) , + modifier = Modifier.padding(start = 8.dp), text = TimeHelper.formatDate(Date(files[0].lastModified())) ) val allFilesForDateSelected = files.all { fileSelectionStates[it] == true } - Checkbox(modifier = Modifier.bounceClick() , - checked = allFilesForDateSelected , - onCheckedChange = { isChecked -> - files.forEach { file -> - onFileSelectionChange(file , isChecked) - } - }) + Checkbox(modifier = Modifier.bounceClick(), + checked = allFilesForDateSelected, + onCheckedChange = { isChecked -> + files.forEach { file -> + onFileSelectionChange(file, isChecked) + } + }) } } @Composable fun FilesGrid( - files : List , - imageLoader : ImageLoader , - fileSelectionStates : Map , - onFileSelectionChange : (File , Boolean) -> Unit , + files: List, + imageLoader: ImageLoader, + fileSelectionStates: Map, + onFileSelectionChange: (File, Boolean) -> Unit, ) { Box( modifier = Modifier.fillMaxSize() ) { NonLazyGrid( - columns = 3 , itemCount = files.size , modifier = Modifier.padding(horizontal = 8.dp) + columns = 3, itemCount = files.size, modifier = Modifier.padding(horizontal = 8.dp) ) { index -> val file = files[index] - FileCard(file = file , - imageLoader = imageLoader , - isChecked = fileSelectionStates[file] ?: false , - onCheckedChange = { isChecked -> onFileSelectionChange(file , isChecked) }) + FileCard(file = file, + imageLoader = imageLoader, + isChecked = fileSelectionStates[file] == true, + onCheckedChange = { isChecked -> onFileSelectionChange(file, isChecked) }) } } } @Composable fun FileCard( - file : File , imageLoader : ImageLoader , onCheckedChange : (Boolean) -> Unit , - isChecked : Boolean , + file: File, imageLoader: ImageLoader, onCheckedChange: (Boolean) -> Unit, + isChecked: Boolean, ) { - val context : Context = LocalContext.current - val fileExtension : String = remember(file.name) { getFileExtension(file.name) } + val context: Context = LocalContext.current + val fileExtension: String = remember(file.name) { getFileExtension(file.name) } val imageExtensions = - remember { context.resources.getStringArray(R.array.image_extensions).toList() } + remember { context.resources.getStringArray(R.array.image_extensions).toList() } val videoExtensions = - remember { context.resources.getStringArray(R.array.video_extensions).toList() } + remember { context.resources.getStringArray(R.array.video_extensions).toList() } Card( modifier = Modifier - .fillMaxWidth() - .aspectRatio(ratio = 1f) - .bounceClick() , + .fillMaxWidth() + .aspectRatio(ratio = 1f) + .bounceClick(), ) { Box(modifier = Modifier.fillMaxSize()) { when (fileExtension) { @@ -473,62 +476,62 @@ fun FileCard( AsyncImage( model = remember(file) { ImageRequest.Builder(context).data(file).size(64) - .crossfade(enable = true).build() - } , - imageLoader = imageLoader , - contentDescription = file.name , - contentScale = ContentScale.Crop , - modifier = Modifier.fillMaxSize() , + .crossfade(enable = true).build() + }, + imageLoader = imageLoader, + contentDescription = file.name, + contentScale = ContentScale.Crop, + modifier = Modifier.fillMaxSize(), ) } in videoExtensions -> { AsyncImage(model = remember(file) { ImageRequest.Builder(context).data(file) - .decoderFactory { result , options , _ -> - VideoFrameDecoder(result.source , options) - }.videoFramePercent(framePercent = 0.5).crossfade(enable = true) - .build() - } , - imageLoader = imageLoader , - contentDescription = file.name , - contentScale = ContentScale.Crop , - modifier = Modifier.fillMaxSize()) + .decoderFactory { result, options, _ -> + VideoFrameDecoder(result.source, options) + }.videoFramePercent(framePercent = 0.5).crossfade(enable = true) + .build() + }, + imageLoader = imageLoader, + contentDescription = file.name, + contentScale = ContentScale.Crop, + modifier = Modifier.fillMaxSize()) } else -> { val fileIcon = remember(fileExtension) { getFileIcon( - fileExtension , context + fileExtension, context ) } Icon( - painter = painterResource(fileIcon) , - contentDescription = null , + painter = painterResource(fileIcon), + contentDescription = null, modifier = Modifier - .size(24.dp) - .align(Alignment.Center) + .size(24.dp) + .align(Alignment.Center) ) } } Checkbox( - checked = isChecked , - onCheckedChange = onCheckedChange , + checked = isChecked, + onCheckedChange = onCheckedChange, modifier = Modifier.align(Alignment.TopEnd) ) Text( - text = file.name , - maxLines = 1 , - overflow = TextOverflow.Ellipsis , + text = file.name, + maxLines = 1, + overflow = TextOverflow.Ellipsis, modifier = Modifier - .fillMaxWidth() - .background( - color = Color.Black.copy(alpha = 0.4f) - ) - .padding(8.dp) - .align(Alignment.BottomCenter) + .fillMaxWidth() + .background( + color = Color.Black.copy(alpha = 0.4f) + ) + .padding(8.dp) + .align(Alignment.BottomCenter) ) } } @@ -545,38 +548,38 @@ fun FileCard( */ @Composable fun SelectAllComposable( - viewModel : HomeViewModel , + viewModel: HomeViewModel, ) { - val uiState : UiHomeModel by viewModel.uiState.collectAsState() + val uiState: UiHomeModel by viewModel.uiState.collectAsState() Row( modifier = Modifier - .fillMaxWidth() - .animateContentSize() , - verticalAlignment = Alignment.CenterVertically , + .fillMaxWidth() + .animateContentSize(), + verticalAlignment = Alignment.CenterVertically, horizontalArrangement = Arrangement.End ) { - val interactionSource : MutableInteractionSource = remember { MutableInteractionSource() } + val interactionSource: MutableInteractionSource = remember { MutableInteractionSource() } FilterChip( - modifier = Modifier.bounceClick() , - selected = uiState.allFilesSelected , + modifier = Modifier.bounceClick(), + selected = uiState.allFilesSelected, onClick = { viewModel.toggleSelectAllFiles() - } , - label = { Text(stringResource(id = R.string.select_all)) } , + }, + label = { Text(stringResource(id = R.string.select_all)) }, leadingIcon = { AnimatedContent( - targetState = uiState.allFilesSelected , label = "Checkmark Animation" + targetState = uiState.allFilesSelected, label = "Checkmark Animation" ) { targetChecked -> if (targetChecked) { Icon( - imageVector = Icons.Filled.Check , - contentDescription = null , + imageVector = Icons.Filled.Check, + contentDescription = null, ) } } - } , - interactionSource = interactionSource , + }, + interactionSource = interactionSource, ) } } \ No newline at end of file diff --git a/app/src/main/kotlin/com/d4rk/cleaner/ui/screens/imageoptimizer/imagepicker/ImagePickerActivity.kt b/app/src/main/kotlin/com/d4rk/cleaner/ui/screens/imageoptimizer/imagepicker/ImagePickerActivity.kt index ac6ebb4..bacbabd 100644 --- a/app/src/main/kotlin/com/d4rk/cleaner/ui/screens/imageoptimizer/imagepicker/ImagePickerActivity.kt +++ b/app/src/main/kotlin/com/d4rk/cleaner/ui/screens/imageoptimizer/imagepicker/ImagePickerActivity.kt @@ -1,6 +1,5 @@ package com.d4rk.cleaner.ui.screens.imageoptimizer.imagepicker -import android.app.Activity import android.content.Intent import android.net.Uri import android.os.Build @@ -18,30 +17,30 @@ import androidx.compose.ui.Modifier import com.d4rk.cleaner.ui.screens.settings.display.theme.style.AppTheme class ImagePickerActivity : AppCompatActivity() { - private val viewModel : ImagePickerViewModel by viewModels() + private val viewModel: ImagePickerViewModel by viewModels() private val pickMediaLauncher = - registerForActivityResult(ActivityResultContracts.PickVisualMedia()) { uri -> - viewModel.setSelectedImageUri(uri) - } + registerForActivityResult(ActivityResultContracts.PickVisualMedia()) { uri -> + viewModel.setSelectedImageUri(uri) + } private val openDocumentLauncher = - registerForActivityResult(ActivityResultContracts.StartActivityForResult()) { result -> - if (result.resultCode == Activity.RESULT_OK && result.data != null) { - val selectedImageUri : Uri? = result.data?.data - viewModel.setSelectedImageUri(selectedImageUri) - } + registerForActivityResult(ActivityResultContracts.StartActivityForResult()) { result -> + if (result.resultCode == RESULT_OK && result.data != null) { + val selectedImageUri: Uri? = result.data?.data + viewModel.setSelectedImageUri(selectedImageUri) } + } - override fun onCreate(savedInstanceState : Bundle?) { + override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) enableEdgeToEdge() setContent { AppTheme { Surface( - modifier = Modifier.fillMaxSize() , color = MaterialTheme.colorScheme.background + modifier = Modifier.fillMaxSize(), color = MaterialTheme.colorScheme.background ) { - ImagePickerComposable(activity = this@ImagePickerActivity , viewModel) + ImagePickerComposable(activity = this@ImagePickerActivity, viewModel) } } } @@ -50,8 +49,7 @@ class ImagePickerActivity : AppCompatActivity() { fun selectImage() { if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) { pickMediaLauncher.launch(PickVisualMediaRequest(ActivityResultContracts.PickVisualMedia.ImageOnly)) - } - else { + } else { val pickIntent = Intent(Intent.ACTION_OPEN_DOCUMENT).apply { type = "image/*" addCategory(Intent.CATEGORY_OPENABLE) diff --git a/app/src/main/kotlin/com/d4rk/cleaner/ui/screens/memory/MemoryManagerScreen.kt b/app/src/main/kotlin/com/d4rk/cleaner/ui/screens/memory/MemoryManagerScreen.kt index 7273a5b..9debee5 100644 --- a/app/src/main/kotlin/com/d4rk/cleaner/ui/screens/memory/MemoryManagerScreen.kt +++ b/app/src/main/kotlin/com/d4rk/cleaner/ui/screens/memory/MemoryManagerScreen.kt @@ -74,45 +74,45 @@ import com.d4rk.cleaner.data.model.ui.error.UiErrorModel import com.d4rk.cleaner.data.model.ui.memorymanager.RamInfo import com.d4rk.cleaner.data.model.ui.memorymanager.StorageInfo import com.d4rk.cleaner.data.model.ui.screens.UiMemoryManagerModel +import com.d4rk.cleaner.ui.components.StorageProgressBar +import com.d4rk.cleaner.ui.components.animations.bounceClick +import com.d4rk.cleaner.ui.components.animations.hapticPagerSwipe import com.d4rk.cleaner.ui.components.dialogs.ErrorAlertDialog import com.d4rk.cleaner.utils.PermissionsUtils import com.d4rk.cleaner.utils.cleaning.StorageUtils.formatSize -import com.d4rk.cleaner.ui.components.animations.bounceClick -import com.d4rk.cleaner.ui.components.StorageProgressBar -import com.d4rk.cleaner.ui.components.animations.hapticPagerSwipe import kotlin.math.absoluteValue @Composable fun MemoryManagerComposable() { - val viewModel : MemoryManagerViewModel = viewModel() - val uiErrorModel : UiErrorModel by viewModel.uiErrorModel.collectAsState() - val uiState : UiMemoryManagerModel by viewModel.uiMemoryManagerModel.collectAsState() - val isLoading : Boolean by viewModel.isLoading.collectAsState() - val context : Context = LocalContext.current - val view : View = LocalView.current + val viewModel: MemoryManagerViewModel = viewModel() + val uiErrorModel: UiErrorModel by viewModel.uiErrorModel.collectAsState() + val uiState: UiMemoryManagerModel by viewModel.uiMemoryManagerModel.collectAsState() + val isLoading: Boolean by viewModel.isLoading.collectAsState() + val context: Context = LocalContext.current + val view: View = LocalView.current - val transition : Transition = - updateTransition(targetState = ! isLoading , label = "LoadingTransition") + val transition: Transition = + updateTransition(targetState = !isLoading, label = "LoadingTransition") - val progressAlpha : Float by transition.animateFloat(label = "Progress Alpha") { + val progressAlpha: Float by transition.animateFloat(label = "Progress Alpha") { if (it) 0f else 1f } - val contentAlpha : Float by transition.animateFloat(label = "Content Alpha") { + val contentAlpha: Float by transition.animateFloat(label = "Content Alpha") { if (it) 1f else 0f } - val pagerState : PagerState = rememberPagerState { 2 } + val pagerState: PagerState = rememberPagerState { 2 } if (uiErrorModel.showErrorDialog) { - ErrorAlertDialog(errorMessage = uiErrorModel.errorMessage , - onDismiss = { viewModel.dismissErrorDialog() }) + ErrorAlertDialog(errorMessage = uiErrorModel.errorMessage, + onDismiss = { viewModel.dismissErrorDialog() }) } LaunchedEffect(Unit) { - if (! PermissionsUtils.hasStoragePermissions(context)) { + if (!PermissionsUtils.hasStoragePermissions(context)) { PermissionsUtils.requestStoragePermissions(context as Activity) } - if (! PermissionsUtils.hasUsageAccessPermissions(context)) { + if (!PermissionsUtils.hasUsageAccessPermissions(context)) { PermissionsUtils.requestUsageAccess(context as Activity) } } @@ -120,23 +120,22 @@ fun MemoryManagerComposable() { if (isLoading) { Box( modifier = Modifier - .fillMaxSize() - .animateContentSize() - .alpha(progressAlpha) , + .fillMaxSize() + .animateContentSize() + .alpha(progressAlpha), contentAlignment = Alignment.Center ) { CircularProgressIndicator() } - } - else { + } else { Column( modifier = Modifier - .fillMaxSize() - .alpha(contentAlpha) + .fillMaxSize() + .alpha(contentAlpha) ) { CarouselLayout( - items = listOf(uiState.storageInfo , uiState.ramInfo) , - peekPreviewWidth = 24.dp , + items = listOf(uiState.storageInfo, uiState.ramInfo), + peekPreviewWidth = 24.dp, pagerState = pagerState ) { item -> when (item) { @@ -148,9 +147,9 @@ fun MemoryManagerComposable() { Spacer(modifier = Modifier.height(16.dp)) DotsIndicator( - modifier = Modifier.align(Alignment.CenterHorizontally) , - totalDots = 2 , - selectedIndex = pagerState.currentPage , + modifier = Modifier.align(Alignment.CenterHorizontally), + totalDots = 2, + selectedIndex = pagerState.currentPage, dotSize = 6.dp ) @@ -158,24 +157,24 @@ fun MemoryManagerComposable() { Row( modifier = Modifier - .fillMaxWidth() - .animateContentSize() - .padding(horizontal = 16.dp) , + .fillMaxWidth() + .animateContentSize() + .padding(horizontal = 16.dp), verticalAlignment = Alignment.CenterVertically ) { Text( - text = stringResource(id = R.string.categories) , - modifier = Modifier.weight(1f) , - style = MaterialTheme.typography.headlineSmall , + text = stringResource(id = R.string.categories), + modifier = Modifier.weight(1f), + style = MaterialTheme.typography.headlineSmall, ) Spacer(modifier = Modifier.width(8.dp)) - IconButton(modifier = Modifier.bounceClick() , onClick = { + IconButton(modifier = Modifier.bounceClick(), onClick = { view.playSoundEffect(SoundEffectConstants.CLICK) viewModel.toggleListExpanded() }) { Icon( - imageVector = if (uiState.listExpanded) Icons.Outlined.ArrowDropDown else Icons.AutoMirrored.Filled.ArrowLeft , + imageVector = if (uiState.listExpanded) Icons.Outlined.ArrowDropDown else Icons.AutoMirrored.Filled.ArrowLeft, contentDescription = if (uiState.listExpanded) "Collapse" else "Expand" ) } @@ -192,105 +191,104 @@ fun MemoryManagerComposable() { @Composable fun CarouselLayout( - items : List , - peekPreviewWidth : Dp , - pagerState : PagerState , - itemContent : @Composable (item : T) -> Unit + items: List, + peekPreviewWidth: Dp, + pagerState: PagerState, + itemContent: @Composable (item: T) -> Unit ) { HorizontalPager( - state = pagerState , + state = pagerState, modifier = Modifier - .fillMaxWidth() - .hapticPagerSwipe(pagerState) , + .fillMaxWidth() + .hapticPagerSwipe(pagerState), contentPadding = PaddingValues(horizontal = peekPreviewWidth) ) { page -> - val pageOffset : Float = (pagerState.currentPage - page).toFloat().absoluteValue + val pageOffset: Float = (pagerState.currentPage - page).toFloat().absoluteValue - val scale : Float by animateFloatAsState( + val scale: Float by animateFloatAsState( targetValue = lerp( - start = 0.95f , stop = 1f , fraction = 1f - pageOffset.coerceIn(0f , 1f) - ) , - animationSpec = tween(durationMillis = 250) , + start = 0.95f, stop = 1f, fraction = 1f - pageOffset.coerceIn(0f, 1f) + ), + animationSpec = tween(durationMillis = 250), label = "Carousel Item Scale Animation" ) - val alpha : Float = - lerp(start = 0.5f , stop = 1f , fraction = 1f - pageOffset.coerceIn(0f , 1f)) + val alpha: Float = + lerp(start = 0.5f, stop = 1f, fraction = 1f - pageOffset.coerceIn(0f, 1f)) Card(modifier = Modifier - .fillMaxWidth() - .graphicsLayer { - scaleX = scale - scaleY = scale - this.alpha = alpha - }) { + .fillMaxWidth() + .graphicsLayer { + scaleX = scale + scaleY = scale + this.alpha = alpha + }) { itemContent(items[page]) } } } @Composable -fun StorageInfoCard(storageInfo : StorageInfo) { +fun StorageInfoCard(storageInfo: StorageInfo) { - val progress : Float = if (storageInfo.totalStorage == 0L) { + val progress: Float = if (storageInfo.totalStorage == 0L) { 0f - } - else { + } else { storageInfo.usedStorage.toFloat() / storageInfo.totalStorage.toFloat() } Column( modifier = Modifier - .padding(16.dp) - .animateContentSize() + .padding(16.dp) + .animateContentSize() ) { Text( - text = stringResource(id = R.string.storage_information) , - style = MaterialTheme.typography.headlineSmall , + text = stringResource(id = R.string.storage_information), + style = MaterialTheme.typography.headlineSmall, fontWeight = FontWeight.Bold ) Spacer(modifier = Modifier.height(8.dp)) LinearProgressIndicator( - progress = { progress } , + progress = { progress }, modifier = Modifier - .fillMaxWidth() - .height(8.dp) , - color = MaterialTheme.colorScheme.primary , + .fillMaxWidth() + .height(8.dp), + color = MaterialTheme.colorScheme.primary, ) Spacer(modifier = Modifier.height(8.dp)) StorageInfoText( - label = stringResource(id = R.string.used) , size = storageInfo.usedStorage + label = stringResource(id = R.string.used), size = storageInfo.usedStorage ) StorageInfoText( - label = stringResource(id = R.string.free) , size = storageInfo.freeStorage + label = stringResource(id = R.string.free), size = storageInfo.freeStorage ) StorageInfoText( - label = stringResource(id = R.string.total) , size = storageInfo.totalStorage + label = stringResource(id = R.string.total), size = storageInfo.totalStorage ) } } @Composable -fun StorageBreakdownGrid(storageBreakdown : Map) { - val items : List> = storageBreakdown.entries.toList() +fun StorageBreakdownGrid(storageBreakdown: Map) { + val items: List> = storageBreakdown.entries.toList() val chunkSize = 2 LazyColumn( modifier = Modifier - .fillMaxWidth() - .animateContentSize() - .padding(horizontal = 16.dp) + .fillMaxWidth() + .animateContentSize() + .padding(horizontal = 16.dp) ) { items( - items = items.chunked(chunkSize) , + items = items.chunked(chunkSize), key = { chunk -> chunk.firstOrNull()?.key ?: "" }) { chunk -> Row( modifier = Modifier - .fillMaxWidth() - .animateContentSize() + .fillMaxWidth() + .animateContentSize() ) { - for (item : Map.Entry in chunk) { - val (icon : String , size : Long) = item - StorageBreakdownItem(icon = icon , size = size , modifier = Modifier.weight(1f)) + for (item: Map.Entry in chunk) { + val (icon: String, size: Long) = item + StorageBreakdownItem(icon = icon, size = size, modifier = Modifier.weight(1f)) } } } @@ -298,37 +296,37 @@ fun StorageBreakdownGrid(storageBreakdown : Map) { } @Composable -fun StorageBreakdownItem(icon : String , size : Long , modifier : Modifier = Modifier) { - val storageIcons : Map = mapOf( - stringResource(id = R.string.installed_apps) to Icons.Outlined.Apps , - stringResource(id = R.string.system) to Icons.Outlined.Android , - stringResource(id = R.string.music) to Icons.Outlined.MusicNote , - stringResource(id = R.string.images) to Icons.Outlined.Image , - stringResource(id = R.string.documents) to Icons.Outlined.FolderOpen , - stringResource(id = R.string.downloads) to Icons.Outlined.Download , - stringResource(id = R.string.other_files) to Icons.Outlined.FolderOpen , +fun StorageBreakdownItem(icon: String, size: Long, modifier: Modifier = Modifier) { + val storageIcons: Map = mapOf( + stringResource(id = R.string.installed_apps) to Icons.Outlined.Apps, + stringResource(id = R.string.system) to Icons.Outlined.Android, + stringResource(id = R.string.music) to Icons.Outlined.MusicNote, + stringResource(id = R.string.images) to Icons.Outlined.Image, + stringResource(id = R.string.documents) to Icons.Outlined.FolderOpen, + stringResource(id = R.string.downloads) to Icons.Outlined.Download, + stringResource(id = R.string.other_files) to Icons.Outlined.FolderOpen, ) Card( modifier = modifier - .padding(vertical = 4.dp , horizontal = 4.dp) - .animateContentSize() + .padding(vertical = 4.dp, horizontal = 4.dp) + .animateContentSize() ) { Row( modifier = Modifier - .fillMaxWidth() - .animateContentSize() - .padding(16.dp) , + .fillMaxWidth() + .animateContentSize() + .padding(16.dp), verticalAlignment = Alignment.CenterVertically ) { Card( - modifier = Modifier.size(48.dp) , - colors = CardDefaults.cardColors(containerColor = MaterialTheme.colorScheme.primaryContainer) , + modifier = Modifier.size(48.dp), + colors = CardDefaults.cardColors(containerColor = MaterialTheme.colorScheme.primaryContainer), ) { - Box(modifier = Modifier.fillMaxSize() , contentAlignment = Alignment.Center) { + Box(modifier = Modifier.fillMaxSize(), contentAlignment = Alignment.Center) { Icon( - modifier = Modifier.bounceClick() , - imageVector = storageIcons[icon] ?: Icons.Outlined.SnippetFolder , - contentDescription = icon , + modifier = Modifier.bounceClick(), + imageVector = storageIcons[icon] ?: Icons.Outlined.SnippetFolder, + contentDescription = icon, tint = MaterialTheme.colorScheme.onPrimaryContainer ) } @@ -338,78 +336,78 @@ fun StorageBreakdownItem(icon : String , size : Long , modifier : Modifier = Mod Column { Text( - text = icon , - style = MaterialTheme.typography.bodyMedium , + text = icon, + style = MaterialTheme.typography.bodyMedium, fontWeight = FontWeight.Bold ) - Text(text = formatSize(size) , style = MaterialTheme.typography.bodySmall) + Text(text = formatSize(size), style = MaterialTheme.typography.bodySmall) } } } } @Composable -fun StorageInfoText(label : String , size : Long) { - Text(text = "$label ${formatSize(size)}" , style = MaterialTheme.typography.bodyMedium) +fun StorageInfoText(label: String, size: Long) { + Text(text = "$label ${formatSize(size)}", style = MaterialTheme.typography.bodyMedium) } @Composable -fun RamInfoCard(ramInfo : RamInfo) { +fun RamInfoCard(ramInfo: RamInfo) { Column(modifier = Modifier.padding(16.dp)) { Text( - text = stringResource(id = R.string.ram_information) , - style = MaterialTheme.typography.headlineSmall , + text = stringResource(id = R.string.ram_information), + style = MaterialTheme.typography.headlineSmall, fontWeight = FontWeight.Bold ) Spacer(modifier = Modifier.height(8.dp)) StorageProgressBar( StorageInfo( - totalStorage = ramInfo.totalRam , - usedStorage = ramInfo.usedRam , + totalStorage = ramInfo.totalRam, + usedStorage = ramInfo.usedRam, freeStorage = ramInfo.availableRam ) ) Spacer(modifier = Modifier.height(8.dp)) - StorageInfoText(label = stringResource(id = R.string.used_ram) , size = ramInfo.usedRam) + StorageInfoText(label = stringResource(id = R.string.used_ram), size = ramInfo.usedRam) StorageInfoText( - label = stringResource(id = R.string.free_ram) , size = ramInfo.availableRam + label = stringResource(id = R.string.free_ram), size = ramInfo.availableRam ) - StorageInfoText(label = stringResource(id = R.string.total_ram) , size = ramInfo.totalRam) + StorageInfoText(label = stringResource(id = R.string.total_ram), size = ramInfo.totalRam) } } @Composable fun DotsIndicator( - modifier : Modifier = Modifier , - totalDots : Int , - selectedIndex : Int , - selectedColor : Color = MaterialTheme.colorScheme.primary , - unSelectedColor : Color = Color.Gray , - dotSize : Dp , - animationDuration : Int = 300 + modifier: Modifier = Modifier, + @Suppress("SameParameterValue") totalDots: Int, + selectedIndex: Int, + selectedColor: Color = MaterialTheme.colorScheme.primary, + unSelectedColor: Color = Color.Gray, + dotSize: Dp, + animationDuration: Int = 300 ) { - val transition : Transition = - updateTransition(targetState = selectedIndex , label = "Dot Transition") + val transition: Transition = + updateTransition(targetState = selectedIndex, label = "Dot Transition") LazyRow( modifier = modifier - .wrapContentWidth() - .height(dotSize) , + .wrapContentWidth() + .height(dotSize), verticalAlignment = Alignment.CenterVertically ) { - items(count = totalDots , key = { index -> index }) { index -> - val animatedDotSize : Dp by transition.animateDp(transitionSpec = { - tween(durationMillis = animationDuration , easing = FastOutSlowInEasing) - } , label = "Dot Size Animation") { + items(count = totalDots, key = { index -> index }) { index -> + val animatedDotSize: Dp by transition.animateDp(transitionSpec = { + tween(durationMillis = animationDuration, easing = FastOutSlowInEasing) + }, label = "Dot Size Animation") { if (it == index) dotSize else dotSize / 1.4f } - val isSelected : Boolean = index == selectedIndex - val size : Dp = if (isSelected) animatedDotSize else animatedDotSize + val isSelected: Boolean = index == selectedIndex + val size: Dp = if (isSelected) animatedDotSize else animatedDotSize IndicatorDot( - color = if (isSelected) selectedColor else unSelectedColor , size = size + color = if (isSelected) selectedColor else unSelectedColor, size = size ) if (index != totalDots - 1) { @@ -421,14 +419,14 @@ fun DotsIndicator( @Composable fun IndicatorDot( - modifier : Modifier = Modifier , - size : Dp , - color : Color , + modifier: Modifier = Modifier, + size: Dp, + color: Color, ) { Box( modifier = modifier - .size(size) - .clip(CircleShape) - .background(color) + .size(size) + .clip(CircleShape) + .background(color) ) } \ No newline at end of file