├── .github ├── FUNDING.yml ├── ISSUE_TEMPLATE │ ├── bug_report.md │ └── feature_request.md └── workflows │ └── static.yml ├── .gitignore ├── LICENSE ├── README.md ├── app ├── .gitignore ├── build.gradle.kts ├── dependencies │ └── renderscript-toolkit-release.aar ├── proguard-rules.pro └── src │ └── main │ ├── AndroidManifest.xml │ ├── java │ └── com │ │ └── anthonyla │ │ └── paperize │ │ ├── App.kt │ │ ├── core │ │ ├── ScalingConstants.kt │ │ ├── SettingsConstants.kt │ │ └── WallpaperUtil.kt │ │ ├── data │ │ ├── SendContactIntent.kt │ │ ├── rememberCurrentOffset.kt │ │ └── settings │ │ │ ├── SettingsDataStore.kt │ │ │ └── SettingsDataStoreImpl.kt │ │ ├── di │ │ └── AppModule.kt │ │ └── feature │ │ └── wallpaper │ │ ├── app_shortcut │ │ └── BroadcastActivity.kt │ │ ├── data │ │ ├── data_source │ │ │ ├── AlbumDao.kt │ │ │ ├── AlbumDatabase.kt │ │ │ └── Converters.kt │ │ └── repository │ │ │ └── AlbumRepositoryImpl.kt │ │ ├── domain │ │ ├── model │ │ │ ├── Album.kt │ │ │ ├── AlbumWithWallpaperAndFolder.kt │ │ │ ├── Folder.kt │ │ │ ├── Metadata.kt │ │ │ └── Wallpaper.kt │ │ └── repository │ │ │ └── AlbumRepository.kt │ │ ├── glance_widget │ │ ├── PaperizeWidget.kt │ │ └── PaperizeWidgetReceiver.kt │ │ ├── presentation │ │ ├── MainActivity.kt │ │ ├── PaperizeApp.kt │ │ ├── add_album_screen │ │ │ ├── AddAlbumEvent.kt │ │ │ ├── AddAlbumScreen.kt │ │ │ ├── AddAlbumState.kt │ │ │ ├── AddAlbumViewModel.kt │ │ │ └── components │ │ │ │ ├── AddAlbumAnimatedFab.kt │ │ │ │ ├── AddAlbumDialog.kt │ │ │ │ ├── AddAlbumSmallTopBar.kt │ │ │ │ └── DeleteImagesAlertDialog.kt │ │ ├── album │ │ │ ├── AlbumsEvent.kt │ │ │ ├── AlbumsState.kt │ │ │ ├── AlbumsViewModel.kt │ │ │ └── components │ │ │ │ ├── AlbumItem.kt │ │ │ │ ├── FolderItem.kt │ │ │ │ └── WallpaperItem.kt │ │ ├── album_view_screen │ │ │ ├── AlbumScreenViewModel.kt │ │ │ ├── AlbumViewEvent.kt │ │ │ ├── AlbumViewScreen.kt │ │ │ ├── AlbumViewState.kt │ │ │ └── components │ │ │ │ ├── AlbumNameDialog.kt │ │ │ │ ├── AlbumViewTopBar.kt │ │ │ │ └── DeleteAlbumAlertDialog.kt │ │ ├── folder_view_screen │ │ │ ├── FolderEvent.kt │ │ │ ├── FolderState.kt │ │ │ ├── FolderViewModel.kt │ │ │ ├── FolderViewScreen.kt │ │ │ └── components │ │ │ │ └── FolderViewTopBar.kt │ │ ├── home_screen │ │ │ ├── HomeScreen.kt │ │ │ └── components │ │ │ │ ├── HomeTopBar.kt │ │ │ │ └── TabItem.kt │ │ ├── library_screen │ │ │ └── LibraryScreen.kt │ │ ├── notifications_screen │ │ │ └── NotificationScreen.kt │ │ ├── privacy_screen │ │ │ └── PrivacyScreen.kt │ │ ├── settings_screen │ │ │ ├── SettingsEvent.kt │ │ │ ├── SettingsScreen.kt │ │ │ ├── SettingsState.kt │ │ │ ├── SettingsViewModel.kt │ │ │ └── components │ │ │ │ ├── AmoledListItem.kt │ │ │ │ ├── AnimationListItem.kt │ │ │ │ ├── ContactListItem.kt │ │ │ │ ├── DarkModeListItem.kt │ │ │ │ ├── DynamicThemingListItem.kt │ │ │ │ ├── ListSectionTitle.kt │ │ │ │ ├── NotificationListItem.kt │ │ │ │ ├── PaperizeListItem.kt │ │ │ │ ├── PrivacyPolicyListItem.kt │ │ │ │ ├── ResetListItem.kt │ │ │ │ └── TranslateListItem.kt │ │ ├── sort_view_screen │ │ │ ├── SortEvent.kt │ │ │ ├── SortState.kt │ │ │ ├── SortViewModel.kt │ │ │ ├── SortViewScreen.kt │ │ │ └── components │ │ │ │ └── SortViewTopBar.kt │ │ ├── startup_screen │ │ │ └── StartupScreen.kt │ │ ├── themes │ │ │ ├── Color.kt │ │ │ └── Theme.kt │ │ ├── wallpaper_screen │ │ │ ├── WallpaperScreen.kt │ │ │ └── components │ │ │ │ ├── AlbumBottomSheet.kt │ │ │ │ ├── BlurSwitchAndSlider.kt │ │ │ │ ├── ChangerSelectionRow.kt │ │ │ │ ├── CurrentAndNextChange.kt │ │ │ │ ├── CurrentSelectedAlbum.kt │ │ │ │ ├── DarkenSwitchAndSlider.kt │ │ │ │ ├── GrayscaleBitmapTransformation.kt │ │ │ │ ├── GrayscaleSwitchAndSlider.kt │ │ │ │ ├── IndividualSchedulingAndToggleRow.kt │ │ │ │ ├── PreviewItem.kt │ │ │ │ ├── RefreshSwitch.kt │ │ │ │ ├── ShuffleSwitch.kt │ │ │ │ ├── TimeSliders.kt │ │ │ │ ├── VignetteBitmapTransformation.kt │ │ │ │ ├── VignetteSwitchAndSlider.kt │ │ │ │ └── WallpaperPreviewAndScale.kt │ │ └── wallpaper_view_screen │ │ │ └── WallpaperViewScreen.kt │ │ ├── tasker_shortcut │ │ ├── WallpaperChangeEvent.kt │ │ └── WallpaperShortcutRunner.kt │ │ ├── util │ │ └── navigation │ │ │ ├── AnimatedScreen.kt │ │ │ ├── NavConstants.kt │ │ │ ├── NavScreens.kt │ │ │ └── SharedXTransition.kt │ │ ├── wallpaper_alarmmanager │ │ ├── WallpaperAlarmItem.kt │ │ ├── WallpaperAlarmScheduler.kt │ │ ├── WallpaperAlarmSchedulerImpl.kt │ │ ├── WallpaperBootAndChangeReceiver.kt │ │ └── WallpaperReceiver.kt │ │ ├── wallpaper_service │ │ ├── HomeWallpaperService.kt │ │ └── LockWallpaperService.kt │ │ └── wallpaper_tile │ │ └── ChangeWallpaperTileService.kt │ └── res │ ├── app_icon-playstore.png │ ├── drawable-hdpi │ └── notification_icon.png │ ├── drawable-mdpi │ └── notification_icon.png │ ├── drawable-xhdpi │ └── notification_icon.png │ ├── drawable-xxhdpi │ └── notification_icon.png │ ├── drawable-xxxhdpi │ └── notification_icon.png │ ├── drawable │ ├── app_icon_foreground.xml │ ├── fdroid.xml │ ├── fill.xml │ ├── fit.xml │ ├── github.xml │ ├── izzyondroid.xml │ ├── next_wallpaper_icon.xml │ ├── none.xml │ ├── splash_background.xml │ └── stretch.xml │ ├── mipmap-anydpi-v26 │ ├── app_icon.xml │ └── app_icon_round.xml │ ├── mipmap-hdpi │ ├── app_icon.webp │ └── app_icon_round.webp │ ├── mipmap-mdpi │ ├── app_icon.webp │ └── app_icon_round.webp │ ├── mipmap-xhdpi │ ├── app_icon.webp │ └── app_icon_round.webp │ ├── mipmap-xxhdpi │ ├── app_icon.webp │ └── app_icon_round.webp │ ├── mipmap-xxxhdpi │ ├── app_icon.webp │ └── app_icon_round.webp │ ├── raw │ ├── notification_animation.json │ ├── onboarding_animation.json │ └── paperize_icon.svg │ ├── resources.properties │ ├── values-af │ └── strings.xml │ ├── values-ar │ └── strings.xml │ ├── values-b+ar+EG │ └── strings.xml │ ├── values-b+pt+BR │ └── strings.xml │ ├── values-b+zh+Hans │ └── strings.xml │ ├── values-b+zh+Hant │ └── strings.xml │ ├── values-bn │ └── strings.xml │ ├── values-ca │ └── strings.xml │ ├── values-cs │ └── strings.xml │ ├── values-da │ └── strings.xml │ ├── values-de │ └── strings.xml │ ├── values-el │ └── strings.xml │ ├── values-es │ └── strings.xml │ ├── values-fi │ └── strings.xml │ ├── values-fr │ └── strings.xml │ ├── values-gu │ └── strings.xml │ ├── values-ha │ └── strings.xml │ ├── values-hi │ └── strings.xml │ ├── values-hu │ └── strings.xml │ ├── values-it │ └── strings.xml │ ├── values-iw │ └── strings.xml │ ├── values-ja │ └── strings.xml │ ├── values-jv │ └── strings.xml │ ├── values-ko │ └── strings.xml │ ├── values-mr │ └── strings.xml │ ├── values-nl │ └── strings.xml │ ├── values-no │ └── strings.xml │ ├── values-pa │ └── strings.xml │ ├── values-pl │ └── strings.xml │ ├── values-pt │ └── strings.xml │ ├── values-ro │ └── strings.xml │ ├── values-ru │ └── strings.xml │ ├── values-sr │ └── strings.xml │ ├── values-sv │ └── strings.xml │ ├── values-ta │ └── strings.xml │ ├── values-te │ └── strings.xml │ ├── values-tr │ └── strings.xml │ ├── values-uk │ └── strings.xml │ ├── values-ur │ └── strings.xml │ ├── values-vi │ └── strings.xml │ ├── values │ ├── app_icon_background.xml │ ├── colors.xml │ ├── strings.xml │ └── themes.xml │ └── xml │ ├── backup_rules.xml │ ├── data_extraction_rules.xml │ ├── paperize.xml │ ├── paperize_widget_info.xml │ └── shortcuts.xml ├── build.gradle.kts ├── docs └── index.html ├── gradle.properties ├── gradle ├── libs.versions.toml └── wrapper │ ├── gradle-wrapper.jar │ └── gradle-wrapper.properties ├── gradlew ├── gradlew.bat ├── metadata └── en-US │ ├── full_description.txt │ ├── images │ ├── featureGraphic.png │ ├── icon.png │ └── phoneScreenshots │ │ ├── 1.png │ │ ├── 2.png │ │ ├── 3.png │ │ ├── 4.png │ │ └── 5.png │ ├── short_description.txt │ ├── title.txt │ └── video.txt ├── renovate.json └── settings.gradle.kts /.github/FUNDING.yml: -------------------------------------------------------------------------------- 1 | # These are supported funding model platforms 2 | 3 | github: Anthonyy232 4 | patreon: # Replace with a single Patreon username 5 | open_collective: # Replace with a single Open Collective username 6 | ko_fi: # Replace with a single Ko-fi username 7 | tidelift: # Replace with a single Tidelift platform-name/package-name e.g., npm/babel 8 | community_bridge: # Replace with a single Community Bridge project-name e.g., cloud-foundry 9 | liberapay: # Replace with a single Liberapay username 10 | issuehunt: # Replace with a single IssueHunt username 11 | lfx_crowdfunding: # Replace with a single LFX Crowdfunding project-name e.g., cloud-foundry 12 | polar: # Replace with a single Polar username 13 | buy_me_a_coffee: # Replace with a single Buy Me a Coffee username 14 | custom: # Replace with up to 4 custom sponsorship URLs e.g., ['link1', 'link2'] 15 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/bug_report.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Bug report 3 | about: Create a report to help us improve 4 | title: "[Bug] " 5 | labels: bug 6 | assignees: '' 7 | 8 | --- 9 | 10 | **Describe the bug** 11 | A clear and concise description of what the bug is. 12 | 13 | **To Reproduce** 14 | Steps to reproduce the behavior: 15 | 1. Go to '...' 16 | 2. Click on '....' 17 | 3. Scroll down to '....' 18 | 4. See error 19 | 20 | **Expected behavior** 21 | A clear and concise description of what you expected to happen. 22 | 23 | **Screenshots** 24 | If applicable, add screenshots to help explain your problem. 25 | 26 | **Smartphone (please complete the following information):** 27 | - Device: [e.g. Pixel 32 Pro] 28 | - OS: [e.g. OneUI 21] 29 | - Version [e.g. v2.1.0] 30 | 31 | **Additional context** 32 | Add any other context about the problem here. 33 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/feature_request.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Feature request 3 | about: Suggest an idea for this project 4 | title: "[Feature request] " 5 | labels: enhancement 6 | assignees: '' 7 | 8 | --- 9 | 10 | **Is your feature request related to a problem? Please describe.** 11 | A clear and concise description of what the problem is. Ex. I'm always frustrated when [...] 12 | 13 | **Describe the solution you'd like** 14 | A clear and concise description of what you want to happen. 15 | 16 | **Describe alternatives you've considered** 17 | A clear and concise description of any alternative solutions or features you've considered. 18 | 19 | **Additional context** 20 | Add any other context or screenshots about the feature request here. 21 | -------------------------------------------------------------------------------- /.github/workflows/static.yml: -------------------------------------------------------------------------------- 1 | # Simple workflow for deploying static content to GitHub Pages 2 | name: Deploy static content to Pages 3 | 4 | on: 5 | # Runs on pushes targeting the default branch 6 | push: 7 | branches: ["master"] 8 | 9 | # Allows you to run this workflow manually from the Actions tab 10 | workflow_dispatch: 11 | 12 | # Sets permissions of the GITHUB_TOKEN to allow deployment to GitHub Pages 13 | permissions: 14 | contents: read 15 | pages: write 16 | id-token: write 17 | 18 | # Allow only one concurrent deployment, skipping runs queued between the run in-progress and latest queued. 19 | # However, do NOT cancel in-progress runs as we want to allow these production deployments to complete. 20 | concurrency: 21 | group: "pages" 22 | cancel-in-progress: false 23 | 24 | jobs: 25 | # Single deploy job since we're just deploying 26 | deploy: 27 | environment: 28 | name: github-pages 29 | url: ${{ steps.deployment.outputs.page_url }} 30 | runs-on: ubuntu-latest 31 | steps: 32 | - name: Checkout 33 | uses: actions/checkout@v4 34 | - name: Setup Pages 35 | uses: actions/configure-pages@v5 36 | - name: Upload artifact 37 | uses: actions/upload-pages-artifact@v3 38 | with: 39 | # Upload entire repository 40 | path: '.' 41 | - name: Deploy to GitHub Pages 42 | id: deployment 43 | uses: actions/deploy-pages@v4 44 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | *.iml 2 | .gradle 3 | .idea 4 | /local.properties 5 | /.idea/caches 6 | /.idea/libraries 7 | /.idea/modules.xml 8 | /.idea/workspace.xml 9 | /.idea/navEditor.xml 10 | /.idea/assetWizardSettings.xml 11 | .DS_Store 12 | /build 13 | /captures 14 | .externalNativeBuild 15 | .cxx 16 | local.properties 17 | /app/build 18 | /app/release 19 | /app/schemas 20 | .kotlin 21 | /.kotlin/sessions -------------------------------------------------------------------------------- /app/.gitignore: -------------------------------------------------------------------------------- 1 | /build -------------------------------------------------------------------------------- /app/build.gradle.kts: -------------------------------------------------------------------------------- 1 | plugins { 2 | alias(libs.plugins.androidApplication) 3 | alias(libs.plugins.kotlinAndroid) 4 | alias(libs.plugins.kotlinCompose) 5 | alias(libs.plugins.hiltAndroid) 6 | alias(libs.plugins.ksp) 7 | alias(libs.plugins.kotlinSerialization) 8 | } 9 | 10 | ksp { 11 | arg("room.schemaLocation", "$projectDir/schemas") 12 | } 13 | 14 | android { 15 | namespace = "com.anthonyla.paperize" 16 | compileSdk = 35 17 | 18 | defaultConfig { 19 | applicationId = "com.anthonyla.paperize" 20 | minSdk = 26 21 | targetSdk = 37 22 | versionCode = 37 23 | versionName = "3.0.0" 24 | testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner" 25 | vectorDrawables { 26 | useSupportLibrary = true 27 | } 28 | } 29 | 30 | buildTypes { 31 | release { 32 | isMinifyEnabled = true 33 | isShrinkResources = true 34 | proguardFiles( 35 | getDefaultProguardFile("proguard-android-optimize.txt"), 36 | "proguard-rules.pro" 37 | ) 38 | signingConfig = signingConfigs.getByName("debug") 39 | } 40 | } 41 | 42 | compileOptions { 43 | sourceCompatibility = JavaVersion.VERSION_17 44 | targetCompatibility = JavaVersion.VERSION_17 45 | } 46 | 47 | kotlinOptions { 48 | jvmTarget = "17" 49 | } 50 | 51 | buildFeatures { 52 | compose = true 53 | viewBinding = true 54 | buildConfig = true 55 | } 56 | 57 | androidResources { 58 | @Suppress("UnstableApiUsage") 59 | generateLocaleConfig = true 60 | } 61 | 62 | buildToolsVersion = "35.0.1" 63 | dependenciesInfo { 64 | includeInApk = false 65 | includeInBundle = false 66 | } 67 | 68 | applicationVariants.all { 69 | this.outputs 70 | .map { it as com.android.build.gradle.internal.api.ApkVariantOutputImpl } 71 | .forEach { output -> 72 | val apkName = "paperize-v${this.versionName}.apk" 73 | output.outputFileName = apkName 74 | } 75 | } 76 | } 77 | 78 | androidComponents { 79 | onVariants(selector().withBuildType("release")) { 80 | it.packaging.resources.excludes.add("META-INF/**") 81 | } 82 | } 83 | 84 | dependencies { 85 | implementation(libs.androidx.core.ktx) 86 | implementation(libs.androidx.lifecycle.runtime.ktx) 87 | implementation(libs.androidx.activity.compose) 88 | implementation(platform(libs.androidx.compose.bom)) 89 | implementation(libs.androidx.ui) 90 | implementation(libs.androidx.ui.graphics) 91 | implementation(libs.androidx.ui.tooling.preview) 92 | implementation(libs.androidx.material3) 93 | implementation(libs.androidx.navigation.compose) 94 | implementation(libs.androidx.material) 95 | implementation(libs.androidx.datastore) 96 | implementation(libs.androidx.datastore.preferences) 97 | implementation(libs.androidx.material.icons.extended) 98 | implementation(libs.androidx.hilt.navigation.compose) 99 | implementation(libs.androidx.animation) 100 | implementation(libs.androidx.core.splashscreen) 101 | implementation(libs.androidx.lifecycle.runtime.compose) 102 | implementation(libs.gson) 103 | implementation(libs.androidx.documentfile) 104 | implementation(libs.zoomable) 105 | implementation(libs.landscapist.glide) 106 | implementation(libs.androidx.work.runtime.ktx) 107 | implementation(libs.androidx.hilt.work) 108 | implementation(libs.lottie.compose) 109 | implementation(libs.accompanist.permissions) 110 | implementation(libs.androidx.foundation) 111 | implementation(libs.lazycolumnscrollbar) 112 | implementation(libs.taskerpluginlibrary) 113 | implementation(libs.androidx.profileinstaller) 114 | implementation(libs.reorderable) 115 | implementation(libs.androidx.glance.appwidget) 116 | implementation(libs.androidx.glance.material3) 117 | implementation(libs.androidx.glance.material) 118 | implementation(files("dependencies/renderscript-toolkit-release.aar")) 119 | testImplementation(libs.junit) 120 | androidTestImplementation(libs.androidx.junit) 121 | androidTestImplementation(libs.androidx.espresso.core) 122 | androidTestImplementation(libs.androidx.ui.test.junit4) 123 | debugImplementation(libs.androidx.ui.tooling) 124 | debugImplementation(libs.androidx.ui.test.manifest) 125 | implementation(libs.hilt.android) 126 | ksp(libs.hilt.android.compiler) 127 | implementation(libs.androidx.room.runtime) 128 | ksp(libs.androidx.room.compiler) 129 | implementation(libs.androidx.room.ktx) 130 | implementation(libs.dfc) 131 | implementation (libs.kotlinx.serialization.json) 132 | implementation(libs.toolbar.compose) 133 | } -------------------------------------------------------------------------------- /app/dependencies/renderscript-toolkit-release.aar: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Anthonyy232/Paperize/89450026526cb5c75e549a80a2a914602075817d/app/dependencies/renderscript-toolkit-release.aar -------------------------------------------------------------------------------- /app/proguard-rules.pro: -------------------------------------------------------------------------------- 1 | # Add project specific ProGuard rules here. 2 | # You can control the set of applied configuration files using the 3 | # proguardFiles setting in build.gradle. 4 | # 5 | # For more details, see 6 | # http://developer.android.com/guide/developing/tools/proguard.html 7 | 8 | # If your project uses WebView with JS, uncomment the following 9 | # and specify the fully qualified class name to the JavaScript interface 10 | # class: 11 | #-keepclassmembers class fqcn.of.javascript.interface.for.webview { 12 | # public *; 13 | #} 14 | 15 | # Uncomment this to preserve the line number information for 16 | # debugging stack traces. 17 | -keepattributes SourceFile,LineNumberTable 18 | 19 | # If you keep the line number information, uncomment this to 20 | # hide the original source file name. 21 | #-renamesourcefileattribute SourceFile 22 | -keepattributes Signature 23 | -keep class com.google.gson.reflect.TypeToken { *; } 24 | -keep class * extends com.google.gson.reflect.TypeToken 25 | -keep class kotlin.coroutines.Continuation 26 | -keep class androidx.datastore.*.** {*;} 27 | 28 | -keepclassmembers class com.anthonyla.paperize.feature.wallpaper.domain.model.Album { 29 | !transient ; 30 | } 31 | -keepclassmembers class com.anthonyla.paperize.feature.wallpaper.domain.model.AlbumWithWallpaperAndFolder { 32 | !transient ; 33 | } 34 | -keepclassmembers class com.anthonyla.paperize.feature.wallpaper.domain.model.Wallpaper { 35 | !transient ; 36 | } 37 | -keepclassmembers class com.anthonyla.paperize.feature.wallpaper.domain.model.Folder { 38 | !transient ; 39 | } 40 | 41 | # Keep wallpaper services and their action enums 42 | -keep class com.anthonyla.paperize.feature.wallpaper.wallpaper_service.HomeWallpaperService { *; } 43 | -keep class com.anthonyla.paperize.feature.wallpaper.wallpaper_service.HomeWallpaperService$Actions { *; } 44 | -keep class com.anthonyla.paperize.feature.wallpaper.wallpaper_service.LockWallpaperService { *; } 45 | -keep class com.anthonyla.paperize.feature.wallpaper.wallpaper_service.LockWallpaperService$Actions { *; } 46 | 47 | # Keep WallpaperAction classes 48 | -keep class com.anthonyla.paperize.feature.wallpaper.wallpaper_alarmmanager.WallpaperAction { *; } 49 | -keep class com.anthonyla.paperize.feature.wallpaper.wallpaper_alarmmanager.WallpaperAction$* { *; } 50 | 51 | # Keep Type enum 52 | -keep class com.anthonyla.paperize.core.Type { *; } -------------------------------------------------------------------------------- /app/src/main/AndroidManifest.xml: -------------------------------------------------------------------------------- 1 | 2 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 24 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 37 | 38 | 39 | 40 | 41 | 47 | 48 | 49 | 51 | 52 | 53 | 54 | 55 | 56 | 57 | 58 | 60 | 61 | 62 | 63 | 66 | 67 | 72 | 74 | 75 | 76 | 81 | 82 | 83 | 84 | 85 | 89 | 90 | 91 | 92 | 93 | 94 | -------------------------------------------------------------------------------- /app/src/main/java/com/anthonyla/paperize/App.kt: -------------------------------------------------------------------------------- 1 | package com.anthonyla.paperize 2 | 3 | import android.annotation.SuppressLint 4 | import android.app.Application 5 | import android.app.NotificationChannel 6 | import android.app.NotificationManager 7 | import android.database.CursorWindow 8 | import dagger.hilt.android.HiltAndroidApp 9 | import java.lang.reflect.Field 10 | 11 | 12 | /** 13 | * Application class for Hilt 14 | */ 15 | @HiltAndroidApp 16 | class App: Application() { 17 | override fun onCreate() { 18 | super.onCreate() 19 | val channel = NotificationChannel("wallpaper_service_channel", "Paperize", NotificationManager.IMPORTANCE_LOW) 20 | val notificationManager = getSystemService(NOTIFICATION_SERVICE) as NotificationManager 21 | notificationManager.createNotificationChannel(channel) 22 | } 23 | } -------------------------------------------------------------------------------- /app/src/main/java/com/anthonyla/paperize/core/ScalingConstants.kt: -------------------------------------------------------------------------------- 1 | package com.anthonyla.paperize.core 2 | 3 | enum class ScalingConstants { 4 | FIT, 5 | FILL, 6 | STRETCH, 7 | NONE 8 | } -------------------------------------------------------------------------------- /app/src/main/java/com/anthonyla/paperize/core/SettingsConstants.kt: -------------------------------------------------------------------------------- 1 | package com.anthonyla.paperize.core 2 | 3 | /** 4 | * Constants for application settings 5 | */ 6 | object SettingsConstants { 7 | const val SETTINGS_DATASTORE = "settings_datastore" 8 | const val DARK_MODE_TYPE = "dark_mode_type" 9 | const val AMOLED_THEME_TYPE = "amoled_theme_type" 10 | const val DYNAMIC_THEME_TYPE = "dynamic_theme_type" 11 | const val ANIMATE_TYPE = "animate_type" 12 | const val ENABLE_CHANGER = "enable_changer" 13 | const val ENABLE_HOME_WALLPAPER = "home_wallpaper" 14 | const val ENABLE_LOCK_WALLPAPER = "lock_wallpaper" 15 | const val CURRENT_HOME_WALLPAPER = "current_home_wallpaper" 16 | const val CURRENT_LOCK_WALLPAPER = "current_lock_wallpaper" 17 | const val HOME_ALBUM_NAME = "home_album_name" 18 | const val LOCK_ALBUM_NAME = "lock_album_name" 19 | const val HOME_WALLPAPER_CHANGE_INTERVAL = "home_wallpaper_change_interval" 20 | const val LOCK_WALLPAPER_CHANGE_INTERVAL = "lock_wallpaper_change_interval" 21 | const val REFRESH = "refresh" 22 | const val WALLPAPER_CHANGE_INTERVAL_DEFAULT = 15 23 | const val WALLPAPER_CHANGE_INTERVAL_MIN = 1 24 | const val FIRST_LAUNCH = "first_launch" 25 | const val FIRST_SET = "first_set" 26 | const val LAST_SET_TIME = "last_set_time" 27 | const val NEXT_SET_TIME = "next_set_time" 28 | const val HOME_DARKEN_PERCENTAGE = "home_darken_percentage" 29 | const val LOCK_DARKEN_PERCENTAGE = "darken_darken_percentage" 30 | const val DARKEN = "darken" 31 | const val WALLPAPER_SCALING = "wallpaper_scaling" 32 | const val SCHEDULE_SEPARATELY = "schedule_separately" 33 | const val BLUR = "blur" 34 | const val HOME_BLUR_PERCENTAGE = "home_blur_percentage" 35 | const val LOCK_BLUR_PERCENTAGE = "lock_blur_percentage" 36 | const val VIGNETTE = "vignette" 37 | const val HOME_VIGNETTE_PERCENTAGE = "home_vignette_percentage" 38 | const val LOCK_VIGNETTE_PERCENTAGE = "lock_vignette_percentage" 39 | const val HOME_NEXT_SET_TIME = "next_set_time_1" 40 | const val LOCK_NEXT_SET_TIME = "next_set_time_2" 41 | const val NEXT_HOME_WALLPAPER = "next_home_wallpaper" 42 | const val NEXT_LOCK_WALLPAPER = "next_lock_wallpaper" 43 | const val GRAYSCALE = "grayscale" 44 | const val HOME_GRAYSCALE_PERCENTAGE = "home_grayscale_percentage" 45 | const val LOCK_GRAYSCALE_PERCENTAGE = "lock_grayscale_percentage" 46 | const val CHANGE_START_TIME = "change_start_time" 47 | const val START_HOUR = "start_hour" 48 | const val START_MINUTE = "start_minute" 49 | const val SHUFFLE = "shuffle" 50 | } -------------------------------------------------------------------------------- /app/src/main/java/com/anthonyla/paperize/data/SendContactIntent.kt: -------------------------------------------------------------------------------- 1 | package com.anthonyla.paperize.data 2 | 3 | import android.content.Context 4 | import android.content.Intent 5 | import android.net.Uri 6 | import androidx.core.content.ContextCompat.startActivity 7 | 8 | /** 9 | * Opens an email intent to contact the developer. 10 | */ 11 | fun SendContactIntent(context: Context) { 12 | val authorEmail = "anthonyyla.dev@gmail.com" 13 | val cc = "" 14 | val subject = "[Support] Paperize" 15 | val bodyText = "This is regarding the Paperize app for Android:\n" 16 | val mailto = "mailto:" + Uri.encode(authorEmail) + 17 | "?cc=" + Uri.encode(cc) + 18 | "&subject=" + Uri.encode(subject) + 19 | "&body=" + Uri.encode(bodyText) 20 | 21 | val emailIntent = Intent(Intent.ACTION_SENDTO) 22 | emailIntent.setData(Uri.parse(mailto)) 23 | startActivity(context, emailIntent, null) 24 | } -------------------------------------------------------------------------------- /app/src/main/java/com/anthonyla/paperize/data/rememberCurrentOffset.kt: -------------------------------------------------------------------------------- 1 | package com.anthonyla.paperize.data 2 | 3 | import androidx.compose.foundation.lazy.LazyListState 4 | import androidx.compose.runtime.Composable 5 | import androidx.compose.runtime.LaunchedEffect 6 | import androidx.compose.runtime.MutableState 7 | import androidx.compose.runtime.SideEffect 8 | import androidx.compose.runtime.State 9 | import androidx.compose.runtime.derivedStateOf 10 | import androidx.compose.runtime.mutableIntStateOf 11 | import androidx.compose.runtime.remember 12 | 13 | /** 14 | * Remember the current offset of a [LazyListState]. 15 | * Source: 16 | * https://stackoverflow.com/a/75677163 17 | * https://stackoverflow.com/a/70558512 18 | */ 19 | @Composable 20 | fun rememberCurrentOffset(state: LazyListState): State { 21 | val position = remember { derivedStateOf { state.firstVisibleItemIndex } } 22 | val itemOffset = remember { derivedStateOf { state.firstVisibleItemScrollOffset } } 23 | val lastPosition = rememberPrevious(position.value) 24 | val lastItemOffset = rememberPrevious(itemOffset.value) 25 | val currentOffset = remember { mutableIntStateOf(0) } 26 | 27 | LaunchedEffect(position.value, itemOffset.value) { 28 | currentOffset.intValue = when { 29 | lastPosition == null || position.value == 0 -> itemOffset.value 30 | lastPosition == position.value -> currentOffset.intValue + (itemOffset.value - (lastItemOffset ?: 0)) 31 | lastPosition > position.value -> currentOffset.intValue - (lastItemOffset ?: 0) 32 | else -> currentOffset.intValue + itemOffset.value 33 | } 34 | } 35 | 36 | return currentOffset 37 | } 38 | 39 | @Composable 40 | fun rememberPrevious( 41 | current: T, 42 | shouldUpdate: (prev: T?, curr: T) -> Boolean = { prev, curr -> prev != curr }, 43 | ): T? { 44 | val ref = rememberRef() 45 | SideEffect { 46 | if (shouldUpdate(ref.value, current)) { 47 | ref.value = current 48 | } 49 | } 50 | return ref.value 51 | } 52 | 53 | @Composable 54 | fun rememberRef(): MutableState { 55 | return remember { 56 | object : MutableState { 57 | override var value: T? = null 58 | override fun component1(): T? = value 59 | override fun component2(): (T?) -> Unit = { value = it } 60 | } 61 | } 62 | } -------------------------------------------------------------------------------- /app/src/main/java/com/anthonyla/paperize/data/settings/SettingsDataStore.kt: -------------------------------------------------------------------------------- 1 | package com.anthonyla.paperize.data.settings 2 | 3 | import kotlinx.coroutines.flow.Flow 4 | 5 | /** 6 | * Interface for the data store that handles the settings for application 7 | */ 8 | interface SettingsDataStore { 9 | suspend fun putBoolean(key: String, value: Boolean) 10 | suspend fun putString(key: String, value: String) 11 | suspend fun putInt(key: String, value: Int) 12 | suspend fun getBoolean(key: String): Boolean? 13 | suspend fun getString(key: String): String? 14 | suspend fun getInt(key: String): Int? 15 | fun getBooleanFlow(key: String): Flow 16 | fun getStringFlow(key: String): Flow 17 | fun getIntFlow(key: String): Flow 18 | suspend fun deleteBoolean(key: String) 19 | suspend fun deleteString(key: String) 20 | suspend fun deleteInt(key: String) 21 | suspend fun clear(keys: List) 22 | suspend fun clearPreferences() 23 | } -------------------------------------------------------------------------------- /app/src/main/java/com/anthonyla/paperize/data/settings/SettingsDataStoreImpl.kt: -------------------------------------------------------------------------------- 1 | package com.anthonyla.paperize.data.settings 2 | 3 | import android.content.Context 4 | import androidx.datastore.core.DataStore 5 | import androidx.datastore.preferences.core.Preferences 6 | import androidx.datastore.preferences.core.booleanPreferencesKey 7 | import androidx.datastore.preferences.core.edit 8 | import androidx.datastore.preferences.core.emptyPreferences 9 | import androidx.datastore.preferences.core.intPreferencesKey 10 | import androidx.datastore.preferences.core.stringPreferencesKey 11 | import androidx.datastore.preferences.preferencesDataStore 12 | import com.anthonyla.paperize.core.SettingsConstants.SETTINGS_DATASTORE 13 | import kotlinx.coroutines.flow.Flow 14 | import kotlinx.coroutines.flow.catch 15 | import kotlinx.coroutines.flow.first 16 | import kotlinx.coroutines.flow.map 17 | 18 | private val Context.dataStore: DataStore by preferencesDataStore(name = SETTINGS_DATASTORE) 19 | 20 | class SettingsDataStoreImpl(private val context: Context) : SettingsDataStore { 21 | override suspend fun putBoolean(key: String, value: Boolean) { 22 | val preferencesKey = booleanPreferencesKey(key) 23 | context.dataStore.edit { 24 | it[preferencesKey] = value 25 | } 26 | } 27 | 28 | override suspend fun putString(key: String, value: String) { 29 | val preferencesKey = stringPreferencesKey(key) 30 | context.dataStore.edit { 31 | it[preferencesKey] = value 32 | } 33 | } 34 | 35 | override suspend fun putInt(key: String, value: Int) { 36 | val preferencesKey = intPreferencesKey(key) 37 | context.dataStore.edit { 38 | it[preferencesKey] = value 39 | } 40 | } 41 | 42 | override suspend fun getBoolean(key: String): Boolean? { 43 | val head = context.dataStore.data.first() 44 | val preferencesKey = booleanPreferencesKey(key) 45 | return head[preferencesKey] 46 | } 47 | 48 | override suspend fun getString(key: String): String? { 49 | return try { 50 | val head = context.dataStore.data.first() 51 | val preferencesKey = stringPreferencesKey(key) 52 | head[preferencesKey] 53 | } catch (exception: Exception) { 54 | exception.printStackTrace() 55 | null 56 | } 57 | } 58 | 59 | override suspend fun getInt(key: String): Int? { 60 | return try { 61 | val head = context.dataStore.data.first() 62 | val preferencesKey = intPreferencesKey(key) 63 | head[preferencesKey]?.toInt() 64 | } catch (exception: Exception) { 65 | exception.printStackTrace() 66 | null 67 | } 68 | } 69 | 70 | override fun getBooleanFlow(key: String): Flow = context.dataStore.data 71 | .catch { exception -> 72 | exception.printStackTrace() 73 | emit(emptyPreferences()) 74 | } 75 | .map { preferences -> 76 | preferences[booleanPreferencesKey(key)] 77 | } 78 | 79 | override fun getStringFlow(key: String): Flow = context.dataStore.data 80 | .catch { exception -> 81 | exception.printStackTrace() 82 | emit(emptyPreferences()) 83 | } 84 | .map { preferences -> 85 | preferences[stringPreferencesKey(key)] 86 | } 87 | 88 | override fun getIntFlow(key: String): Flow = context.dataStore.data 89 | .catch { exception -> 90 | exception.printStackTrace() 91 | emit(emptyPreferences()) 92 | } 93 | .map { preferences -> 94 | preferences[intPreferencesKey(key)] 95 | } 96 | 97 | override suspend fun deleteBoolean(key: String) { 98 | val preferencesKey = booleanPreferencesKey(key) 99 | context.dataStore.edit { 100 | if (it.contains(preferencesKey)) { 101 | it.remove(preferencesKey) 102 | } 103 | } 104 | } 105 | 106 | override suspend fun deleteString(key: String) { 107 | val preferencesKey = stringPreferencesKey(key) 108 | context.dataStore.edit { 109 | if (it.contains(preferencesKey)) { 110 | it.remove(preferencesKey) 111 | } 112 | } 113 | } 114 | 115 | override suspend fun deleteInt(key: String) { 116 | val preferencesKey = intPreferencesKey(key) 117 | context.dataStore.edit { 118 | if (it.contains(preferencesKey)) { 119 | it.remove(preferencesKey) 120 | } 121 | } 122 | } 123 | 124 | override suspend fun clearPreferences() { 125 | context.dataStore.edit { 126 | it.clear() 127 | } 128 | } 129 | 130 | override suspend fun clear(keys: List) { 131 | context.dataStore.edit { preferences -> 132 | keys.forEach { key -> 133 | if (preferences.contains(booleanPreferencesKey(key))) { 134 | preferences.remove(booleanPreferencesKey(key)) 135 | } 136 | else if (preferences.contains(stringPreferencesKey(key))) { 137 | preferences.remove(stringPreferencesKey(key)) 138 | } 139 | else if (preferences.contains(intPreferencesKey(key))) { 140 | preferences.remove(intPreferencesKey(key)) 141 | } 142 | } 143 | } 144 | } 145 | } -------------------------------------------------------------------------------- /app/src/main/java/com/anthonyla/paperize/di/AppModule.kt: -------------------------------------------------------------------------------- 1 | package com.anthonyla.paperize.di 2 | 3 | import android.app.Application 4 | import android.content.Context 5 | import androidx.room.Room 6 | import com.anthonyla.paperize.data.settings.SettingsDataStore 7 | import com.anthonyla.paperize.data.settings.SettingsDataStoreImpl 8 | import com.anthonyla.paperize.feature.wallpaper.data.data_source.AlbumDatabase 9 | import com.anthonyla.paperize.feature.wallpaper.data.repository.AlbumRepositoryImpl 10 | import com.anthonyla.paperize.feature.wallpaper.domain.repository.AlbumRepository 11 | import com.anthonyla.paperize.feature.wallpaper.wallpaper_alarmmanager.WallpaperAlarmSchedulerImpl 12 | import dagger.Module 13 | import dagger.Provides 14 | import dagger.hilt.InstallIn 15 | import dagger.hilt.android.qualifiers.ApplicationContext 16 | import dagger.hilt.components.SingletonComponent 17 | import javax.inject.Singleton 18 | 19 | /** 20 | * AppModule provides dependencies for the application for Dagger Hilt to inject 21 | */ 22 | @Module 23 | @InstallIn(SingletonComponent::class) 24 | object AppModule { 25 | @Provides 26 | @Singleton 27 | fun provideAlbumDatabase(app: Application): AlbumDatabase { 28 | return Room.databaseBuilder( 29 | app, 30 | AlbumDatabase::class.java, 31 | AlbumDatabase.DATABASE_NAME 32 | ).fallbackToDestructiveMigration(true).addMigrations().build() 33 | } 34 | 35 | @Provides 36 | @Singleton 37 | fun provideAlbumDao(db: AlbumDatabase) = db.albumDao 38 | 39 | @Provides 40 | @Singleton 41 | fun provideAlbumRepository( 42 | db: AlbumDatabase 43 | ): AlbumRepository { 44 | return AlbumRepositoryImpl(db.albumDao) 45 | } 46 | 47 | @Provides 48 | @Singleton 49 | fun provideSettingsDataStore ( 50 | @ApplicationContext context: Context 51 | ): SettingsDataStore = SettingsDataStoreImpl(context) 52 | 53 | @Provides 54 | @Singleton 55 | fun provideContext( 56 | @ApplicationContext context: Context, 57 | ): Context { return context } 58 | } -------------------------------------------------------------------------------- /app/src/main/java/com/anthonyla/paperize/feature/wallpaper/app_shortcut/BroadcastActivity.kt: -------------------------------------------------------------------------------- 1 | package com.anthonyla.paperize.feature.wallpaper.app_shortcut 2 | 3 | import android.app.Activity 4 | import android.content.Intent 5 | import android.os.Bundle 6 | import com.anthonyla.paperize.feature.wallpaper.wallpaper_alarmmanager.WallpaperBootAndChangeReceiver 7 | 8 | class BroadcastActivity : Activity() { 9 | companion object { 10 | const val ACTION_CUSTOM_BROADCAST = "com.anthonyla.paperize.SHORTCUT" 11 | } 12 | 13 | override fun onCreate(savedInstanceState: Bundle?) { 14 | super.onCreate(savedInstanceState) 15 | val intent = Intent(ACTION_CUSTOM_BROADCAST).apply { 16 | setClass(this@BroadcastActivity, WallpaperBootAndChangeReceiver::class.java) 17 | addFlags(Intent.FLAG_INCLUDE_STOPPED_PACKAGES) 18 | } 19 | sendBroadcast(intent) 20 | finish() 21 | } 22 | } -------------------------------------------------------------------------------- /app/src/main/java/com/anthonyla/paperize/feature/wallpaper/data/data_source/AlbumDao.kt: -------------------------------------------------------------------------------- 1 | package com.anthonyla.paperize.feature.wallpaper.data.data_source 2 | 3 | import androidx.room.Dao 4 | import androidx.room.Delete 5 | import androidx.room.Insert 6 | import androidx.room.OnConflictStrategy 7 | import androidx.room.Query 8 | import androidx.room.Transaction 9 | import androidx.room.Update 10 | import com.anthonyla.paperize.feature.wallpaper.domain.model.Album 11 | import com.anthonyla.paperize.feature.wallpaper.domain.model.AlbumWithWallpaperAndFolder 12 | import com.anthonyla.paperize.feature.wallpaper.domain.model.Folder 13 | import com.anthonyla.paperize.feature.wallpaper.domain.model.Wallpaper 14 | import kotlinx.coroutines.flow.Flow 15 | 16 | /** 17 | * Data access object for AlbumWithWallpaperAndFolder which is used for application's list of user albums 18 | */ 19 | @Dao 20 | interface AlbumDao { 21 | @Transaction 22 | @Query("SELECT * FROM album") 23 | fun getAlbumsWithWallpaperAndFolder(): Flow> 24 | 25 | @Transaction 26 | @Query("SELECT * FROM album WHERE selected = 1") 27 | fun getSelectedAlbums(): Flow> 28 | 29 | @Query("UPDATE album SET selected = :selected WHERE initialAlbumName = :albumName") 30 | suspend fun updateAlbumSelection(albumName: String, selected: Boolean) 31 | 32 | @Query("UPDATE album SET selected = 0") 33 | suspend fun deselectAllAlbums() 34 | 35 | @Insert(onConflict = OnConflictStrategy.REPLACE) 36 | suspend fun upsertAlbum(album: Album) 37 | 38 | @Insert(onConflict = OnConflictStrategy.REPLACE) 39 | suspend fun upsertWallpaper(wallpaper: Wallpaper) 40 | 41 | @Insert(onConflict = OnConflictStrategy.REPLACE) 42 | suspend fun upsertFolder(folder: Folder) 43 | 44 | @Insert(onConflict = OnConflictStrategy.REPLACE) 45 | suspend fun upsertWallpaperList(wallpapers: List) 46 | 47 | @Insert(onConflict = OnConflictStrategy.REPLACE) 48 | suspend fun upsertFolderList(folders: List) 49 | 50 | @Delete 51 | suspend fun deleteAlbum(album: Album) 52 | 53 | @Delete 54 | suspend fun deleteWallpaper(wallpaper: Wallpaper) 55 | 56 | @Delete 57 | suspend fun deleteFolder(folder: Folder) 58 | @Delete 59 | suspend fun deleteWallpaperList(wallpapers: List) 60 | 61 | @Delete 62 | suspend fun deleteFolderList(folders: List) 63 | 64 | @Query("DELETE FROM wallpaper WHERE initialAlbumName=:initialAlbumName") 65 | suspend fun cascadeDeleteWallpaper(initialAlbumName: String) 66 | 67 | @Query("DELETE FROM folder WHERE initialAlbumName=:initialAlbumName") 68 | suspend fun cascadeDeleteFolder(initialAlbumName: String) 69 | 70 | @Update 71 | suspend fun updateAlbum(album: Album) 72 | 73 | @Update 74 | suspend fun updateWallpaper(wallpaper: Wallpaper) 75 | 76 | @Update 77 | suspend fun updateFolder(folder: Folder) 78 | 79 | @Query("DELETE FROM album") 80 | suspend fun deleteAllAlbums() 81 | 82 | @Query("DELETE FROM wallpaper") 83 | suspend fun deleteAllWallpapers() 84 | 85 | @Query("DELETE FROM folder") 86 | suspend fun deleteAllFolders() 87 | 88 | @Transaction 89 | suspend fun deleteAllData() { 90 | deleteAllWallpapers() 91 | deleteAllFolders() 92 | deleteAllAlbums() 93 | } 94 | } -------------------------------------------------------------------------------- /app/src/main/java/com/anthonyla/paperize/feature/wallpaper/data/data_source/AlbumDatabase.kt: -------------------------------------------------------------------------------- 1 | package com.anthonyla.paperize.feature.wallpaper.data.data_source 2 | 3 | import androidx.room.Database 4 | import androidx.room.RoomDatabase 5 | import androidx.room.TypeConverters 6 | import com.anthonyla.paperize.feature.wallpaper.domain.model.Album 7 | import com.anthonyla.paperize.feature.wallpaper.domain.model.Folder 8 | import com.anthonyla.paperize.feature.wallpaper.domain.model.Wallpaper 9 | 10 | /** 11 | * Database for storing albums, wallpapers, and folders 12 | * Stores [Album], [Wallpaper], and [Folder] inside AlbumWithWallpaperAndFolder 13 | */ 14 | @Database( 15 | entities = [Album::class, Wallpaper::class, Folder::class], 16 | version = 11 17 | ) 18 | @TypeConverters(Converters::class) 19 | abstract class AlbumDatabase: RoomDatabase() { 20 | abstract val albumDao: AlbumDao 21 | companion object { 22 | const val DATABASE_NAME = "paperize_album_db" 23 | } 24 | } -------------------------------------------------------------------------------- /app/src/main/java/com/anthonyla/paperize/feature/wallpaper/data/data_source/Converters.kt: -------------------------------------------------------------------------------- 1 | package com.anthonyla.paperize.feature.wallpaper.data.data_source 2 | 3 | import androidx.room.TypeConverter 4 | import com.anthonyla.paperize.feature.wallpaper.domain.model.Wallpaper 5 | import com.google.gson.Gson 6 | import com.google.gson.reflect.TypeToken 7 | 8 | /** 9 | * Converters for Room database to convert list of strings to json string and vice versa 10 | */ 11 | class Converters { 12 | private val gson = Gson() 13 | 14 | /** 15 | * Convert list of strings to json string 16 | */ 17 | @TypeConverter 18 | fun listToJsonString(value: List?): String = gson.toJson(value) 19 | 20 | /** 21 | * Convert json string to list of strings 22 | */ 23 | @TypeConverter 24 | fun jsonStringToList(value: String?): List { 25 | if (value == null) return emptyList() 26 | return gson.fromJson(value, Array::class.java).toList() 27 | } 28 | 29 | @TypeConverter 30 | fun fromWallpaperList(value: List): String? = gson.toJson(value) 31 | 32 | @TypeConverter 33 | fun toWallpaperList(value: String?): List { 34 | if (value == null) return emptyList() 35 | val listType = object : TypeToken>() {}.type 36 | return gson.fromJson(value, listType) 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /app/src/main/java/com/anthonyla/paperize/feature/wallpaper/domain/model/Album.kt: -------------------------------------------------------------------------------- 1 | package com.anthonyla.paperize.feature.wallpaper.domain.model 2 | 3 | import androidx.room.Entity 4 | import androidx.room.PrimaryKey 5 | 6 | /** 7 | * Album model 8 | * 9 | * @param initialAlbumName The initial album name -- should not be changed as it is used as the key for database queries 10 | * @param displayedAlbumName The displayed album name 11 | * @param coverUri The cover uri of the album 12 | * @param homeWallpapersInQueue The list of home wallpapers in the queue 13 | * @param lockWallpapersInQueue The list of lock wallpapers in the queue 14 | * @param selected Whether the album is selected 15 | */ 16 | @Entity 17 | data class Album( 18 | @PrimaryKey(autoGenerate = false) val initialAlbumName: String, 19 | val displayedAlbumName: String, 20 | val coverUri: String?, 21 | val homeWallpapersInQueue: List = emptyList(), 22 | val lockWallpapersInQueue: List = emptyList(), 23 | val selected : Boolean = false 24 | ) -------------------------------------------------------------------------------- /app/src/main/java/com/anthonyla/paperize/feature/wallpaper/domain/model/AlbumWithWallpaperAndFolder.kt: -------------------------------------------------------------------------------- 1 | package com.anthonyla.paperize.feature.wallpaper.domain.model 2 | 3 | import androidx.room.Embedded 4 | import androidx.room.Relation 5 | 6 | /** 7 | * Complete data class that represents an album with its wallpapers and folders. 8 | * @param album The album. 9 | * @param wallpapers The wallpapers. 10 | * @param folders The folders. 11 | * */ 12 | data class AlbumWithWallpaperAndFolder( 13 | @Embedded val album: Album, 14 | @Relation( 15 | parentColumn = "initialAlbumName", 16 | entityColumn = "initialAlbumName", 17 | entity = Wallpaper::class, 18 | ) 19 | val wallpapers: List = emptyList(), 20 | @Relation( 21 | parentColumn = "initialAlbumName", 22 | entityColumn = "initialAlbumName", 23 | entity = Folder::class 24 | ) 25 | val folders: List = emptyList(), 26 | ) { 27 | val totalWallpapers: List 28 | get() = folders.flatMap { it.wallpapers } + wallpapers 29 | val sortedFolders: List 30 | get() = folders.map { folder -> 31 | folder.copy(wallpapers = folder.wallpapers.sortedBy { it.order }) 32 | }.sortedBy { it.order } 33 | val sortedWallpapers: List 34 | get() = wallpapers.sortedBy { it.order } 35 | val sortedTotalWallpapers: List 36 | get() = (folders.flatMap { it.wallpapers } + wallpapers).sortedBy { it.order } 37 | } -------------------------------------------------------------------------------- /app/src/main/java/com/anthonyla/paperize/feature/wallpaper/domain/model/Folder.kt: -------------------------------------------------------------------------------- 1 | package com.anthonyla.paperize.feature.wallpaper.domain.model 2 | 3 | import androidx.room.Entity 4 | import androidx.room.PrimaryKey 5 | 6 | /** 7 | * Represents a folder directory containing wallpapers 8 | * 9 | * @param initialAlbumName The initial album name 10 | * @param folderName The folder name 11 | * @param folderUri The folder directory URI 12 | * @param coverUri The cover URI 13 | * @param dateModified The date modified of the folder 14 | * @param wallpapers The list of wallpaper URI strings in the folder 15 | * @param order The order of the folder 16 | */ 17 | @Entity 18 | data class Folder( 19 | val initialAlbumName: String, 20 | val folderName: String?, 21 | val folderUri: String, 22 | val coverUri: String?, 23 | val dateModified: Long, 24 | val wallpapers: List = emptyList(), 25 | val order: Int, 26 | @PrimaryKey(autoGenerate = false) val key: Int 27 | ) -------------------------------------------------------------------------------- /app/src/main/java/com/anthonyla/paperize/feature/wallpaper/domain/model/Metadata.kt: -------------------------------------------------------------------------------- 1 | package com.anthonyla.paperize.feature.wallpaper.domain.model 2 | 3 | data class Metadata( 4 | val filename: String, 5 | val lastModified: Long 6 | ) 7 | -------------------------------------------------------------------------------- /app/src/main/java/com/anthonyla/paperize/feature/wallpaper/domain/model/Wallpaper.kt: -------------------------------------------------------------------------------- 1 | package com.anthonyla.paperize.feature.wallpaper.domain.model 2 | 3 | import androidx.room.Entity 4 | import androidx.room.PrimaryKey 5 | 6 | /** 7 | * Represents a wallpaper object 8 | * 9 | * @param initialAlbumName The name of the album the wallpaper belongs to 10 | * @param wallpaperUri The URI of the wallpaper 11 | * @param fileName The name of the file 12 | * @param dateModified The date the wallpaper was last modified 13 | */ 14 | @Entity 15 | data class Wallpaper( 16 | val initialAlbumName: String, 17 | val wallpaperUri: String, 18 | val fileName: String, 19 | val dateModified: Long, 20 | val order: Int, 21 | @PrimaryKey(autoGenerate = false) val key: Int 22 | ) -------------------------------------------------------------------------------- /app/src/main/java/com/anthonyla/paperize/feature/wallpaper/domain/repository/AlbumRepository.kt: -------------------------------------------------------------------------------- 1 | package com.anthonyla.paperize.feature.wallpaper.domain.repository 2 | 3 | import com.anthonyla.paperize.feature.wallpaper.domain.model.Album 4 | import com.anthonyla.paperize.feature.wallpaper.domain.model.AlbumWithWallpaperAndFolder 5 | import com.anthonyla.paperize.feature.wallpaper.domain.model.Folder 6 | import com.anthonyla.paperize.feature.wallpaper.domain.model.Wallpaper 7 | import kotlinx.coroutines.flow.Flow 8 | 9 | interface AlbumRepository { 10 | /** 11 | * Get all albums with wallpaper and folder 12 | */ 13 | fun getAlbumsWithWallpaperAndFolder(): Flow> 14 | 15 | /** 16 | * Get all selected albums with wallpaper and folder 17 | */ 18 | fun getSelectedAlbums(): Flow> 19 | 20 | /** 21 | * Update album selection 22 | */ 23 | suspend fun updateAlbumSelection(albumName: String, selected: Boolean) 24 | 25 | /** 26 | * Deselect all albums 27 | */ 28 | suspend fun deselectAllAlbums() 29 | 30 | /** 31 | * Insert or update album with wallpaper and folder 32 | */ 33 | suspend fun upsertAlbumWithWallpaperAndFolder(albumWithWallpaperAndFolder: AlbumWithWallpaperAndFolder) 34 | 35 | /** 36 | * Insert or update album and its wallpaper and folder 37 | */ 38 | suspend fun upsertAlbum(album: Album) 39 | 40 | /** 41 | * Delete album and its wallpaper and folder 42 | */ 43 | suspend fun deleteAlbum(album: Album) 44 | 45 | /** 46 | * Update album 47 | */ 48 | suspend fun updateAlbum(album: Album) 49 | 50 | /** 51 | * Cascade delete album and its wallpaper and folder. Proper way to delete an AlbumWithWallpaperAndFolder 52 | */ 53 | suspend fun cascadeDeleteAlbum(album: Album) 54 | 55 | /** 56 | * Insert or update wallpaper 57 | */ 58 | suspend fun upsertWallpaper(wallpaper: Wallpaper) 59 | 60 | /** 61 | * Insert or update wallpaper list in one transaction 62 | */ 63 | suspend fun upsertWallpaperList(wallpapers: List) 64 | 65 | /** 66 | * Delete wallpaper 67 | */ 68 | suspend fun deleteWallpaper(wallpaper: Wallpaper) 69 | 70 | /** 71 | * Delete wallpaper list in one transaction 72 | */ 73 | suspend fun deleteWallpaperList(wallpapers: List) 74 | 75 | /** 76 | * Update wallpaper 77 | */ 78 | suspend fun updateWallpaper(wallpaper: Wallpaper) 79 | 80 | /** 81 | * Cascade delete wallpaper. Proper way to delete a single wallpaper 82 | */ 83 | suspend fun cascadeDeleteWallpaper(initialAlbumName: String) 84 | 85 | /** 86 | * Insert or update folder 87 | */ 88 | suspend fun upsertFolder(folder: Folder) 89 | 90 | /** 91 | * Insert or update folder list in one transaction 92 | */ 93 | suspend fun upsertFolderList(folders: List) 94 | 95 | /** 96 | * Delete folder 97 | */ 98 | suspend fun deleteFolder(folder: Folder) 99 | 100 | /** 101 | * Update folder 102 | */ 103 | suspend fun updateFolder(folder: Folder) 104 | 105 | /** 106 | * Cascade delete folder. Proper way to delete a single folder 107 | */ 108 | suspend fun cascadeDeleteFolder(initialAlbumName: String) 109 | 110 | /** 111 | * Delete folder list in one transaction 112 | */ 113 | suspend fun deleteFolderList(folders: List) 114 | 115 | /** 116 | * Delete all data in database 117 | */ 118 | suspend fun deleteAllData() 119 | } -------------------------------------------------------------------------------- /app/src/main/java/com/anthonyla/paperize/feature/wallpaper/glance_widget/PaperizeWidget.kt: -------------------------------------------------------------------------------- 1 | package com.anthonyla.paperize.feature.wallpaper.glance_widget 2 | 3 | import android.content.Context 4 | import android.content.Intent 5 | import androidx.compose.runtime.Composable 6 | import androidx.glance.GlanceId 7 | import androidx.glance.GlanceModifier 8 | import androidx.glance.Image 9 | import androidx.glance.ImageProvider 10 | import androidx.glance.action.clickable 11 | import androidx.glance.appwidget.GlanceAppWidget 12 | import androidx.glance.appwidget.provideContent 13 | import androidx.glance.layout.Box 14 | import androidx.glance.layout.ContentScale 15 | import androidx.glance.layout.fillMaxSize 16 | import com.anthonyla.paperize.R 17 | import androidx.glance.ColorFilter 18 | import androidx.glance.GlanceTheme 19 | import com.anthonyla.paperize.feature.wallpaper.wallpaper_alarmmanager.WallpaperBootAndChangeReceiver 20 | 21 | class PaperizeWidget : GlanceAppWidget() { 22 | override suspend fun provideGlance(context: Context, id: GlanceId) { 23 | provideContent { 24 | WidgetContent(context) 25 | } 26 | } 27 | } 28 | 29 | @Composable 30 | private fun WidgetContent(context: Context) { 31 | val ACTION_CHANGE_WALLPAPER = "com.anthonyla.paperize.SHORTCUT" 32 | Box( 33 | modifier = GlanceModifier 34 | .fillMaxSize() 35 | .clickable( 36 | { 37 | 38 | val intent = Intent(ACTION_CHANGE_WALLPAPER).apply { 39 | setClass(context, WallpaperBootAndChangeReceiver::class.java) 40 | addFlags(Intent.FLAG_INCLUDE_STOPPED_PACKAGES) 41 | } 42 | context.sendBroadcast(intent) 43 | } 44 | ) 45 | ) { 46 | Image( 47 | provider = ImageProvider(R.drawable.next_wallpaper_icon), 48 | contentDescription = "Paperize Widget", 49 | modifier = GlanceModifier.fillMaxSize(), 50 | contentScale = ContentScale.Fit, 51 | colorFilter = ColorFilter.tint(GlanceTheme.colors.primaryContainer) 52 | ) 53 | } 54 | } 55 | -------------------------------------------------------------------------------- /app/src/main/java/com/anthonyla/paperize/feature/wallpaper/glance_widget/PaperizeWidgetReceiver.kt: -------------------------------------------------------------------------------- 1 | package com.anthonyla.paperize.feature.wallpaper.glance_widget 2 | import androidx.glance.appwidget.GlanceAppWidget 3 | import androidx.glance.appwidget.GlanceAppWidgetReceiver 4 | 5 | class PaperizeWidgetReceiver : GlanceAppWidgetReceiver() { 6 | override val glanceAppWidget: GlanceAppWidget = PaperizeWidget() 7 | } -------------------------------------------------------------------------------- /app/src/main/java/com/anthonyla/paperize/feature/wallpaper/presentation/add_album_screen/AddAlbumEvent.kt: -------------------------------------------------------------------------------- 1 | package com.anthonyla.paperize.feature.wallpaper.presentation.add_album_screen 2 | 3 | import com.anthonyla.paperize.feature.wallpaper.domain.model.Folder 4 | import com.anthonyla.paperize.feature.wallpaper.domain.model.Wallpaper 5 | 6 | sealed class AddAlbumEvent { 7 | data object Reset: AddAlbumEvent() 8 | data object DeleteSelected: AddAlbumEvent() 9 | data object SelectAll: AddAlbumEvent() 10 | data object DeselectAll: AddAlbumEvent() 11 | data class SaveAlbum( 12 | val initialAlbumName: String 13 | ): AddAlbumEvent() 14 | data class AddWallpapers( 15 | val wallpaperUris: List 16 | ): AddAlbumEvent() 17 | data class AddFolder( 18 | val directoryUri: String 19 | ): AddAlbumEvent() 20 | data class SelectWallpaper( 21 | val wallpaperUri: String 22 | ): AddAlbumEvent() 23 | data class SelectFolder( 24 | val directoryUri: String 25 | ): AddAlbumEvent() 26 | data class DeselectFolder( 27 | val directoryUri: String 28 | ): AddAlbumEvent() 29 | data class DeselectWallpaper( 30 | val wallpaperUri: String 31 | ): AddAlbumEvent() 32 | data class SetLoading( 33 | val isLoading: Boolean 34 | ): AddAlbumEvent() 35 | data class LoadFoldersAndWallpapers( 36 | val folders: List, 37 | val wallpapers: List 38 | ): AddAlbumEvent() 39 | } -------------------------------------------------------------------------------- /app/src/main/java/com/anthonyla/paperize/feature/wallpaper/presentation/add_album_screen/AddAlbumState.kt: -------------------------------------------------------------------------------- 1 | package com.anthonyla.paperize.feature.wallpaper.presentation.add_album_screen 2 | 3 | import com.anthonyla.paperize.feature.wallpaper.domain.model.Folder 4 | import com.anthonyla.paperize.feature.wallpaper.domain.model.Wallpaper 5 | 6 | data class AddAlbumState( 7 | val wallpapers: List = emptyList(), 8 | val folders: List = emptyList(), 9 | val selectionState: SelectionState = SelectionState(), 10 | val isEmpty: Boolean = true, 11 | val isLoading: Boolean = false 12 | ) 13 | 14 | data class SelectionState( 15 | val selectedWallpapers: List = emptyList(), 16 | val selectedFolders: List = emptyList(), 17 | val allSelected: Boolean = false, 18 | val selectedCount: Int = 0 19 | ) -------------------------------------------------------------------------------- /app/src/main/java/com/anthonyla/paperize/feature/wallpaper/presentation/add_album_screen/components/DeleteImagesAlertDialog.kt: -------------------------------------------------------------------------------- 1 | package com.anthonyla.paperize.feature.wallpaper.presentation.add_album_screen.components 2 | 3 | import androidx.compose.material.icons.Icons 4 | import androidx.compose.material.icons.outlined.Delete 5 | import androidx.compose.material3.AlertDialog 6 | import androidx.compose.material3.Icon 7 | import androidx.compose.material3.Text 8 | import androidx.compose.material3.TextButton 9 | import androidx.compose.runtime.Composable 10 | import androidx.compose.ui.res.stringResource 11 | import com.anthonyla.paperize.R 12 | 13 | /** 14 | * AlertDialog to confirm deletion of images 15 | */ 16 | @Composable 17 | fun DeleteImagesAlertDialog( 18 | onDismissRequest: () -> Unit, 19 | onConfirmation: () -> Unit, 20 | ) { 21 | AlertDialog( 22 | icon = { 23 | Icon(Icons.Outlined.Delete, contentDescription = stringResource(R.string.delete_confirmation)) 24 | }, 25 | title = { Text(text = stringResource(R.string.delete_these)) }, 26 | text = { Text(text = stringResource(R.string.are_you_sure_you_want_to_delete_these_wallpapers)) }, 27 | onDismissRequest = { onDismissRequest() }, 28 | confirmButton = { 29 | TextButton(onClick = { onConfirmation() }) { 30 | Text(stringResource(R.string.confirm)) 31 | } 32 | }, 33 | dismissButton = { 34 | TextButton(onClick = { onDismissRequest() }) { 35 | Text(stringResource(R.string.cancel)) 36 | } 37 | } 38 | ) 39 | } -------------------------------------------------------------------------------- /app/src/main/java/com/anthonyla/paperize/feature/wallpaper/presentation/album/AlbumsEvent.kt: -------------------------------------------------------------------------------- 1 | package com.anthonyla.paperize.feature.wallpaper.presentation.album 2 | 3 | import com.anthonyla.paperize.feature.wallpaper.domain.model.AlbumWithWallpaperAndFolder 4 | 5 | sealed class AlbumsEvent { 6 | data object DeselectSelected: AlbumsEvent() 7 | data object Reset: AlbumsEvent() 8 | data object Refresh: AlbumsEvent() 9 | 10 | data class AddSelectedAlbum( 11 | val album: AlbumWithWallpaperAndFolder, 12 | val deselectAlbumName: String? = null, 13 | val shuffle: Boolean = true 14 | ): AlbumsEvent() 15 | data class RemoveSelectedAlbum(val deselectAlbumName: String): AlbumsEvent() 16 | } -------------------------------------------------------------------------------- /app/src/main/java/com/anthonyla/paperize/feature/wallpaper/presentation/album/AlbumsState.kt: -------------------------------------------------------------------------------- 1 | package com.anthonyla.paperize.feature.wallpaper.presentation.album 2 | 3 | import com.anthonyla.paperize.feature.wallpaper.domain.model.AlbumWithWallpaperAndFolder 4 | 5 | data class AlbumsState( 6 | val albumsWithWallpapers: List = emptyList(), 7 | val selectedAlbum: List = emptyList() 8 | ) -------------------------------------------------------------------------------- /app/src/main/java/com/anthonyla/paperize/feature/wallpaper/presentation/album/components/AlbumItem.kt: -------------------------------------------------------------------------------- 1 | package com.anthonyla.paperize.feature.wallpaper.presentation.album.components 2 | 3 | import androidx.compose.foundation.layout.Arrangement 4 | import androidx.compose.foundation.layout.Box 5 | import androidx.compose.foundation.layout.Column 6 | import androidx.compose.foundation.layout.Spacer 7 | import androidx.compose.foundation.layout.aspectRatio 8 | import androidx.compose.foundation.layout.fillMaxSize 9 | import androidx.compose.foundation.layout.padding 10 | import androidx.compose.foundation.shape.RoundedCornerShape 11 | import androidx.compose.material3.Card 12 | import androidx.compose.material3.CardDefaults 13 | import androidx.compose.material3.MaterialTheme 14 | import androidx.compose.material3.Text 15 | import androidx.compose.runtime.Composable 16 | import androidx.compose.runtime.getValue 17 | import androidx.compose.runtime.mutableStateOf 18 | import androidx.compose.runtime.remember 19 | import androidx.compose.ui.Alignment 20 | import androidx.compose.ui.Modifier 21 | import androidx.compose.ui.draw.clip 22 | import androidx.compose.ui.layout.ContentScale 23 | import androidx.compose.ui.platform.LocalContext 24 | import androidx.compose.ui.unit.IntSize 25 | import androidx.compose.ui.unit.dp 26 | import androidx.compose.ui.unit.sp 27 | import androidx.core.net.toUri 28 | import com.anthonyla.paperize.core.decompress 29 | import com.anthonyla.paperize.core.isValidUri 30 | import com.anthonyla.paperize.feature.wallpaper.domain.model.Album 31 | import com.skydoves.landscapist.ImageOptions 32 | import com.skydoves.landscapist.glide.GlideImage 33 | 34 | /** 35 | * AlbumItem composable is a single item view for the Album list. Shows the thumbnail cover image. 36 | */ 37 | @Composable 38 | fun AlbumItem( 39 | album: Album, 40 | onAlbumViewClick: () -> Unit, 41 | modifier: Modifier = Modifier, 42 | ) { 43 | val context = LocalContext.current 44 | val showCoverUri by remember { mutableStateOf(!album.coverUri.isNullOrEmpty() && isValidUri(context, album.coverUri)) } 45 | 46 | Card( 47 | colors = CardDefaults.cardColors(containerColor = MaterialTheme.colorScheme.surfaceVariant), 48 | modifier = modifier.fillMaxSize(), 49 | shape = RoundedCornerShape(16.dp), 50 | onClick = onAlbumViewClick 51 | ) { 52 | Column( 53 | verticalArrangement = Arrangement.Top, 54 | horizontalAlignment = Alignment.CenterHorizontally 55 | ) { 56 | Box { 57 | if (showCoverUri) { 58 | GlideImage( 59 | imageModel = { 60 | album.coverUri?.decompress("content://com.android.externalstorage.documents/")?.toUri() 61 | }, 62 | imageOptions = ImageOptions( 63 | contentScale = ContentScale.Crop, 64 | alignment = Alignment.Center, 65 | requestSize = IntSize(300, 300), 66 | ), 67 | modifier = Modifier 68 | .aspectRatio(1f) 69 | .clip(RoundedCornerShape(16.dp)) 70 | ) 71 | } 72 | } 73 | 74 | Spacer(modifier = Modifier.padding(6.dp)) 75 | Text( 76 | text = album.displayedAlbumName, 77 | modifier = Modifier 78 | .padding(horizontal = 24.dp) 79 | .align(Alignment.Start), 80 | fontSize = 16.sp, 81 | style = MaterialTheme.typography.titleSmall 82 | ) 83 | Spacer(modifier = Modifier.padding(8.dp)) 84 | } 85 | } 86 | } -------------------------------------------------------------------------------- /app/src/main/java/com/anthonyla/paperize/feature/wallpaper/presentation/album/components/WallpaperItem.kt: -------------------------------------------------------------------------------- 1 | package com.anthonyla.paperize.feature.wallpaper.presentation.album.components 2 | 3 | import android.content.Context 4 | import androidx.compose.animation.core.animateDp 5 | import androidx.compose.animation.core.updateTransition 6 | import androidx.compose.foundation.ExperimentalFoundationApi 7 | import androidx.compose.foundation.background 8 | import androidx.compose.foundation.border 9 | import androidx.compose.foundation.combinedClickable 10 | import androidx.compose.foundation.layout.Box 11 | import androidx.compose.foundation.layout.fillMaxSize 12 | import androidx.compose.foundation.layout.padding 13 | import androidx.compose.foundation.shape.CircleShape 14 | import androidx.compose.foundation.shape.RoundedCornerShape 15 | import androidx.compose.material.icons.Icons 16 | import androidx.compose.material.icons.filled.CheckCircle 17 | import androidx.compose.material.icons.filled.RadioButtonUnchecked 18 | import androidx.compose.material3.Icon 19 | import androidx.compose.material3.MaterialTheme 20 | import androidx.compose.material3.surfaceColorAtElevation 21 | import androidx.compose.runtime.Composable 22 | import androidx.compose.runtime.getValue 23 | import androidx.compose.runtime.mutableStateOf 24 | import androidx.compose.runtime.remember 25 | import androidx.compose.ui.Alignment 26 | import androidx.compose.ui.Modifier 27 | import androidx.compose.ui.draw.clip 28 | import androidx.compose.ui.graphics.Color 29 | import androidx.compose.ui.hapticfeedback.HapticFeedbackType 30 | import androidx.compose.ui.platform.LocalContext 31 | import androidx.compose.ui.platform.LocalHapticFeedback 32 | import androidx.compose.ui.res.stringResource 33 | import androidx.compose.ui.unit.IntSize 34 | import androidx.compose.ui.unit.dp 35 | import androidx.core.net.toUri 36 | import com.anthonyla.paperize.R 37 | import com.anthonyla.paperize.core.decompress 38 | import com.anthonyla.paperize.core.isValidUri 39 | import com.skydoves.landscapist.ImageOptions 40 | import com.skydoves.landscapist.glide.GlideImage 41 | 42 | /** 43 | * A composable that displays a wallpaper item. It shows the wallpaper image and a selection icon when selected 44 | */ 45 | @OptIn(ExperimentalFoundationApi::class) 46 | @Composable 47 | fun WallpaperItem( 48 | wallpaperUri: String, 49 | itemSelected: Boolean, 50 | selectionMode: Boolean, 51 | modifier: Modifier = Modifier, 52 | allowHapticFeedback: Boolean = true, 53 | onItemSelection: () -> Unit, 54 | onWallpaperViewClick: () -> Unit, 55 | ) { 56 | val context = LocalContext.current 57 | val haptics = LocalHapticFeedback.current 58 | val transition = updateTransition(itemSelected, label = "") 59 | 60 | val paddingTransition by transition.animateDp(label = "") { selected -> 61 | if (selected) 5.dp else 0.dp 62 | } 63 | val roundedCornerShapeTransition by transition.animateDp(label = "") { selected -> 64 | if (selected) 24.dp else 16.dp 65 | } 66 | 67 | val showUri by remember { mutableStateOf(isValidUri(context, wallpaperUri)) } 68 | 69 | Box( 70 | modifier = modifier 71 | .padding(paddingTransition) 72 | .clip(RoundedCornerShape(roundedCornerShapeTransition)) 73 | .combinedClickable( 74 | onClick = { 75 | if (!selectionMode) { 76 | onWallpaperViewClick() 77 | } else { 78 | onItemSelection() 79 | } 80 | }, 81 | onLongClick = { 82 | if (!selectionMode) { 83 | if (allowHapticFeedback) haptics.performHapticFeedback(HapticFeedbackType.LongPress) 84 | onItemSelection() 85 | } 86 | } 87 | ) 88 | ) { 89 | if (showUri) { 90 | GlideImage( 91 | imageModel = { wallpaperUri.decompress("content://com.android.externalstorage.documents/").toUri() }, 92 | imageOptions = ImageOptions( 93 | requestSize = IntSize(200, 200), 94 | alignment = Alignment.Center, 95 | ), 96 | modifier = Modifier 97 | .fillMaxSize() 98 | .clip(RoundedCornerShape(roundedCornerShapeTransition)) 99 | ) 100 | } 101 | if (selectionMode) { 102 | val bgColor = MaterialTheme.colorScheme.surfaceColorAtElevation(5.dp) 103 | if (itemSelected) { 104 | Icon( 105 | imageVector = Icons.Default.CheckCircle, 106 | contentDescription = stringResource(R.string.image_is_selected), 107 | modifier = Modifier 108 | .padding(9.dp) 109 | .clip(CircleShape) 110 | .background(bgColor) 111 | .border(2.dp, bgColor, CircleShape) 112 | ) 113 | } else { 114 | Icon( 115 | imageVector = Icons.Default.RadioButtonUnchecked, 116 | contentDescription = stringResource(R.string.image_is_not_selected), 117 | tint = Color.White.copy(alpha = 0.7f), 118 | modifier = Modifier.padding(6.dp) 119 | ) 120 | } 121 | } 122 | } 123 | } -------------------------------------------------------------------------------- /app/src/main/java/com/anthonyla/paperize/feature/wallpaper/presentation/album_view_screen/AlbumViewEvent.kt: -------------------------------------------------------------------------------- 1 | package com.anthonyla.paperize.feature.wallpaper.presentation.album_view_screen 2 | 3 | import com.anthonyla.paperize.feature.wallpaper.domain.model.Folder 4 | import com.anthonyla.paperize.feature.wallpaper.domain.model.Wallpaper 5 | import com.anthonyla.paperize.feature.wallpaper.presentation.add_album_screen.AddAlbumEvent 6 | 7 | sealed class AlbumViewEvent { 8 | data object Reset: AlbumViewEvent() 9 | data object SelectAll: AlbumViewEvent() 10 | data object DeselectAll: AlbumViewEvent() 11 | data object DeleteSelected: AlbumViewEvent() 12 | data object DeleteAlbum: AlbumViewEvent() 13 | 14 | data class LoadAlbum( 15 | val initialAlbumName: String 16 | ): AlbumViewEvent() 17 | data class AddWallpapers( 18 | val wallpaperUris: List 19 | ): AlbumViewEvent() 20 | data class AddFolder( 21 | val directoryUri: String 22 | ): AlbumViewEvent() 23 | data class SelectWallpaper( 24 | val wallpaperUri: String 25 | ): AlbumViewEvent() 26 | data class SelectFolder( 27 | val directoryUri: String 28 | ): AlbumViewEvent() 29 | data class DeselectFolder( 30 | val directoryUri: String 31 | ): AlbumViewEvent() 32 | data class DeselectWallpaper( 33 | val wallpaperUri: String 34 | ): AlbumViewEvent() 35 | data class ChangeAlbumName( 36 | val displayName: String 37 | ): AlbumViewEvent() 38 | data class SetLoading( 39 | val isLoading: Boolean 40 | ): AlbumViewEvent() 41 | data class LoadFoldersAndWallpapers( 42 | val folders: List, 43 | val wallpapers: List 44 | ): AlbumViewEvent() 45 | } -------------------------------------------------------------------------------- /app/src/main/java/com/anthonyla/paperize/feature/wallpaper/presentation/album_view_screen/AlbumViewState.kt: -------------------------------------------------------------------------------- 1 | package com.anthonyla.paperize.feature.wallpaper.presentation.album_view_screen 2 | import com.anthonyla.paperize.feature.wallpaper.domain.model.AlbumWithWallpaperAndFolder 3 | import com.anthonyla.paperize.feature.wallpaper.presentation.add_album_screen.SelectionState 4 | 5 | data class AlbumViewState ( 6 | val albums: List = emptyList(), 7 | val initialAlbumName: String = "", 8 | val selectionState: SelectionState = SelectionState(), 9 | val isEmpty: Boolean = true, 10 | val isLoading: Boolean = false 11 | ) -------------------------------------------------------------------------------- /app/src/main/java/com/anthonyla/paperize/feature/wallpaper/presentation/album_view_screen/components/AlbumNameDialog.kt: -------------------------------------------------------------------------------- 1 | package com.anthonyla.paperize.feature.wallpaper.presentation.album_view_screen.components 2 | 3 | import androidx.compose.foundation.layout.Arrangement 4 | import androidx.compose.foundation.layout.Column 5 | import androidx.compose.foundation.layout.Row 6 | import androidx.compose.foundation.layout.Spacer 7 | import androidx.compose.foundation.layout.fillMaxWidth 8 | import androidx.compose.foundation.layout.padding 9 | import androidx.compose.foundation.shape.RoundedCornerShape 10 | import androidx.compose.foundation.text.KeyboardActions 11 | import androidx.compose.foundation.text.KeyboardOptions 12 | import androidx.compose.material3.Card 13 | import androidx.compose.material3.CardDefaults 14 | import androidx.compose.material3.MaterialTheme 15 | import androidx.compose.material3.OutlinedTextField 16 | import androidx.compose.material3.Text 17 | import androidx.compose.material3.TextButton 18 | import androidx.compose.runtime.Composable 19 | import androidx.compose.runtime.LaunchedEffect 20 | import androidx.compose.runtime.getValue 21 | import androidx.compose.runtime.mutableStateOf 22 | import androidx.compose.runtime.remember 23 | import androidx.compose.runtime.saveable.rememberSaveable 24 | import androidx.compose.runtime.setValue 25 | import androidx.compose.ui.Alignment 26 | import androidx.compose.ui.Modifier 27 | import androidx.compose.ui.focus.FocusRequester 28 | import androidx.compose.ui.focus.focusRequester 29 | import androidx.compose.ui.res.stringResource 30 | import androidx.compose.ui.text.input.ImeAction 31 | import androidx.compose.ui.text.input.KeyboardType 32 | import androidx.compose.ui.unit.dp 33 | import androidx.compose.ui.window.Dialog 34 | import com.anthonyla.paperize.R 35 | import kotlinx.coroutines.android.awaitFrame 36 | 37 | /** 38 | * Dialog for creating a new album. Input of album name is required. 39 | */ 40 | @Composable 41 | fun AlbumNameDialog( 42 | onDismissRequest: () -> Unit, 43 | onConfirmation: (String) -> Unit 44 | ) { 45 | val focusRequester = remember { FocusRequester() } 46 | var albumName by rememberSaveable { mutableStateOf("") } 47 | Dialog(onDismissRequest = onDismissRequest) { 48 | Card( 49 | modifier = Modifier 50 | .fillMaxWidth() 51 | .padding(24.dp), 52 | shape = RoundedCornerShape(16.dp), 53 | colors = CardDefaults.cardColors( 54 | containerColor = MaterialTheme.colorScheme.surface, 55 | contentColor = MaterialTheme.colorScheme.onSurface 56 | ) 57 | ) { 58 | Column( 59 | verticalArrangement = Arrangement.Top, 60 | horizontalAlignment = Alignment.CenterHorizontally, 61 | ) { 62 | LaunchedEffect(focusRequester) { 63 | awaitFrame() 64 | focusRequester.requestFocus() 65 | } 66 | Spacer(modifier = Modifier.padding(10.dp)) 67 | OutlinedTextField( 68 | value = albumName, 69 | onValueChange = { albumName = it }, 70 | label = { Text(stringResource(R.string.album_name)) }, 71 | modifier = Modifier 72 | .padding(vertical = 12.dp, horizontal = 32.dp) 73 | .focusRequester(focusRequester), 74 | singleLine = true, 75 | shape = RoundedCornerShape(8.dp), 76 | keyboardOptions = KeyboardOptions( 77 | keyboardType = KeyboardType.Text, 78 | imeAction = ImeAction.Done, 79 | ), 80 | keyboardActions = KeyboardActions( 81 | onDone = { 82 | if (albumName.isNotEmpty()) { 83 | onConfirmation(albumName) 84 | onDismissRequest() 85 | } 86 | } 87 | ) 88 | ) 89 | } 90 | Row( 91 | modifier = Modifier.fillMaxWidth(), 92 | horizontalArrangement = Arrangement.Center, 93 | ) { 94 | TextButton( 95 | onClick = { onDismissRequest() }, 96 | modifier = Modifier.padding(8.dp), 97 | ) { 98 | Text(stringResource(R.string.cancel)) 99 | } 100 | TextButton( 101 | modifier = Modifier.padding(8.dp), 102 | onClick = { 103 | if (albumName.isNotEmpty()) { 104 | onConfirmation(albumName) 105 | onDismissRequest() 106 | } 107 | } 108 | ) { 109 | Text(stringResource(R.string.save)) 110 | } 111 | } 112 | } 113 | } 114 | } 115 | -------------------------------------------------------------------------------- /app/src/main/java/com/anthonyla/paperize/feature/wallpaper/presentation/album_view_screen/components/DeleteAlbumAlertDialog.kt: -------------------------------------------------------------------------------- 1 | package com.anthonyla.paperize.feature.wallpaper.presentation.album_view_screen.components 2 | 3 | import androidx.compose.material.icons.Icons 4 | import androidx.compose.material.icons.outlined.Delete 5 | import androidx.compose.material3.AlertDialog 6 | import androidx.compose.material3.Icon 7 | import androidx.compose.material3.Text 8 | import androidx.compose.material3.TextButton 9 | import androidx.compose.runtime.Composable 10 | import androidx.compose.ui.res.stringResource 11 | import com.anthonyla.paperize.R 12 | 13 | /** 14 | * AlertDialog for confirmation of deleting an album 15 | */ 16 | @Composable 17 | fun DeleteAlbumAlertDialog( 18 | onDismissRequest: () -> Unit, 19 | onConfirmation: () -> Unit, 20 | ) { 21 | AlertDialog( 22 | icon = { 23 | Icon(Icons.Outlined.Delete, contentDescription = stringResource(R.string.delete_confirmation)) 24 | }, 25 | title = { Text(text = stringResource(R.string.delete_album_question)) }, 26 | text = { Text(text = stringResource(R.string.are_you_sure_you_want_to_delete_this)) }, 27 | onDismissRequest = { onDismissRequest() }, 28 | confirmButton = { 29 | TextButton(onClick = { onConfirmation() }) { 30 | Text(stringResource(R.string.confirm)) 31 | } 32 | }, 33 | dismissButton = { 34 | TextButton(onClick = { onDismissRequest() }) { 35 | Text(stringResource(R.string.cancel)) 36 | } 37 | } 38 | ) 39 | } -------------------------------------------------------------------------------- /app/src/main/java/com/anthonyla/paperize/feature/wallpaper/presentation/folder_view_screen/FolderEvent.kt: -------------------------------------------------------------------------------- 1 | package com.anthonyla.paperize.feature.wallpaper.presentation.folder_view_screen 2 | 3 | import com.anthonyla.paperize.feature.wallpaper.domain.model.Folder 4 | 5 | sealed class FolderEvent { 6 | data object Reset: FolderEvent() 7 | 8 | data class LoadFolderView( 9 | val folder: Folder? = null 10 | ): FolderEvent() 11 | } -------------------------------------------------------------------------------- /app/src/main/java/com/anthonyla/paperize/feature/wallpaper/presentation/folder_view_screen/FolderState.kt: -------------------------------------------------------------------------------- 1 | package com.anthonyla.paperize.feature.wallpaper.presentation.folder_view_screen 2 | 3 | import com.anthonyla.paperize.feature.wallpaper.domain.model.Folder 4 | import com.anthonyla.paperize.feature.wallpaper.domain.model.Wallpaper 5 | 6 | data class FolderState( 7 | val folder: Folder? = null, 8 | ) 9 | -------------------------------------------------------------------------------- /app/src/main/java/com/anthonyla/paperize/feature/wallpaper/presentation/folder_view_screen/FolderViewModel.kt: -------------------------------------------------------------------------------- 1 | package com.anthonyla.paperize.feature.wallpaper.presentation.folder_view_screen 2 | 3 | import androidx.compose.runtime.mutableStateOf 4 | import androidx.lifecycle.ViewModel 5 | import androidx.lifecycle.viewModelScope 6 | import com.anthonyla.paperize.feature.wallpaper.domain.model.Folder 7 | import com.anthonyla.paperize.feature.wallpaper.presentation.sort_view_screen.SortEvent 8 | import com.anthonyla.paperize.feature.wallpaper.presentation.sort_view_screen.SortState 9 | import kotlinx.coroutines.flow.MutableStateFlow 10 | import kotlinx.coroutines.flow.SharingStarted 11 | import kotlinx.coroutines.flow.stateIn 12 | import javax.inject.Inject 13 | 14 | /** 15 | * ViewModel for the folder view screen to hold the folder name and the list of wallpapers 16 | */ 17 | class FolderViewModel @Inject constructor (): ViewModel() { 18 | private val _state = MutableStateFlow(FolderState()) 19 | val state = _state.stateIn( 20 | scope = viewModelScope, 21 | started = SharingStarted.WhileSubscribed(5000), 22 | initialValue = FolderState() 23 | ) 24 | 25 | fun onEvent(event: FolderEvent) { 26 | when (event) { 27 | is FolderEvent.LoadFolderView -> { 28 | _state.value = state.value.copy( 29 | folder = event.folder 30 | ) 31 | } 32 | is FolderEvent.Reset -> { 33 | _state.value = FolderState() 34 | } 35 | } 36 | } 37 | } -------------------------------------------------------------------------------- /app/src/main/java/com/anthonyla/paperize/feature/wallpaper/presentation/folder_view_screen/FolderViewScreen.kt: -------------------------------------------------------------------------------- 1 | package com.anthonyla.paperize.feature.wallpaper.presentation.folder_view_screen 2 | 3 | import androidx.activity.compose.BackHandler 4 | import androidx.compose.animation.core.FastOutSlowInEasing 5 | import androidx.compose.animation.core.tween 6 | import androidx.compose.foundation.layout.Arrangement 7 | import androidx.compose.foundation.layout.PaddingValues 8 | import androidx.compose.foundation.layout.fillMaxSize 9 | import androidx.compose.foundation.layout.padding 10 | import androidx.compose.foundation.layout.size 11 | import androidx.compose.foundation.lazy.grid.GridCells 12 | import androidx.compose.foundation.lazy.grid.LazyVerticalGrid 13 | import androidx.compose.foundation.lazy.grid.items 14 | import androidx.compose.foundation.lazy.grid.rememberLazyGridState 15 | import androidx.compose.foundation.shape.RoundedCornerShape 16 | import androidx.compose.material3.MaterialTheme 17 | import androidx.compose.material3.Scaffold 18 | import androidx.compose.runtime.Composable 19 | import androidx.compose.ui.Modifier 20 | import androidx.compose.ui.unit.dp 21 | import com.anthonyla.paperize.feature.wallpaper.domain.model.Folder 22 | import com.anthonyla.paperize.feature.wallpaper.presentation.album.components.WallpaperItem 23 | import com.anthonyla.paperize.feature.wallpaper.presentation.folder_view_screen.components.FolderViewTopBar 24 | import my.nanihadesuka.compose.LazyVerticalGridScrollbar 25 | import my.nanihadesuka.compose.ScrollbarSettings 26 | 27 | @Composable 28 | fun FolderViewScreen( 29 | folder: Folder, 30 | onBackClick: () -> Unit, 31 | onShowWallpaperView: (String, String) -> Unit, 32 | animate: Boolean 33 | ) { 34 | val lazyListState = rememberLazyGridState() 35 | BackHandler { onBackClick() } 36 | 37 | Scaffold( 38 | topBar = { 39 | FolderViewTopBar( 40 | title = folder.folderName ?: "", 41 | onBackClick = onBackClick 42 | ) 43 | }, 44 | content = { 45 | LazyVerticalGridScrollbar( 46 | state = lazyListState, 47 | settings = ScrollbarSettings.Default.copy( 48 | thumbUnselectedColor = MaterialTheme.colorScheme.primary, 49 | thumbSelectedColor = MaterialTheme.colorScheme.primary, 50 | thumbShape = RoundedCornerShape(16.dp), 51 | scrollbarPadding = 1.dp, 52 | ), 53 | modifier = Modifier 54 | .fillMaxSize() 55 | .padding(it) 56 | ) { 57 | LazyVerticalGrid( 58 | state = lazyListState, 59 | modifier = Modifier.fillMaxSize(), 60 | columns = GridCells.Adaptive(150.dp), 61 | contentPadding = PaddingValues(4.dp, 4.dp), 62 | horizontalArrangement = Arrangement.Start, 63 | ) { 64 | items(items = folder.wallpapers, key = { wallpaper -> wallpaper.wallpaperUri }) { wallpaper -> 65 | WallpaperItem( 66 | wallpaperUri = wallpaper.wallpaperUri, 67 | itemSelected = false, 68 | selectionMode = false, 69 | allowHapticFeedback = false, 70 | onItemSelection = {}, 71 | onWallpaperViewClick = { 72 | onShowWallpaperView(wallpaper.wallpaperUri, wallpaper.fileName) 73 | }, 74 | modifier = Modifier 75 | .padding(4.dp) 76 | .size(150.dp, 350.dp) 77 | .then( 78 | if (animate) Modifier.animateItem( 79 | placementSpec = tween( 80 | durationMillis = 800, 81 | delayMillis = 0, 82 | easing = FastOutSlowInEasing 83 | ) 84 | ) else Modifier 85 | ) 86 | ) 87 | } 88 | } 89 | } 90 | } 91 | ) 92 | } -------------------------------------------------------------------------------- /app/src/main/java/com/anthonyla/paperize/feature/wallpaper/presentation/folder_view_screen/components/FolderViewTopBar.kt: -------------------------------------------------------------------------------- 1 | package com.anthonyla.paperize.feature.wallpaper.presentation.folder_view_screen.components 2 | 3 | import androidx.compose.foundation.layout.padding 4 | import androidx.compose.foundation.layout.requiredSize 5 | import androidx.compose.material.icons.Icons 6 | import androidx.compose.material.icons.automirrored.filled.ArrowBack 7 | import androidx.compose.material3.ExperimentalMaterial3Api 8 | import androidx.compose.material3.Icon 9 | import androidx.compose.material3.IconButton 10 | import androidx.compose.material3.MaterialTheme 11 | import androidx.compose.material3.Text 12 | import androidx.compose.material3.TopAppBar 13 | import androidx.compose.material3.TopAppBarDefaults 14 | import androidx.compose.material3.TopAppBarDefaults.topAppBarColors 15 | import androidx.compose.material3.surfaceColorAtElevation 16 | import androidx.compose.runtime.Composable 17 | import androidx.compose.ui.Modifier 18 | import androidx.compose.ui.res.stringResource 19 | import androidx.compose.ui.unit.dp 20 | import com.anthonyla.paperize.R 21 | 22 | /** 23 | * Top bar for the folder view screen. 24 | */ 25 | @OptIn(ExperimentalMaterial3Api::class) 26 | @Composable 27 | fun FolderViewTopBar( 28 | title: String, 29 | onBackClick: () -> Unit 30 | ) { 31 | TopAppBar( 32 | colors = topAppBarColors(containerColor = MaterialTheme.colorScheme.surfaceColorAtElevation(5.dp)), 33 | title = { Text(title) }, 34 | navigationIcon = { 35 | IconButton( 36 | onClick = onBackClick, 37 | modifier = Modifier 38 | .padding(16.dp) 39 | .requiredSize(24.dp) 40 | ) { 41 | Icon( 42 | imageVector = Icons.AutoMirrored.Filled.ArrowBack, 43 | contentDescription = stringResource(id = R.string.home_screen), 44 | ) 45 | } 46 | } 47 | ) 48 | } -------------------------------------------------------------------------------- /app/src/main/java/com/anthonyla/paperize/feature/wallpaper/presentation/home_screen/components/HomeTopBar.kt: -------------------------------------------------------------------------------- 1 | package com.anthonyla.paperize.feature.wallpaper.presentation.home_screen.components 2 | 3 | import androidx.compose.foundation.background 4 | import androidx.compose.foundation.border 5 | import androidx.compose.foundation.layout.Arrangement 6 | import androidx.compose.foundation.layout.Row 7 | import androidx.compose.foundation.layout.padding 8 | import androidx.compose.foundation.shape.CircleShape 9 | import androidx.compose.material.icons.Icons 10 | import androidx.compose.material.icons.filled.CheckCircle 11 | import androidx.compose.material.icons.outlined.Delete 12 | import androidx.compose.material.icons.outlined.Settings 13 | import androidx.compose.material3.CenterAlignedTopAppBar 14 | import androidx.compose.material3.ExperimentalMaterial3Api 15 | import androidx.compose.material3.Icon 16 | import androidx.compose.material3.IconButton 17 | import androidx.compose.material3.MaterialTheme 18 | import androidx.compose.material3.Text 19 | import androidx.compose.material3.TopAppBarDefaults 20 | import androidx.compose.material3.surfaceColorAtElevation 21 | import androidx.compose.runtime.Composable 22 | import androidx.compose.runtime.getValue 23 | import androidx.compose.runtime.mutableStateOf 24 | import androidx.compose.runtime.saveable.rememberSaveable 25 | import androidx.compose.runtime.setValue 26 | import androidx.compose.ui.Alignment 27 | import androidx.compose.ui.Modifier 28 | import androidx.compose.ui.draw.clip 29 | import androidx.compose.ui.graphics.Color 30 | import androidx.compose.ui.platform.testTag 31 | import androidx.compose.ui.res.stringResource 32 | import androidx.compose.ui.unit.dp 33 | import com.anthonyla.paperize.R 34 | import com.anthonyla.paperize.feature.wallpaper.presentation.add_album_screen.components.DeleteImagesAlertDialog 35 | 36 | /** 37 | * Top bar for the home screen 38 | */ 39 | @OptIn(ExperimentalMaterial3Api::class) 40 | @Composable 41 | fun HomeTopBar ( 42 | showSelectionModeAppBar: Boolean, 43 | selectionCount: Int, 44 | onSettingsClick: () -> Unit, 45 | ) { 46 | CenterAlignedTopAppBar( 47 | colors = TopAppBarDefaults.centerAlignedTopAppBarColors(containerColor = Color.Transparent), 48 | title = { if (!showSelectionModeAppBar) Text(stringResource(R.string.app_name)) }, 49 | actions = { 50 | if (showSelectionModeAppBar) { 51 | var showAlertDialog by rememberSaveable { mutableStateOf(false) } 52 | if (showAlertDialog) DeleteImagesAlertDialog ( 53 | onDismissRequest = { showAlertDialog = false }, 54 | onConfirmation = { 55 | showAlertDialog = false 56 | } 57 | ) 58 | IconButton( 59 | onClick = { showAlertDialog = true } 60 | ) { 61 | Icon( 62 | imageVector = Icons.Outlined.Delete, 63 | contentDescription = stringResource(R.string.select_all_images_for_deletion), 64 | tint = Color.White.copy(alpha = 0.7f), 65 | modifier = Modifier.padding(6.dp) 66 | ) 67 | } 68 | } 69 | }, 70 | navigationIcon = { 71 | if (showSelectionModeAppBar) { 72 | Row ( 73 | horizontalArrangement = Arrangement.Start, 74 | verticalAlignment = Alignment.CenterVertically 75 | ) { 76 | IconButton( 77 | onClick = { } 78 | ) { 79 | val bgColor = MaterialTheme.colorScheme.surfaceColorAtElevation(5.dp) 80 | Icon( 81 | imageVector = Icons.Default.CheckCircle, 82 | contentDescription = stringResource(R.string.all_images_selected_for_deletion), 83 | modifier = Modifier 84 | .padding(4.dp) 85 | .border(2.dp, bgColor, CircleShape) 86 | .clip(CircleShape) 87 | .background(bgColor) 88 | ) 89 | } 90 | Text(stringResource(R.string.selected_count, selectionCount)) 91 | } 92 | } else { 93 | IconButton( 94 | onClick = { onSettingsClick() } 95 | ) { 96 | Icon( 97 | imageVector = Icons.Outlined.Settings, 98 | contentDescription = stringResource(R.string.SettingsIcon), 99 | ) 100 | } 101 | } 102 | } 103 | ) 104 | } -------------------------------------------------------------------------------- /app/src/main/java/com/anthonyla/paperize/feature/wallpaper/presentation/home_screen/components/TabItem.kt: -------------------------------------------------------------------------------- 1 | package com.anthonyla.paperize.feature.wallpaper.presentation.home_screen.components 2 | 3 | import androidx.compose.material.icons.Icons 4 | import androidx.compose.material.icons.filled.AddToPhotos 5 | import androidx.compose.material.icons.filled.Image 6 | import androidx.compose.material.icons.outlined.AddToPhotos 7 | import androidx.compose.material.icons.outlined.Image 8 | import androidx.compose.runtime.Composable 9 | import androidx.compose.ui.graphics.vector.ImageVector 10 | import androidx.compose.ui.platform.LocalContext 11 | import com.anthonyla.paperize.R 12 | 13 | /** 14 | * Data class representing a tab item 15 | */ 16 | data class TabItem( 17 | val title: String, 18 | val filledIcon: ImageVector, 19 | val unfilledIcon: ImageVector, 20 | ) 21 | 22 | @Composable 23 | fun getTabItems(): List { 24 | val context = LocalContext.current 25 | return listOf( 26 | TabItem( 27 | title = context.getString(R.string.wallpaper_screen), 28 | filledIcon = Icons.Filled.Image, 29 | unfilledIcon = Icons.Outlined.Image 30 | ), 31 | TabItem( 32 | title = context.getString(R.string.library_screen), 33 | filledIcon = Icons.Filled.AddToPhotos, 34 | unfilledIcon = Icons.Outlined.AddToPhotos 35 | ) 36 | ) 37 | } -------------------------------------------------------------------------------- /app/src/main/java/com/anthonyla/paperize/feature/wallpaper/presentation/library_screen/LibraryScreen.kt: -------------------------------------------------------------------------------- 1 | package com.anthonyla.paperize.feature.wallpaper.presentation.library_screen 2 | 3 | import androidx.compose.foundation.layout.Arrangement 4 | import androidx.compose.foundation.layout.PaddingValues 5 | import androidx.compose.foundation.layout.fillMaxSize 6 | import androidx.compose.foundation.layout.padding 7 | import androidx.compose.foundation.lazy.grid.GridCells 8 | import androidx.compose.foundation.lazy.grid.LazyVerticalGrid 9 | import androidx.compose.foundation.lazy.grid.items 10 | import androidx.compose.foundation.lazy.grid.rememberLazyGridState 11 | import androidx.compose.material.icons.Icons 12 | import androidx.compose.material.icons.filled.Add 13 | import androidx.compose.material3.Icon 14 | import androidx.compose.material3.LargeFloatingActionButton 15 | import androidx.compose.material3.MaterialTheme 16 | import androidx.compose.material3.Scaffold 17 | import androidx.compose.runtime.Composable 18 | import androidx.compose.ui.ExperimentalComposeUiApi 19 | import androidx.compose.ui.Modifier 20 | import androidx.compose.ui.res.stringResource 21 | import androidx.compose.ui.unit.dp 22 | import com.anthonyla.paperize.R 23 | import com.anthonyla.paperize.feature.wallpaper.domain.model.AlbumWithWallpaperAndFolder 24 | import com.anthonyla.paperize.feature.wallpaper.presentation.album.components.AlbumItem 25 | 26 | @OptIn(ExperimentalComposeUiApi::class) 27 | @Composable 28 | fun LibraryScreen( 29 | albums: List, 30 | onAddNewAlbumClick: () -> Unit, 31 | onViewAlbum: (String) -> Unit 32 | ) { 33 | val lazyListState = rememberLazyGridState() 34 | Scaffold( 35 | modifier = Modifier.fillMaxSize(), 36 | floatingActionButton = { 37 | LargeFloatingActionButton( 38 | containerColor = MaterialTheme.colorScheme.primaryContainer, 39 | contentColor = MaterialTheme.colorScheme.onPrimaryContainer, 40 | onClick = onAddNewAlbumClick, 41 | ) { 42 | Icon( 43 | Icons.Filled.Add, 44 | contentDescription = stringResource(R.string.add_album), 45 | modifier = Modifier.padding(16.dp), 46 | ) 47 | } 48 | }, 49 | content = { it 50 | LazyVerticalGrid( 51 | state = lazyListState, 52 | modifier = Modifier.fillMaxSize(), 53 | columns = GridCells.Adaptive(150.dp), 54 | contentPadding = PaddingValues(4.dp, 4.dp), 55 | horizontalArrangement = Arrangement.Start, 56 | content = { 57 | items (albums, key = { album -> album.album.initialAlbumName} 58 | ) { album -> 59 | AlbumItem( 60 | album = album.album, 61 | onAlbumViewClick = { onViewAlbum(album.album.initialAlbumName) }, 62 | modifier = Modifier.padding(4.dp), 63 | ) 64 | } 65 | } 66 | ) 67 | } 68 | ) 69 | } -------------------------------------------------------------------------------- /app/src/main/java/com/anthonyla/paperize/feature/wallpaper/presentation/settings_screen/SettingsEvent.kt: -------------------------------------------------------------------------------- 1 | package com.anthonyla.paperize.feature.wallpaper.presentation.settings_screen 2 | 3 | import com.anthonyla.paperize.core.ScalingConstants 4 | 5 | 6 | sealed class SettingsEvent { 7 | data object Reset: SettingsEvent() 8 | data object SetFirstLaunch: SettingsEvent() 9 | data object RefreshNextSetTime: SettingsEvent() 10 | data class SetDarkMode(val darkMode: Boolean?): SettingsEvent() 11 | data class SetAmoledTheme(val amoledTheme: Boolean): SettingsEvent() 12 | data class SetDynamicTheming(val dynamicTheming: Boolean): SettingsEvent() 13 | data class SetAnimate(val animate: Boolean): SettingsEvent() 14 | data class SetDarken(val darken: Boolean): SettingsEvent() 15 | data class SetBlur(val blur: Boolean): SettingsEvent() 16 | data class SetVignette(val vignette: Boolean): SettingsEvent() 17 | data class SetGrayscale(val grayscale: Boolean): SettingsEvent() 18 | data class SetLock(val lock: Boolean): SettingsEvent() 19 | data class SetHome(val home: Boolean): SettingsEvent() 20 | data class SetChangeStartTime(val changeStartTime: Boolean): SettingsEvent() 21 | data class SetCurrentWallpaper(val currentHomeWallpaper: String?, val currentLockWallpaper: String?): SettingsEvent() 22 | data class SetAlbum(val homeAlbumName: String? = null, val lockAlbumName: String? = null): SettingsEvent() 23 | data class RemoveSelectedAlbumAsType(val removeLock: Boolean = false, val removeHome: Boolean = false): SettingsEvent() 24 | data class RemoveSelectedAlbumAsName(val albumName: String): SettingsEvent() 25 | data class SetScheduleSeparately(val scheduleSeparately: Boolean): SettingsEvent() 26 | data class SetHomeWallpaperInterval(val interval: Int): SettingsEvent() 27 | data class SetLockWallpaperInterval(val interval: Int): SettingsEvent() 28 | data class SetDarkenPercentage(val homeDarkenPercentage: Int?, val lockDarkenPercentage: Int?): SettingsEvent() 29 | data class SetBlurPercentage(val homeBlurPercentage: Int?, val lockBlurPercentage: Int?): SettingsEvent() 30 | data class SetVignettePercentage(val homeVignettePercentage: Int?, val lockVignettePercentage: Int?): SettingsEvent() 31 | data class SetGrayscalePercentage(val homeGrayscalePercentage: Int?, val lockGrayscalePercentage: Int?): SettingsEvent() 32 | data class SetChangerToggle(val toggle: Boolean): SettingsEvent() 33 | data class SetWallpaperScaling(val scaling: ScalingConstants): SettingsEvent() 34 | data class SetStartTime(val hour: Int, val minute: Int): SettingsEvent() 35 | data class SetShuffle(val shuffle: Boolean): SettingsEvent() 36 | data class SetRefresh(val refresh: Boolean): SettingsEvent() 37 | } -------------------------------------------------------------------------------- /app/src/main/java/com/anthonyla/paperize/feature/wallpaper/presentation/settings_screen/SettingsState.kt: -------------------------------------------------------------------------------- 1 | package com.anthonyla.paperize.feature.wallpaper.presentation.settings_screen 2 | 3 | import com.anthonyla.paperize.core.ScalingConstants 4 | import com.anthonyla.paperize.core.SettingsConstants.WALLPAPER_CHANGE_INTERVAL_DEFAULT 5 | 6 | data class SettingsState( 7 | val firstLaunch: Boolean = true, 8 | val initialized: Boolean = false, 9 | val themeSettings: ThemeSettings = ThemeSettings(), 10 | val wallpaperSettings: WallpaperSettings = WallpaperSettings(), 11 | val scheduleSettings: ScheduleSettings = ScheduleSettings(), 12 | val effectSettings: EffectSettings = EffectSettings() 13 | ) { 14 | data class ThemeSettings( 15 | val darkMode: Boolean? = null, 16 | val amoledTheme: Boolean = false, 17 | val dynamicTheming: Boolean = false, 18 | val animate: Boolean = true 19 | ) 20 | 21 | data class WallpaperSettings( 22 | val enableChanger: Boolean = false, 23 | val setHomeWallpaper: Boolean = false, 24 | val setLockWallpaper: Boolean = false, 25 | val currentHomeWallpaper: String? = null, 26 | val currentLockWallpaper: String? = null, 27 | val nextHomeWallpaper: String? = null, 28 | val nextLockWallpaper: String? = null, 29 | val homeAlbumName: String? = null, 30 | val lockAlbumName: String? = null, 31 | val wallpaperScaling: ScalingConstants = ScalingConstants.FILL 32 | ) 33 | 34 | data class ScheduleSettings( 35 | val scheduleSeparately: Boolean = false, 36 | val shuffle: Boolean = true, 37 | val homeInterval: Int = WALLPAPER_CHANGE_INTERVAL_DEFAULT, 38 | val lockInterval: Int = WALLPAPER_CHANGE_INTERVAL_DEFAULT, 39 | val lastSetTime: String? = null, 40 | val nextSetTime: String? = null, 41 | val changeStartTime: Boolean = false, 42 | val startTime: Pair = Pair(0, 0), 43 | val refresh: Boolean = true 44 | ) 45 | 46 | data class EffectSettings( 47 | val darken: Boolean = false, 48 | val homeDarkenPercentage: Int = 100, 49 | val lockDarkenPercentage: Int = 100, 50 | val blur: Boolean = false, 51 | val homeBlurPercentage: Int = 0, 52 | val lockBlurPercentage: Int = 0, 53 | val vignette: Boolean = false, 54 | val homeVignettePercentage: Int = 0, 55 | val lockVignettePercentage: Int = 0, 56 | val grayscale: Boolean = false, 57 | val homeGrayscalePercentage: Int = 0, 58 | val lockGrayscalePercentage: Int = 0 59 | ) 60 | 61 | data class ServiceSettings( 62 | val enableChanger: Boolean, 63 | val setHome: Boolean, 64 | val setLock: Boolean, 65 | val scaling: ScalingConstants, 66 | val darken: Boolean, 67 | val homeDarkenPercentage: Int, 68 | val lockDarkenPercentage: Int, 69 | val blur: Boolean, 70 | val homeBlurPercentage: Int, 71 | val lockBlurPercentage: Int, 72 | val vignette: Boolean, 73 | val homeVignettePercentage: Int, 74 | val lockVignettePercentage: Int, 75 | val grayscale: Boolean, 76 | val homeGrayscalePercentage: Int, 77 | val lockGrayscalePercentage: Int, 78 | val lockAlbumName: String, 79 | val homeAlbumName: String, 80 | val shuffle: Boolean 81 | ) 82 | } -------------------------------------------------------------------------------- /app/src/main/java/com/anthonyla/paperize/feature/wallpaper/presentation/settings_screen/components/AmoledListItem.kt: -------------------------------------------------------------------------------- 1 | package com.anthonyla.paperize.feature.wallpaper.presentation.settings_screen.components 2 | 3 | import androidx.compose.foundation.clickable 4 | import androidx.compose.foundation.layout.Row 5 | import androidx.compose.foundation.shape.RoundedCornerShape 6 | import androidx.compose.material.icons.Icons 7 | import androidx.compose.material.icons.filled.DarkMode 8 | import androidx.compose.material3.Icon 9 | import androidx.compose.material3.ListItem 10 | import androidx.compose.material3.MaterialTheme 11 | import androidx.compose.material3.Switch 12 | import androidx.compose.material3.Text 13 | import androidx.compose.runtime.Composable 14 | import androidx.compose.ui.Modifier 15 | import androidx.compose.ui.draw.clip 16 | import androidx.compose.ui.platform.LocalContext 17 | import androidx.compose.ui.res.stringResource 18 | import androidx.compose.ui.semantics.contentDescription 19 | import androidx.compose.ui.semantics.semantics 20 | import androidx.compose.ui.unit.dp 21 | import com.anthonyla.paperize.R 22 | 23 | /** 24 | * AMOLED list item to enable AMOLED mode 25 | */ 26 | @Composable 27 | fun AmoledListItem(amoledMode: Boolean, onAmoledClick: (Boolean) -> Unit) { 28 | val context = LocalContext.current 29 | Row { 30 | ListItem( 31 | modifier = Modifier 32 | .clip(RoundedCornerShape(16.dp)) 33 | .clickable { onAmoledClick(!amoledMode) }, 34 | headlineContent = { 35 | Text( 36 | text = stringResource(R.string.amoled_mode), 37 | style = MaterialTheme.typography.titleMedium 38 | 39 | ) }, 40 | supportingContent = { 41 | Text( 42 | text = stringResource(R.string.full_dark_mode), 43 | style = MaterialTheme.typography.bodySmall 44 | ) }, 45 | trailingContent = { 46 | Switch( 47 | modifier = Modifier.semantics { contentDescription = context.getString(R.string.amoled_mode) }, 48 | checked = amoledMode, 49 | onCheckedChange = onAmoledClick, 50 | enabled = true 51 | ) }, 52 | leadingContent = { 53 | Icon( 54 | Icons.Filled.DarkMode, 55 | contentDescription = stringResource(R.string.amoled_mode), 56 | tint = MaterialTheme.colorScheme.primary 57 | ) }, 58 | tonalElevation = 5.dp 59 | ) 60 | } 61 | } -------------------------------------------------------------------------------- /app/src/main/java/com/anthonyla/paperize/feature/wallpaper/presentation/settings_screen/components/AnimationListItem.kt: -------------------------------------------------------------------------------- 1 | package com.anthonyla.paperize.feature.wallpaper.presentation.settings_screen.components 2 | 3 | import androidx.compose.foundation.clickable 4 | import androidx.compose.foundation.layout.Row 5 | import androidx.compose.foundation.shape.RoundedCornerShape 6 | import androidx.compose.material.icons.Icons 7 | import androidx.compose.material.icons.outlined.Animation 8 | import androidx.compose.material3.Icon 9 | import androidx.compose.material3.ListItem 10 | import androidx.compose.material3.MaterialTheme 11 | import androidx.compose.material3.Switch 12 | import androidx.compose.material3.Text 13 | import androidx.compose.runtime.Composable 14 | import androidx.compose.ui.Modifier 15 | import androidx.compose.ui.draw.clip 16 | import androidx.compose.ui.platform.LocalContext 17 | import androidx.compose.ui.res.stringResource 18 | import androidx.compose.ui.semantics.contentDescription 19 | import androidx.compose.ui.semantics.semantics 20 | import androidx.compose.ui.unit.dp 21 | import com.anthonyla.paperize.R 22 | 23 | /** 24 | * Enable animation switch 25 | */ 26 | @Composable 27 | fun AnimationListItem(animate: Boolean, onAnimateClick: (Boolean) -> Unit) { 28 | val context = LocalContext.current 29 | Row { 30 | ListItem( 31 | modifier = Modifier 32 | .clip(RoundedCornerShape(16.dp)) 33 | .clickable { onAnimateClick(!animate) }, 34 | headlineContent = { 35 | Text( 36 | text = stringResource(R.string.animation), 37 | style = MaterialTheme.typography.titleMedium 38 | ) }, 39 | supportingContent = { 40 | Text( 41 | text = stringResource(R.string.increase_visual_appeal), 42 | style = MaterialTheme.typography.bodySmall 43 | ) }, 44 | trailingContent = { 45 | Switch( 46 | modifier = Modifier.semantics { contentDescription = context.getString(R.string.animation) }, 47 | checked = animate, 48 | onCheckedChange = onAnimateClick, 49 | enabled = true 50 | ) }, 51 | leadingContent = { 52 | Icon( 53 | Icons.Outlined.Animation, 54 | contentDescription = stringResource(R.string.animation), 55 | tint = MaterialTheme.colorScheme.primary 56 | ) }, 57 | tonalElevation = 5.dp 58 | ) 59 | } 60 | } -------------------------------------------------------------------------------- /app/src/main/java/com/anthonyla/paperize/feature/wallpaper/presentation/settings_screen/components/ContactListItem.kt: -------------------------------------------------------------------------------- 1 | package com.anthonyla.paperize.feature.wallpaper.presentation.settings_screen.components 2 | 3 | import androidx.compose.foundation.clickable 4 | import androidx.compose.foundation.layout.Row 5 | import androidx.compose.foundation.shape.RoundedCornerShape 6 | import androidx.compose.material.icons.Icons 7 | import androidx.compose.material.icons.outlined.Email 8 | import androidx.compose.material3.Icon 9 | import androidx.compose.material3.ListItem 10 | import androidx.compose.material3.MaterialTheme 11 | import androidx.compose.material3.Text 12 | import androidx.compose.runtime.Composable 13 | import androidx.compose.ui.Modifier 14 | import androidx.compose.ui.draw.clip 15 | import androidx.compose.ui.res.stringResource 16 | import androidx.compose.ui.unit.dp 17 | import com.anthonyla.paperize.R 18 | 19 | /** 20 | * List item to send an intent to contact author through email 21 | */ 22 | @Composable 23 | fun ContactListItem(onContactClick: () -> Unit) { 24 | Row { 25 | ListItem( 26 | modifier = Modifier 27 | .clip(RoundedCornerShape(16.dp)) 28 | .clickable { onContactClick() }, 29 | headlineContent = { 30 | Text( 31 | text = stringResource(R.string.contact), 32 | style = MaterialTheme.typography.titleMedium 33 | ) }, 34 | supportingContent = { 35 | Text( 36 | text = stringResource(R.string.contact_the_author_by_email), 37 | style = MaterialTheme.typography.bodySmall 38 | ) }, 39 | trailingContent = {}, 40 | leadingContent = { 41 | Icon( 42 | Icons.Outlined.Email, 43 | contentDescription = stringResource(R.string.contact), 44 | tint = MaterialTheme.colorScheme.primary 45 | ) }, 46 | tonalElevation = 5.dp 47 | ) 48 | } 49 | } -------------------------------------------------------------------------------- /app/src/main/java/com/anthonyla/paperize/feature/wallpaper/presentation/settings_screen/components/DynamicThemingListItem.kt: -------------------------------------------------------------------------------- 1 | package com.anthonyla.paperize.feature.wallpaper.presentation.settings_screen.components 2 | 3 | import androidx.compose.foundation.clickable 4 | import androidx.compose.foundation.layout.Row 5 | import androidx.compose.foundation.shape.RoundedCornerShape 6 | import androidx.compose.material.icons.Icons 7 | import androidx.compose.material.icons.outlined.Palette 8 | import androidx.compose.material3.Icon 9 | import androidx.compose.material3.ListItem 10 | import androidx.compose.material3.MaterialTheme 11 | import androidx.compose.material3.Switch 12 | import androidx.compose.material3.Text 13 | import androidx.compose.runtime.Composable 14 | import androidx.compose.ui.Modifier 15 | import androidx.compose.ui.draw.clip 16 | import androidx.compose.ui.platform.LocalContext 17 | import androidx.compose.ui.res.stringResource 18 | import androidx.compose.ui.semantics.contentDescription 19 | import androidx.compose.ui.semantics.semantics 20 | import androidx.compose.ui.unit.dp 21 | import com.anthonyla.paperize.R 22 | 23 | /** 24 | * Dynamic theming switch 25 | */ 26 | @Composable 27 | fun DynamicThemingListItem(dynamicTheming: Boolean, onDynamicThemingClick: (Boolean) -> Unit) { 28 | val context = LocalContext.current 29 | Row { 30 | ListItem( 31 | modifier = Modifier 32 | .clip(RoundedCornerShape(16.dp)) 33 | .clickable { onDynamicThemingClick(!dynamicTheming) }, 34 | headlineContent = { 35 | Text( 36 | text = stringResource(R.string.dynamic_theme), 37 | style = MaterialTheme.typography.titleMedium 38 | 39 | ) }, 40 | supportingContent = { 41 | Text( 42 | text = stringResource(R.string.material_you), 43 | style = MaterialTheme.typography.bodySmall 44 | ) }, 45 | trailingContent = { 46 | Switch( 47 | modifier = Modifier.semantics { contentDescription = context.getString(R.string.dynamic_theme) }, 48 | checked = dynamicTheming, 49 | onCheckedChange = onDynamicThemingClick, 50 | enabled = true 51 | ) }, 52 | leadingContent = { 53 | Icon( 54 | Icons.Outlined.Palette, 55 | contentDescription = stringResource(R.string.dynamic_theming), 56 | tint = MaterialTheme.colorScheme.primary 57 | ) }, 58 | tonalElevation = 5.dp 59 | ) 60 | } 61 | } -------------------------------------------------------------------------------- /app/src/main/java/com/anthonyla/paperize/feature/wallpaper/presentation/settings_screen/components/ListSectionTitle.kt: -------------------------------------------------------------------------------- 1 | package com.anthonyla.paperize.feature.wallpaper.presentation.settings_screen.components 2 | 3 | import androidx.compose.material3.MaterialTheme 4 | import androidx.compose.material3.Text 5 | import androidx.compose.runtime.Composable 6 | import androidx.compose.ui.text.font.FontWeight 7 | 8 | /** 9 | * A composable that displays a section title for a list. 10 | */ 11 | @Composable 12 | fun ListSectionTitle(title: String) { 13 | Text( 14 | text = title, 15 | style = MaterialTheme.typography.titleMedium, 16 | color = MaterialTheme.colorScheme.primary, 17 | fontWeight = FontWeight.Medium 18 | ) 19 | } -------------------------------------------------------------------------------- /app/src/main/java/com/anthonyla/paperize/feature/wallpaper/presentation/settings_screen/components/NotificationListItem.kt: -------------------------------------------------------------------------------- 1 | package com.anthonyla.paperize.feature.wallpaper.presentation.settings_screen.components 2 | 3 | import androidx.compose.foundation.clickable 4 | import androidx.compose.foundation.layout.Row 5 | import androidx.compose.foundation.shape.RoundedCornerShape 6 | import androidx.compose.material.icons.Icons 7 | import androidx.compose.material.icons.outlined.Notifications 8 | import androidx.compose.material3.Icon 9 | import androidx.compose.material3.ListItem 10 | import androidx.compose.material3.MaterialTheme 11 | import androidx.compose.material3.Text 12 | import androidx.compose.runtime.Composable 13 | import androidx.compose.ui.Modifier 14 | import androidx.compose.ui.draw.clip 15 | import androidx.compose.ui.res.stringResource 16 | import androidx.compose.ui.unit.dp 17 | import com.anthonyla.paperize.R 18 | 19 | /** 20 | * Notification list item to send to the notification permission page 21 | */ 22 | @Composable 23 | fun NotificationListItem(onClick: () -> Unit) { 24 | Row { 25 | ListItem( 26 | modifier = Modifier 27 | .clip(RoundedCornerShape(16.dp)) 28 | .clickable { onClick() }, 29 | headlineContent = { 30 | Text( 31 | text = stringResource(R.string.notifications), 32 | style = MaterialTheme.typography.titleMedium 33 | ) }, 34 | supportingContent = { 35 | Text( 36 | text = stringResource(R.string.change_notification_settings), 37 | style = MaterialTheme.typography.bodySmall 38 | ) }, 39 | trailingContent = {}, 40 | leadingContent = { 41 | Icon( 42 | Icons.Outlined.Notifications, 43 | contentDescription = stringResource(R.string.notifications), 44 | tint = MaterialTheme.colorScheme.primary 45 | ) }, 46 | tonalElevation = 5.dp 47 | ) 48 | } 49 | } 50 | -------------------------------------------------------------------------------- /app/src/main/java/com/anthonyla/paperize/feature/wallpaper/presentation/settings_screen/components/PrivacyPolicyListItem.kt: -------------------------------------------------------------------------------- 1 | package com.anthonyla.paperize.feature.wallpaper.presentation.settings_screen.components 2 | 3 | import androidx.compose.foundation.clickable 4 | import androidx.compose.foundation.layout.Row 5 | import androidx.compose.foundation.shape.RoundedCornerShape 6 | import androidx.compose.material.icons.Icons 7 | import androidx.compose.material.icons.outlined.PrivacyTip 8 | import androidx.compose.material3.Icon 9 | import androidx.compose.material3.ListItem 10 | import androidx.compose.material3.MaterialTheme 11 | import androidx.compose.material3.Text 12 | import androidx.compose.runtime.Composable 13 | import androidx.compose.ui.Modifier 14 | import androidx.compose.ui.draw.clip 15 | import androidx.compose.ui.res.stringResource 16 | import androidx.compose.ui.unit.dp 17 | import com.anthonyla.paperize.R 18 | 19 | /** 20 | * Privacy policy card 21 | */ 22 | @Composable 23 | fun PrivacyPolicyListItem(onPrivacyPolicyClick: () -> Unit) { 24 | Row { 25 | ListItem( 26 | modifier = Modifier 27 | .clip(RoundedCornerShape(16.dp)) 28 | .clickable { onPrivacyPolicyClick() }, 29 | headlineContent = { 30 | Text( 31 | text = stringResource(R.string.privacy_policy), 32 | style = MaterialTheme.typography.titleMedium 33 | ) }, 34 | supportingContent = { 35 | Text( 36 | text = stringResource(R.string.click_here_to_view_our_privacy_policy), 37 | style = MaterialTheme.typography.bodySmall 38 | ) }, 39 | trailingContent = {}, 40 | leadingContent = { 41 | Icon( 42 | Icons.Outlined.PrivacyTip, 43 | contentDescription = stringResource(R.string.privacy_policy), 44 | tint = MaterialTheme.colorScheme.primary 45 | ) }, 46 | tonalElevation = 5.dp 47 | ) 48 | } 49 | } -------------------------------------------------------------------------------- /app/src/main/java/com/anthonyla/paperize/feature/wallpaper/presentation/settings_screen/components/ResetListItem.kt: -------------------------------------------------------------------------------- 1 | package com.anthonyla.paperize.feature.wallpaper.presentation.settings_screen.components 2 | 3 | import androidx.compose.foundation.clickable 4 | import androidx.compose.foundation.layout.Arrangement 5 | import androidx.compose.foundation.layout.Box 6 | import androidx.compose.foundation.layout.Row 7 | import androidx.compose.foundation.layout.fillMaxWidth 8 | import androidx.compose.foundation.layout.padding 9 | import androidx.compose.foundation.shape.RoundedCornerShape 10 | import androidx.compose.material3.AlertDialog 11 | import androidx.compose.material3.Card 12 | import androidx.compose.material3.CardDefaults 13 | import androidx.compose.material3.MaterialTheme 14 | import androidx.compose.material3.Text 15 | import androidx.compose.material3.TextButton 16 | import androidx.compose.runtime.Composable 17 | import androidx.compose.runtime.getValue 18 | import androidx.compose.runtime.mutableStateOf 19 | import androidx.compose.runtime.remember 20 | import androidx.compose.runtime.setValue 21 | import androidx.compose.ui.Alignment 22 | import androidx.compose.ui.Modifier 23 | import androidx.compose.ui.draw.clip 24 | import androidx.compose.ui.res.stringResource 25 | import androidx.compose.ui.unit.dp 26 | import com.anthonyla.paperize.R 27 | 28 | /** 29 | * List item to reset all settings and data to default 30 | */ 31 | @Composable 32 | fun ResetListItem(onResetClick: () -> Unit) { 33 | var showDialog by remember { mutableStateOf(false) } 34 | 35 | if (showDialog) { 36 | AlertDialog( 37 | onDismissRequest = { showDialog = false }, 38 | title = { Text(stringResource(R.string.reset_all_data)) }, 39 | text = { Text(stringResource(R.string.are_you_sure_you_want_to_reset_all_settings_and_data_to_default)) }, 40 | confirmButton = { 41 | TextButton( 42 | onClick = { 43 | onResetClick() 44 | showDialog = false 45 | } 46 | ) { 47 | Text(stringResource(R.string.confirm)) 48 | } 49 | }, 50 | dismissButton = { 51 | TextButton( 52 | onClick = { showDialog = false } 53 | ) { 54 | Text(stringResource(R.string.cancel)) 55 | } 56 | } 57 | ) 58 | } 59 | 60 | Box ( 61 | modifier = Modifier 62 | .fillMaxWidth() 63 | ){ 64 | Card( 65 | modifier = Modifier 66 | .align(Alignment.Center) 67 | .clip(RoundedCornerShape(16.dp)) 68 | .clickable(onClick = { showDialog = true }), 69 | elevation = CardDefaults.cardElevation(10.dp), 70 | colors = CardDefaults.cardColors(MaterialTheme.colorScheme.errorContainer) 71 | ) { 72 | Row ( 73 | verticalAlignment = Alignment.CenterVertically, 74 | horizontalArrangement = Arrangement.Center, 75 | modifier = Modifier.padding(16.dp) 76 | ) { 77 | Text( 78 | text = stringResource(R.string.reset_to_default), 79 | style = MaterialTheme.typography.titleMedium, 80 | fontSize = MaterialTheme.typography.titleSmall.fontSize, 81 | ) 82 | } 83 | } 84 | } 85 | } -------------------------------------------------------------------------------- /app/src/main/java/com/anthonyla/paperize/feature/wallpaper/presentation/settings_screen/components/TranslateListItem.kt: -------------------------------------------------------------------------------- 1 | package com.anthonyla.paperize.feature.wallpaper.presentation.settings_screen.components 2 | 3 | import androidx.compose.foundation.clickable 4 | import androidx.compose.foundation.layout.Row 5 | import androidx.compose.foundation.shape.RoundedCornerShape 6 | import androidx.compose.material.icons.Icons 7 | import androidx.compose.material.icons.outlined.Translate 8 | import androidx.compose.material3.Icon 9 | import androidx.compose.material3.ListItem 10 | import androidx.compose.material3.MaterialTheme 11 | import androidx.compose.material3.Text 12 | import androidx.compose.runtime.Composable 13 | import androidx.compose.ui.Modifier 14 | import androidx.compose.ui.draw.clip 15 | import androidx.compose.ui.res.stringResource 16 | import androidx.compose.ui.unit.dp 17 | import com.anthonyla.paperize.R 18 | 19 | /** 20 | * A list item that links the user to the translation page 21 | */ 22 | @Composable 23 | fun TranslateListItem(onClick: () -> Unit) { 24 | Row { 25 | ListItem( 26 | modifier = Modifier 27 | .clip(RoundedCornerShape(16.dp)) 28 | .clickable { onClick() }, 29 | headlineContent = { 30 | Text( 31 | text = stringResource(R.string.translation), 32 | style = MaterialTheme.typography.titleMedium 33 | ) }, 34 | supportingContent = { 35 | Text( 36 | text = stringResource(R.string.help_translate_the_app_on_crowdin), 37 | style = MaterialTheme.typography.bodySmall 38 | ) }, 39 | trailingContent = {}, 40 | leadingContent = { 41 | Icon( 42 | Icons.Outlined.Translate, 43 | contentDescription = stringResource(R.string.translation), 44 | tint = MaterialTheme.colorScheme.primary 45 | ) }, 46 | tonalElevation = 5.dp 47 | ) 48 | } 49 | } -------------------------------------------------------------------------------- /app/src/main/java/com/anthonyla/paperize/feature/wallpaper/presentation/sort_view_screen/SortEvent.kt: -------------------------------------------------------------------------------- 1 | package com.anthonyla.paperize.feature.wallpaper.presentation.sort_view_screen 2 | 3 | import androidx.compose.foundation.lazy.LazyListItemInfo 4 | import com.anthonyla.paperize.feature.wallpaper.domain.model.Folder 5 | import com.anthonyla.paperize.feature.wallpaper.domain.model.Wallpaper 6 | 7 | sealed class SortEvent { 8 | data object Reset: SortEvent() 9 | 10 | data class LoadSortView( 11 | val folders: List, 12 | val wallpapers: List 13 | ): SortEvent() 14 | 15 | data class ShiftFolder( 16 | val from: LazyListItemInfo, 17 | val to: LazyListItemInfo 18 | ): SortEvent() 19 | 20 | data class ShiftWallpaper( 21 | val from: LazyListItemInfo, 22 | val to: LazyListItemInfo 23 | ): SortEvent() 24 | 25 | data class ShiftFolderWallpaper( 26 | val folderId: String, 27 | val from: LazyListItemInfo, 28 | val to: LazyListItemInfo 29 | ): SortEvent() 30 | 31 | data object SortAlphabetically : SortEvent() 32 | data object SortAlphabeticallyReverse : SortEvent() 33 | data object SortByLastModified : SortEvent() 34 | data object SortByLastModifiedReverse : SortEvent() 35 | } -------------------------------------------------------------------------------- /app/src/main/java/com/anthonyla/paperize/feature/wallpaper/presentation/sort_view_screen/SortState.kt: -------------------------------------------------------------------------------- 1 | package com.anthonyla.paperize.feature.wallpaper.presentation.sort_view_screen 2 | 3 | import com.anthonyla.paperize.feature.wallpaper.domain.model.Folder 4 | import com.anthonyla.paperize.feature.wallpaper.domain.model.Wallpaper 5 | 6 | data class SortState( 7 | val folders: List = emptyList(), 8 | val wallpapers: List = emptyList() 9 | ) 10 | -------------------------------------------------------------------------------- /app/src/main/java/com/anthonyla/paperize/feature/wallpaper/presentation/themes/Color.kt: -------------------------------------------------------------------------------- 1 | package com.anthonyla.paperize.feature.wallpaper.presentation.themes 2 | import androidx.compose.ui.graphics.Color 3 | 4 | val md_theme_light_primary = Color(0xFF984061) 5 | val md_theme_light_onPrimary = Color(0xFFFFFFFF) 6 | val md_theme_light_primaryContainer = Color(0xFFFFD9E2) 7 | val md_theme_light_onPrimaryContainer = Color(0xFF3E001D) 8 | val md_theme_light_secondary = Color(0xFF74565F) 9 | val md_theme_light_onSecondary = Color(0xFFFFFFFF) 10 | val md_theme_light_secondaryContainer = Color(0xFFFFD9E2) 11 | val md_theme_light_onSecondaryContainer = Color(0xFF2B151C) 12 | val md_theme_light_tertiary = Color(0xFF7C5635) 13 | val md_theme_light_onTertiary = Color(0xFFFFFFFF) 14 | val md_theme_light_tertiaryContainer = Color(0xFFFFDCC2) 15 | val md_theme_light_onTertiaryContainer = Color(0xFF2E1500) 16 | val md_theme_light_error = Color(0xFFBA1A1A) 17 | val md_theme_light_errorContainer = Color(0xFFFFDAD6) 18 | val md_theme_light_onError = Color(0xFFFFFFFF) 19 | val md_theme_light_onErrorContainer = Color(0xFF410002) 20 | val md_theme_light_background = Color(0xFFFFFBFF) 21 | val md_theme_light_onBackground = Color(0xFF201A1B) 22 | val md_theme_light_surface = Color(0xFFFFFBFF) 23 | val md_theme_light_onSurface = Color(0xFF201A1B) 24 | val md_theme_light_surfaceVariant = Color(0xFFF2DDE2) 25 | val md_theme_light_onSurfaceVariant = Color(0xFF514347) 26 | val md_theme_light_outline = Color(0xFF837377) 27 | val md_theme_light_inverseOnSurface = Color(0xFFFAEEEF) 28 | val md_theme_light_inverseSurface = Color(0xFF352F30) 29 | val md_theme_light_inversePrimary = Color(0xFFFFB0C8) 30 | val md_theme_light_shadow = Color(0xFF000000) 31 | val md_theme_light_surfaceTint = Color(0xFF984061) 32 | val md_theme_light_outlineVariant = Color(0xFFD5C2C6) 33 | val md_theme_light_scrim = Color(0xFF000000) 34 | 35 | val md_theme_dark_primary = Color(0xFFFFB0C8) 36 | val md_theme_dark_onPrimary = Color(0xFF5E1133) 37 | val md_theme_dark_primaryContainer = Color(0xFF7B2949) 38 | val md_theme_dark_onPrimaryContainer = Color(0xFFFFD9E2) 39 | val md_theme_dark_secondary = Color(0xFFE2BDC6) 40 | val md_theme_dark_onSecondary = Color(0xFF422931) 41 | val md_theme_dark_secondaryContainer = Color(0xFF5A3F47) 42 | val md_theme_dark_onSecondaryContainer = Color(0xFFFFD9E2) 43 | val md_theme_dark_tertiary = Color(0xFFEFBD94) 44 | val md_theme_dark_onTertiary = Color(0xFF48290C) 45 | val md_theme_dark_tertiaryContainer = Color(0xFF623F20) 46 | val md_theme_dark_onTertiaryContainer = Color(0xFFFFDCC2) 47 | val md_theme_dark_error = Color(0xFFFFB4AB) 48 | val md_theme_dark_errorContainer = Color(0xFF93000A) 49 | val md_theme_dark_onError = Color(0xFF690005) 50 | val md_theme_dark_onErrorContainer = Color(0xFFFFDAD6) 51 | val md_theme_dark_background = Color(0xFF201A1B) 52 | val md_theme_dark_onBackground = Color(0xFFEBE0E1) 53 | val md_theme_dark_surface = Color(0xFF201A1B) 54 | val md_theme_dark_onSurface = Color(0xFFEBE0E1) 55 | val md_theme_dark_surfaceVariant = Color(0xFF514347) 56 | val md_theme_dark_onSurfaceVariant = Color(0xFFD5C2C6) 57 | val md_theme_dark_outline = Color(0xFF9E8C90) 58 | val md_theme_dark_inverseOnSurface = Color(0xFF201A1B) 59 | val md_theme_dark_inverseSurface = Color(0xFFEBE0E1) 60 | val md_theme_dark_inversePrimary = Color(0xFF984061) 61 | val md_theme_dark_shadow = Color(0xFF000000) 62 | val md_theme_dark_surfaceTint = Color(0xFFFFB0C8) 63 | val md_theme_dark_outlineVariant = Color(0xFF514347) 64 | val md_theme_dark_scrim = Color(0xFF000000) 65 | 66 | 67 | val seed = Color(0xFF984061) 68 | -------------------------------------------------------------------------------- /app/src/main/java/com/anthonyla/paperize/feature/wallpaper/presentation/wallpaper_screen/components/ChangerSelectionRow.kt: -------------------------------------------------------------------------------- 1 | package com.anthonyla.paperize.feature.wallpaper.presentation.wallpaper_screen.components 2 | 3 | import androidx.compose.foundation.layout.PaddingValues 4 | import androidx.compose.foundation.layout.fillMaxWidth 5 | import androidx.compose.foundation.layout.padding 6 | import androidx.compose.material3.MultiChoiceSegmentedButtonRow 7 | import androidx.compose.material3.SegmentedButton 8 | import androidx.compose.material3.SegmentedButtonDefaults 9 | import androidx.compose.material3.Text 10 | import androidx.compose.runtime.Composable 11 | import androidx.compose.ui.Modifier 12 | import androidx.compose.ui.res.stringResource 13 | import androidx.compose.ui.text.style.TextOverflow 14 | import androidx.compose.ui.unit.dp 15 | import com.anthonyla.paperize.R 16 | 17 | @Composable 18 | fun ChangerSelectionRow( 19 | homeEnabled : Boolean, 20 | lockEnabled : Boolean, 21 | onHomeCheckedChange: (Boolean) -> Unit, 22 | onLockCheckedChange: (Boolean) -> Unit 23 | ) { 24 | MultiChoiceSegmentedButtonRow ( 25 | modifier = Modifier 26 | .fillMaxWidth() 27 | .padding(PaddingValues(start = 16.dp, end = 16.dp)) 28 | ) { 29 | SegmentedButton( 30 | shape = SegmentedButtonDefaults.itemShape(index = 0, count = 2), 31 | onCheckedChange = onLockCheckedChange, 32 | checked = lockEnabled, 33 | ) { 34 | Text( 35 | stringResource(R.string.lock_screen_btn), 36 | maxLines = 1, 37 | overflow = TextOverflow.Ellipsis, 38 | ) 39 | } 40 | SegmentedButton( 41 | shape = SegmentedButtonDefaults.itemShape(index = 1, count = 2), 42 | onCheckedChange = onHomeCheckedChange, 43 | checked = homeEnabled, 44 | ) { 45 | Text( 46 | text = stringResource(R.string.home_screen_btn), 47 | maxLines = 1, 48 | overflow = TextOverflow.Ellipsis, 49 | ) 50 | } 51 | } 52 | } -------------------------------------------------------------------------------- /app/src/main/java/com/anthonyla/paperize/feature/wallpaper/presentation/wallpaper_screen/components/GrayscaleBitmapTransformation.kt: -------------------------------------------------------------------------------- 1 | package com.anthonyla.paperize.feature.wallpaper.presentation.wallpaper_screen.components 2 | 3 | import android.graphics.Bitmap 4 | import com.anthonyla.paperize.core.grayBitmap 5 | import com.bumptech.glide.load.engine.bitmap_recycle.BitmapPool 6 | import com.bumptech.glide.load.resource.bitmap.BitmapTransformation 7 | import com.bumptech.glide.util.Util 8 | import java.security.MessageDigest 9 | 10 | /** 11 | * A BitmapTransformation that applies a grayscale effect to a bitmap to be used with Glide 12 | */ 13 | class GrayscaleBitmapTransformation(private val percent: Int): BitmapTransformation() { 14 | 15 | override fun transform( 16 | pool: BitmapPool, 17 | toTransform: Bitmap, 18 | outWidth: Int, 19 | outHeight: Int 20 | ): Bitmap { 21 | return grayBitmap(toTransform, percent) 22 | } 23 | 24 | override fun equals(other: Any?): Boolean { 25 | return other is GrayscaleBitmapTransformation && other.percent == percent 26 | } 27 | 28 | override fun hashCode(): Int { 29 | return Util.hashCode(percent) 30 | } 31 | 32 | override fun updateDiskCacheKey(messageDigest: MessageDigest) { 33 | messageDigest.update((ID + percent).toByteArray(CHARSET)) 34 | } 35 | 36 | companion object { 37 | private const val ID = "com.bumptech.glide.transformations.GrayscaleBitmapTransformation" 38 | } 39 | } -------------------------------------------------------------------------------- /app/src/main/java/com/anthonyla/paperize/feature/wallpaper/presentation/wallpaper_screen/components/IndividualSchedulingAndToggleRow.kt: -------------------------------------------------------------------------------- 1 | package com.anthonyla.paperize.feature.wallpaper.presentation.wallpaper_screen.components 2 | 3 | import androidx.compose.animation.AnimatedVisibility 4 | import androidx.compose.animation.animateContentSize 5 | import androidx.compose.animation.core.Spring 6 | import androidx.compose.animation.core.spring 7 | import androidx.compose.animation.expandVertically 8 | import androidx.compose.animation.fadeIn 9 | import androidx.compose.animation.fadeOut 10 | import androidx.compose.animation.shrinkVertically 11 | import androidx.compose.foundation.layout.Arrangement 12 | import androidx.compose.foundation.layout.Column 13 | import androidx.compose.foundation.layout.Row 14 | import androidx.compose.foundation.layout.Spacer 15 | import androidx.compose.foundation.layout.fillMaxWidth 16 | import androidx.compose.foundation.layout.padding 17 | import androidx.compose.foundation.shape.RoundedCornerShape 18 | import androidx.compose.material3.HorizontalDivider 19 | import androidx.compose.material3.MaterialTheme 20 | import androidx.compose.material3.Surface 21 | import androidx.compose.material3.Switch 22 | import androidx.compose.material3.Text 23 | import androidx.compose.runtime.Composable 24 | import androidx.compose.runtime.remember 25 | import androidx.compose.ui.Alignment 26 | import androidx.compose.ui.Modifier 27 | import androidx.compose.ui.res.stringResource 28 | import androidx.compose.ui.text.font.FontWeight 29 | import androidx.compose.ui.text.style.TextOverflow 30 | import androidx.compose.ui.unit.dp 31 | import com.anthonyla.paperize.R 32 | 33 | private val HorizontalPadding = 32.dp 34 | private val VerticalPadding = 8.dp 35 | 36 | /** 37 | * Composable to toggle individual scheduling settings and toggle wallpaper changer when scheduling separately 38 | */ 39 | @Composable 40 | fun IndividualSchedulingAndToggleRow( 41 | animate: Boolean, 42 | scheduleSeparately: Boolean, 43 | enableChanger: Boolean, 44 | onToggleChanger: (Boolean) -> Unit, 45 | onScheduleSeparatelyChange: (Boolean) -> Unit, 46 | ) { 47 | val columnModifier = remember { 48 | if (animate) { 49 | Modifier.animateContentSize( 50 | animationSpec = spring( 51 | dampingRatio = Spring.DampingRatioMediumBouncy, 52 | stiffness = Spring.StiffnessLow 53 | ) 54 | ) 55 | } else Modifier 56 | } 57 | 58 | Surface( 59 | tonalElevation = 10.dp, 60 | shape = RoundedCornerShape(16.dp), 61 | modifier = Modifier 62 | .fillMaxWidth() 63 | .padding(horizontal = 16.dp, vertical = VerticalPadding) 64 | ) { 65 | Column(modifier = columnModifier) { 66 | SettingsRow( 67 | text = stringResource(R.string.individual_scheduling), 68 | checked = scheduleSeparately, 69 | onCheckedChange = onScheduleSeparatelyChange 70 | ) 71 | 72 | if (scheduleSeparately) { 73 | if (animate) { 74 | AnimatedVisibility( 75 | visible = true, 76 | enter = expandVertically() + fadeIn(), 77 | exit = shrinkVertically() + fadeOut() 78 | ) { 79 | ChangerToggleSection(enableChanger, onToggleChanger) 80 | } 81 | } else { 82 | ChangerToggleSection(enableChanger, onToggleChanger) 83 | } 84 | } 85 | } 86 | } 87 | } 88 | 89 | @Composable 90 | private fun SettingsRow( 91 | text: String, 92 | checked: Boolean, 93 | onCheckedChange: (Boolean) -> Unit 94 | ) { 95 | Row( 96 | modifier = Modifier 97 | .fillMaxWidth() 98 | .padding( 99 | start = HorizontalPadding, 100 | end = HorizontalPadding, 101 | top = VerticalPadding, 102 | bottom = VerticalPadding 103 | ), 104 | horizontalArrangement = Arrangement.Center, 105 | verticalAlignment = Alignment.CenterVertically 106 | ) { 107 | Text( 108 | text = text, 109 | fontWeight = FontWeight.W500, 110 | overflow = TextOverflow.Ellipsis, 111 | maxLines = 1 112 | ) 113 | Spacer(modifier = Modifier.padding(VerticalPadding)) 114 | Switch( 115 | checked = checked, 116 | onCheckedChange = onCheckedChange 117 | ) 118 | } 119 | } 120 | 121 | @Composable 122 | private fun ChangerToggleSection( 123 | enableChanger: Boolean, 124 | onToggleChanger: (Boolean) -> Unit 125 | ) { 126 | HorizontalDivider( 127 | modifier = Modifier 128 | .fillMaxWidth() 129 | .padding(horizontal = 48.dp), 130 | color = MaterialTheme.colorScheme.outline, 131 | thickness = 2.dp 132 | ) 133 | SettingsRow( 134 | text = stringResource(R.string.enable_wallpaper_changer), 135 | checked = enableChanger, 136 | onCheckedChange = onToggleChanger 137 | ) 138 | } -------------------------------------------------------------------------------- /app/src/main/java/com/anthonyla/paperize/feature/wallpaper/presentation/wallpaper_screen/components/PreviewItem.kt: -------------------------------------------------------------------------------- 1 | package com.anthonyla.paperize.feature.wallpaper.presentation.wallpaper_screen.components 2 | 3 | import android.util.Log 4 | import androidx.compose.foundation.background 5 | import androidx.compose.foundation.border 6 | import androidx.compose.foundation.layout.size 7 | import androidx.compose.foundation.shape.RoundedCornerShape 8 | import androidx.compose.runtime.Composable 9 | import androidx.compose.runtime.getValue 10 | import androidx.compose.runtime.mutableStateOf 11 | import androidx.compose.runtime.remember 12 | import androidx.compose.ui.Alignment 13 | import androidx.compose.ui.Modifier 14 | import androidx.compose.ui.draw.blur 15 | import androidx.compose.ui.draw.clip 16 | import androidx.compose.ui.graphics.BlendMode 17 | import androidx.compose.ui.graphics.Color 18 | import androidx.compose.ui.graphics.ColorFilter 19 | import androidx.compose.ui.layout.ContentScale 20 | import androidx.compose.ui.platform.LocalConfiguration 21 | import androidx.compose.ui.platform.LocalContext 22 | import androidx.compose.ui.unit.IntSize 23 | import androidx.compose.ui.unit.dp 24 | import androidx.compose.ui.unit.times 25 | import androidx.core.net.toUri 26 | import com.anthonyla.paperize.core.ScalingConstants 27 | import com.anthonyla.paperize.core.decompress 28 | import com.anthonyla.paperize.core.isValidUri 29 | import com.bumptech.glide.request.RequestOptions 30 | import com.skydoves.landscapist.ImageOptions 31 | import com.skydoves.landscapist.glide.GlideImage 32 | 33 | /** 34 | * A composable that displays a wallpaper for preview 35 | */ 36 | @Composable 37 | fun PreviewItem( 38 | wallpaperUri: String, 39 | darken: Boolean, 40 | darkenPercentage: Int, 41 | blur: Boolean, 42 | blurPercentage: Int, 43 | scaling: ScalingConstants, 44 | vignette: Boolean, 45 | vignettePercentage: Int, 46 | grayscale: Boolean, 47 | grayscalePercentage: Int 48 | ) { 49 | val context = LocalContext.current 50 | val showUri by remember { mutableStateOf(isValidUri(context, wallpaperUri)) } 51 | val configuration = LocalConfiguration.current 52 | val screenWidth = configuration.screenWidthDp.dp 53 | val screenHeight = configuration.screenHeightDp.dp 54 | if (showUri) { 55 | val uri = wallpaperUri.decompress("content://com.android.externalstorage.documents/").toUri() 56 | GlideImage( 57 | imageModel = { uri }, 58 | imageOptions = ImageOptions( 59 | contentScale = when (scaling) { 60 | ScalingConstants.FILL -> ContentScale.FillHeight 61 | ScalingConstants.FIT -> ContentScale.FillWidth 62 | ScalingConstants.STRETCH -> ContentScale.FillBounds 63 | ScalingConstants.NONE -> ContentScale.Crop 64 | }, 65 | requestSize = IntSize(300, 300), 66 | alignment = if (scaling == ScalingConstants.NONE) Alignment.CenterStart else Alignment.Center, 67 | colorFilter = if (darken && darkenPercentage < 100) { 68 | ColorFilter.tint( 69 | Color.Black.copy(alpha = (100 - darkenPercentage).toFloat().div(100f)), 70 | BlendMode.Darken 71 | ) 72 | } else { null }, 73 | tag = vignette.toString() + vignettePercentage.toString() + grayscale.toString() + grayscalePercentage.toString(), 74 | ), 75 | requestOptions = { 76 | when { 77 | grayscale && grayscalePercentage > 0 && vignette && vignettePercentage > 0 -> { 78 | RequestOptions().transform( 79 | VignetteBitmapTransformation(vignettePercentage), 80 | GrayscaleBitmapTransformation(grayscalePercentage) 81 | ) 82 | } 83 | grayscale && grayscalePercentage > 0 -> { 84 | RequestOptions().transform(GrayscaleBitmapTransformation(grayscalePercentage)) 85 | } 86 | vignette && vignettePercentage > 0 -> { 87 | RequestOptions().transform(VignetteBitmapTransformation(vignettePercentage)) 88 | } 89 | else -> RequestOptions() 90 | } 91 | }, 92 | modifier = Modifier 93 | .size(screenWidth * 0.35f, screenHeight * 0.35f) 94 | .clip(RoundedCornerShape(16.dp)) 95 | .border(3.dp, Color.Black, RoundedCornerShape(16.dp)) 96 | .background(Color.Black) 97 | .blur( 98 | if (blur && blurPercentage > 0) { 99 | blurPercentage 100 | .toFloat() 101 | .div(100f) * 1.5.dp 102 | } else { 103 | 0.dp 104 | } 105 | ) 106 | ) 107 | } 108 | } -------------------------------------------------------------------------------- /app/src/main/java/com/anthonyla/paperize/feature/wallpaper/presentation/wallpaper_screen/components/RefreshSwitch.kt: -------------------------------------------------------------------------------- 1 | package com.anthonyla.paperize.feature.wallpaper.presentation.wallpaper_screen.components 2 | 3 | import androidx.compose.foundation.layout.Arrangement 4 | import androidx.compose.foundation.layout.Column 5 | import androidx.compose.foundation.layout.PaddingValues 6 | import androidx.compose.foundation.layout.Row 7 | import androidx.compose.foundation.layout.fillMaxWidth 8 | import androidx.compose.foundation.layout.padding 9 | import androidx.compose.foundation.shape.RoundedCornerShape 10 | import androidx.compose.material3.Surface 11 | import androidx.compose.material3.Switch 12 | import androidx.compose.material3.Text 13 | import androidx.compose.runtime.Composable 14 | import androidx.compose.ui.Alignment 15 | import androidx.compose.ui.Modifier 16 | import androidx.compose.ui.res.stringResource 17 | import androidx.compose.ui.text.font.FontWeight 18 | import androidx.compose.ui.unit.dp 19 | import com.anthonyla.paperize.R 20 | 21 | /** 22 | * Composable that toggles whether to allow album refresh (24 hrs) 23 | */ 24 | @Composable 25 | fun RefreshSwitch( 26 | refresh: Boolean, 27 | onRefreshChange: (Boolean) -> Unit, 28 | ) { 29 | Surface( 30 | tonalElevation = 10.dp, 31 | shape = RoundedCornerShape(16.dp), 32 | modifier = Modifier 33 | .fillMaxWidth() 34 | .padding(PaddingValues(horizontal = 16.dp, vertical = 8.dp)) 35 | ) { 36 | Column ( 37 | horizontalAlignment = Alignment.CenterHorizontally, 38 | verticalArrangement = Arrangement.Center, 39 | ) { 40 | Row (horizontalArrangement = Arrangement.Center, verticalAlignment = Alignment.CenterVertically) { 41 | Text( 42 | text = stringResource(R.string.periodic_refresh), 43 | modifier = Modifier.padding(16.dp), 44 | fontWeight = FontWeight.W500 45 | ) 46 | Switch( 47 | checked = refresh, 48 | onCheckedChange = onRefreshChange 49 | ) 50 | } 51 | } 52 | } 53 | } -------------------------------------------------------------------------------- /app/src/main/java/com/anthonyla/paperize/feature/wallpaper/presentation/wallpaper_screen/components/ShuffleSwitch.kt: -------------------------------------------------------------------------------- 1 | package com.anthonyla.paperize.feature.wallpaper.presentation.wallpaper_screen.components 2 | 3 | import androidx.compose.animation.animateContentSize 4 | import androidx.compose.animation.core.LinearOutSlowInEasing 5 | import androidx.compose.animation.core.tween 6 | import androidx.compose.foundation.layout.Arrangement 7 | import androidx.compose.foundation.layout.Column 8 | import androidx.compose.foundation.layout.PaddingValues 9 | import androidx.compose.foundation.layout.Row 10 | import androidx.compose.foundation.layout.fillMaxWidth 11 | import androidx.compose.foundation.layout.padding 12 | import androidx.compose.foundation.shape.RoundedCornerShape 13 | import androidx.compose.material3.Surface 14 | import androidx.compose.material3.Switch 15 | import androidx.compose.material3.Text 16 | import androidx.compose.runtime.Composable 17 | import androidx.compose.ui.Alignment 18 | import androidx.compose.ui.Modifier 19 | import androidx.compose.ui.res.stringResource 20 | import androidx.compose.ui.text.font.FontWeight 21 | import androidx.compose.ui.unit.dp 22 | import com.anthonyla.paperize.R 23 | 24 | @Composable 25 | fun ShuffleSwitch( 26 | shuffle: Boolean, 27 | onShuffleCheck: (Boolean) -> Unit 28 | ) { 29 | Surface( 30 | tonalElevation = 10.dp, 31 | shape = RoundedCornerShape(16.dp), 32 | modifier = Modifier 33 | .fillMaxWidth() 34 | .padding(PaddingValues(horizontal = 16.dp, vertical = 8.dp)) 35 | ) { 36 | Column( 37 | horizontalAlignment = Alignment.CenterHorizontally, 38 | verticalArrangement = Arrangement.Center 39 | ) { 40 | Row( 41 | horizontalArrangement = Arrangement.Center, 42 | verticalAlignment = Alignment.CenterVertically 43 | ) { 44 | Text( 45 | text = stringResource(R.string.shuffle_wallpaper), 46 | modifier = Modifier.padding(16.dp), 47 | fontWeight = FontWeight.W500 48 | ) 49 | Switch( 50 | checked = shuffle, 51 | onCheckedChange = onShuffleCheck, 52 | ) 53 | } 54 | } 55 | } 56 | } -------------------------------------------------------------------------------- /app/src/main/java/com/anthonyla/paperize/feature/wallpaper/presentation/wallpaper_screen/components/VignetteBitmapTransformation.kt: -------------------------------------------------------------------------------- 1 | package com.anthonyla.paperize.feature.wallpaper.presentation.wallpaper_screen.components 2 | 3 | import android.graphics.Bitmap 4 | import com.anthonyla.paperize.core.vignetteBitmap 5 | import com.bumptech.glide.load.engine.bitmap_recycle.BitmapPool 6 | import com.bumptech.glide.load.resource.bitmap.BitmapTransformation 7 | import com.bumptech.glide.util.Util 8 | import java.security.MessageDigest 9 | 10 | /** 11 | * A BitmapTransformation that applies a vignette effect to a bitmap to be used with Glide 12 | */ 13 | class VignetteBitmapTransformation(private val percent: Int): BitmapTransformation() { 14 | 15 | override fun transform( 16 | pool: BitmapPool, 17 | toTransform: Bitmap, 18 | outWidth: Int, 19 | outHeight: Int 20 | ): Bitmap { 21 | return vignetteBitmap(toTransform, percent) 22 | } 23 | 24 | override fun equals(other: Any?): Boolean { 25 | return other is VignetteBitmapTransformation && other.percent == percent 26 | } 27 | 28 | override fun hashCode(): Int { 29 | return Util.hashCode(percent) 30 | } 31 | 32 | override fun updateDiskCacheKey(messageDigest: MessageDigest) { 33 | messageDigest.update((ID + percent).toByteArray(CHARSET)) 34 | } 35 | 36 | companion object { 37 | private const val ID = "com.bumptech.glide.transformations.VignettedBitmapTransformation" 38 | } 39 | } -------------------------------------------------------------------------------- /app/src/main/java/com/anthonyla/paperize/feature/wallpaper/tasker_shortcut/WallpaperChangeEvent.kt: -------------------------------------------------------------------------------- 1 | package com.anthonyla.paperize.feature.wallpaper.tasker_shortcut 2 | 3 | import android.app.Activity 4 | import android.content.Context 5 | import android.os.Bundle 6 | import com.joaomgcd.taskerpluginlibrary.config.TaskerPluginConfig 7 | import com.joaomgcd.taskerpluginlibrary.config.TaskerPluginConfigHelperEventNoOutputOrInputOrUpdate 8 | import com.joaomgcd.taskerpluginlibrary.config.TaskerPluginConfigNoInput 9 | import com.joaomgcd.taskerpluginlibrary.extensions.requestQuery 10 | import com.joaomgcd.taskerpluginlibrary.input.TaskerInput 11 | 12 | class BasicEventHelper(config: TaskerPluginConfig) : TaskerPluginConfigHelperEventNoOutputOrInputOrUpdate(config) { 13 | override fun addToStringBlurb(input: TaskerInput, blurbBuilder: StringBuilder) { 14 | blurbBuilder.append("Triggers when the app changes the wallpaper") 15 | } 16 | } 17 | 18 | class ActivityConfigBasicEvent : Activity(), TaskerPluginConfigNoInput { 19 | override val context: Context get() = applicationContext 20 | override fun onCreate(savedInstanceState: Bundle?) { 21 | super.onCreate(savedInstanceState) 22 | BasicEventHelper(this).finishForTasker() 23 | } 24 | } 25 | 26 | fun Context.triggerWallpaperTaskerEvent() = ActivityConfigBasicEvent::class.java.requestQuery(this) 27 | -------------------------------------------------------------------------------- /app/src/main/java/com/anthonyla/paperize/feature/wallpaper/tasker_shortcut/WallpaperShortcutRunner.kt: -------------------------------------------------------------------------------- 1 | package com.anthonyla.paperize.feature.wallpaper.tasker_shortcut 2 | 3 | import android.app.Activity 4 | import android.content.Context 5 | import android.content.Intent 6 | import android.os.Bundle 7 | import android.os.Handler 8 | import android.os.Looper 9 | import com.anthonyla.paperize.feature.wallpaper.wallpaper_alarmmanager.WallpaperBootAndChangeReceiver 10 | import com.joaomgcd.taskerpluginlibrary.action.TaskerPluginRunnerActionNoOutputOrInput 11 | import com.joaomgcd.taskerpluginlibrary.config.TaskerPluginConfig 12 | import com.joaomgcd.taskerpluginlibrary.config.TaskerPluginConfigHelperNoOutputOrInput 13 | import com.joaomgcd.taskerpluginlibrary.config.TaskerPluginConfigNoInput 14 | import com.joaomgcd.taskerpluginlibrary.input.TaskerInput 15 | import com.joaomgcd.taskerpluginlibrary.runner.TaskerPluginResult 16 | import com.joaomgcd.taskerpluginlibrary.runner.TaskerPluginResultSucess 17 | 18 | /** 19 | * Tasker Shortcut to change wallpaper 20 | */ 21 | class ShortcutHelper(config: TaskerPluginConfig) : TaskerPluginConfigHelperNoOutputOrInput(config) { 22 | override val runnerClass: Class get() = ShortcutActionRunner::class.java 23 | override fun addToStringBlurb(input: TaskerInput, blurbBuilder: StringBuilder) { 24 | blurbBuilder.append("Changes the wallpaper") 25 | } 26 | } 27 | 28 | class ActivityConfigBasicAction : Activity(), TaskerPluginConfigNoInput { 29 | override val context: Context get() = applicationContext 30 | private val taskerHelper by lazy { ShortcutHelper(this) } 31 | override fun onCreate(savedInstanceState: Bundle?) { 32 | super.onCreate(savedInstanceState) 33 | taskerHelper.finishForTasker() 34 | } 35 | } 36 | 37 | class ShortcutActionRunner : TaskerPluginRunnerActionNoOutputOrInput() { 38 | companion object { 39 | private const val ACTION_CHANGE_WALLPAPER = "com.anthonyla.paperize.SHORTCUT" 40 | } 41 | 42 | override fun run(context: Context, input: TaskerInput): TaskerPluginResult { 43 | Handler(Looper.getMainLooper()).post { 44 | val intent = Intent(ACTION_CHANGE_WALLPAPER).apply { 45 | setClass(context, WallpaperBootAndChangeReceiver::class.java) 46 | addFlags(Intent.FLAG_INCLUDE_STOPPED_PACKAGES) 47 | } 48 | context.sendBroadcast(intent) 49 | } 50 | return TaskerPluginResultSucess() 51 | } 52 | } -------------------------------------------------------------------------------- /app/src/main/java/com/anthonyla/paperize/feature/wallpaper/util/navigation/AnimatedScreen.kt: -------------------------------------------------------------------------------- 1 | package com.anthonyla.paperize.feature.wallpaper.util.navigation 2 | 3 | import androidx.compose.animation.AnimatedContentScope 4 | import androidx.compose.runtime.Composable 5 | import androidx.navigation.NavBackStackEntry 6 | import androidx.navigation.NavDeepLink 7 | import androidx.navigation.NavGraphBuilder 8 | import androidx.navigation.NavType 9 | import androidx.navigation.compose.composable 10 | import com.anthonyla.paperize.feature.wallpaper.util.navigation.NavConstants.INITIAL_OFFSET 11 | import kotlin.reflect.KType 12 | 13 | inline fun NavGraphBuilder.animatedScreen( 14 | typeMap: Map> = emptyMap(), 15 | animate: Boolean = true, 16 | deepLinks: List = emptyList(), 17 | noinline content: @Composable AnimatedContentScope.(NavBackStackEntry) -> Unit 18 | ) = composable( 19 | typeMap = typeMap, 20 | deepLinks = deepLinks, 21 | enterTransition = { 22 | if (animate) { 23 | sharedXTransitionIn(initial = { (it * INITIAL_OFFSET).toInt() }) 24 | } else { null } 25 | }, 26 | exitTransition = { 27 | if (animate) { 28 | sharedXTransitionOut(target = { -(it * INITIAL_OFFSET).toInt() }) 29 | } else { null } 30 | }, 31 | popEnterTransition = { 32 | if (animate) { 33 | sharedXTransitionIn(initial = { -(it * INITIAL_OFFSET).toInt() }) 34 | } else { null } 35 | }, 36 | popExitTransition = { 37 | if (animate) { 38 | sharedXTransitionOut(target = { -(it * INITIAL_OFFSET).toInt() }) 39 | } else { null } 40 | }, 41 | content = content 42 | ) -------------------------------------------------------------------------------- /app/src/main/java/com/anthonyla/paperize/feature/wallpaper/util/navigation/NavConstants.kt: -------------------------------------------------------------------------------- 1 | package com.anthonyla.paperize.feature.wallpaper.util.navigation 2 | 3 | object NavConstants { 4 | const val NAVIGATION_TIME: Int = 300 5 | const val INITIAL_OFFSET = 0.175f 6 | const val OFFSET_LIMIT = 0.4f 7 | } -------------------------------------------------------------------------------- /app/src/main/java/com/anthonyla/paperize/feature/wallpaper/util/navigation/NavScreens.kt: -------------------------------------------------------------------------------- 1 | package com.anthonyla.paperize.feature.wallpaper.util.navigation 2 | 3 | import kotlinx.serialization.Serializable 4 | 5 | 6 | /** 7 | * Data class for Startup screen 8 | */ 9 | @Serializable 10 | object Startup 11 | 12 | /** 13 | * Object for Notification screen 14 | */ 15 | @Serializable 16 | object Notification 17 | 18 | /** 19 | * Object for Home screen 20 | */ 21 | @Serializable 22 | object Home 23 | 24 | /** 25 | * Object for Settings screen 26 | */ 27 | @Serializable 28 | object Settings 29 | 30 | /** 31 | * Object for Privacy screen 32 | */ 33 | @Serializable 34 | object Privacy 35 | 36 | /** 37 | * Data class for AlbumView screen 38 | */ 39 | @Serializable 40 | data class AlbumView(val initialAlbumName: String) 41 | 42 | /** 43 | * Data class for AddEdit screen 44 | */ 45 | @Serializable 46 | data class AddAlbum(val initialAlbumName: String) 47 | 48 | /** 49 | * Data class for WallpaperView screen 50 | */ 51 | @Serializable 52 | data class WallpaperView(val wallpaperUri: String, val wallpaperName: String) 53 | 54 | /** 55 | * Object for FolderView screen 56 | */ 57 | @Serializable 58 | object FolderView 59 | 60 | /** 61 | * Object for SortView screen 62 | */ 63 | @Serializable 64 | object SortView 65 | 66 | -------------------------------------------------------------------------------- /app/src/main/java/com/anthonyla/paperize/feature/wallpaper/util/navigation/SharedXTransition.kt: -------------------------------------------------------------------------------- 1 | package com.anthonyla.paperize.feature.wallpaper.util.navigation 2 | 3 | import androidx.compose.animation.EnterTransition 4 | import androidx.compose.animation.ExitTransition 5 | import androidx.compose.animation.core.FastOutLinearInEasing 6 | import androidx.compose.animation.core.FastOutSlowInEasing 7 | import androidx.compose.animation.core.LinearOutSlowInEasing 8 | import androidx.compose.animation.core.tween 9 | import androidx.compose.animation.fadeIn 10 | import androidx.compose.animation.fadeOut 11 | import androidx.compose.animation.slideInHorizontally 12 | import androidx.compose.animation.slideOutHorizontally 13 | import com.anthonyla.paperize.feature.wallpaper.util.navigation.NavConstants.NAVIGATION_TIME 14 | import com.anthonyla.paperize.feature.wallpaper.util.navigation.NavConstants.OFFSET_LIMIT 15 | 16 | fun sharedXTransitionIn( 17 | initial: (fullWidth: Int) -> Int, 18 | durationMillis: Int = NAVIGATION_TIME, 19 | ): EnterTransition { 20 | val outgoingDuration = (durationMillis * OFFSET_LIMIT).toInt() 21 | val incomingDuration = durationMillis - outgoingDuration 22 | 23 | return slideInHorizontally( 24 | animationSpec = tween( 25 | durationMillis = durationMillis, 26 | easing = FastOutSlowInEasing 27 | ), initialOffsetX = initial 28 | ) + fadeIn( 29 | animationSpec = tween( 30 | durationMillis = incomingDuration, 31 | delayMillis = outgoingDuration, 32 | easing = LinearOutSlowInEasing 33 | ) 34 | ) 35 | } 36 | 37 | fun sharedXTransitionOut( 38 | target: (fullWidth: Int) -> Int, 39 | durationMillis: Int = NAVIGATION_TIME, 40 | ): ExitTransition { 41 | val outgoingDuration = (durationMillis * OFFSET_LIMIT).toInt() 42 | 43 | return slideOutHorizontally( 44 | animationSpec = tween( 45 | durationMillis = durationMillis, 46 | easing = FastOutSlowInEasing 47 | ), targetOffsetX = target 48 | ) + fadeOut( 49 | animationSpec = tween( 50 | durationMillis = outgoingDuration, 51 | delayMillis = 0, 52 | easing = FastOutLinearInEasing 53 | ) 54 | ) 55 | } -------------------------------------------------------------------------------- /app/src/main/java/com/anthonyla/paperize/feature/wallpaper/wallpaper_alarmmanager/WallpaperAlarmItem.kt: -------------------------------------------------------------------------------- 1 | package com.anthonyla.paperize.feature.wallpaper.wallpaper_alarmmanager 2 | 3 | import com.anthonyla.paperize.core.SettingsConstants 4 | 5 | /** 6 | * Data class for storing wallpaper alarm item 7 | * @param homeInterval: Int - time in minutes for home wallpaper or both if scheduled together 8 | * @param lockInterval: Int - time in minutes for lock wallpaper 9 | * @param scheduleSeparately: Boolean - schedule wallpapers separately 10 | * @param setHome: Boolean - set home wallpaper 11 | * @param setLock: Boolean - set lock wallpaper 12 | */ 13 | data class WallpaperAlarmItem( 14 | val homeInterval: Int = SettingsConstants.WALLPAPER_CHANGE_INTERVAL_DEFAULT, 15 | val lockInterval: Int = SettingsConstants.WALLPAPER_CHANGE_INTERVAL_DEFAULT, 16 | val scheduleSeparately: Boolean = false, 17 | val setHome: Boolean = false, 18 | val setLock: Boolean = false, 19 | val changeStartTime: Boolean = false, 20 | val startTime: Pair = Pair(0, 0), 21 | val shuffle: Boolean = true 22 | ) -------------------------------------------------------------------------------- /app/src/main/java/com/anthonyla/paperize/feature/wallpaper/wallpaper_alarmmanager/WallpaperAlarmScheduler.kt: -------------------------------------------------------------------------------- 1 | package com.anthonyla.paperize.feature.wallpaper.wallpaper_alarmmanager 2 | 3 | interface WallpaperAlarmScheduler { 4 | /** 5 | * Schedule a wallpaper alarm to change the wallpaper 6 | * @param wallpaperAlarmItem the wallpaper alarm item to schedule 7 | * @param origin where the request came from (null if not from a specific origin, 0 if home screen, 1 if lock screen, 2 if both) 8 | * @param changeImmediate whether to change the wallpaper immediately or just schedule the alarm 9 | * @param cancelImmediate whether to cancel all alarms before scheduling the new one 10 | * @param setAlarm whether to set the alarm or not 11 | * @param firstLaunch whether this is the first launch of the wallpaper changer 12 | */ 13 | suspend fun scheduleWallpaperAlarm( 14 | wallpaperAlarmItem: WallpaperAlarmItem, 15 | origin: Int? = null, 16 | changeImmediate: Boolean = false, 17 | cancelImmediate: Boolean = false, 18 | setAlarm: Boolean = true, 19 | firstLaunch: Boolean = false, 20 | homeNextTime: String? = "", 21 | lockNextTime: String? = "", 22 | ) 23 | 24 | /** 25 | * Update the wallpaper alarm with new times without changing the wallpaper 26 | */ 27 | fun updateWallpaperAlarm(wallpaperAlarmItem: WallpaperAlarmItem, firstLaunch: Boolean = false) 28 | 29 | /** 30 | * Update the wallpaper without changing the alarm 31 | */ 32 | fun updateWallpaper(scheduleSeparately: Boolean, setHome: Boolean, setLock: Boolean) 33 | 34 | /** 35 | * Cancel all wallpaper alarms 36 | */ 37 | fun cancelWallpaperAlarm(cancelLock: Boolean = true, cancelHome: Boolean = true) 38 | } -------------------------------------------------------------------------------- /app/src/main/java/com/anthonyla/paperize/feature/wallpaper/wallpaper_alarmmanager/WallpaperReceiver.kt: -------------------------------------------------------------------------------- 1 | package com.anthonyla.paperize.feature.wallpaper.wallpaper_alarmmanager 2 | 3 | import android.content.BroadcastReceiver 4 | import android.content.Context 5 | import android.content.Intent 6 | import com.anthonyla.paperize.core.SettingsConstants 7 | import com.anthonyla.paperize.core.SettingsConstants.WALLPAPER_CHANGE_INTERVAL_DEFAULT 8 | import com.anthonyla.paperize.core.Type 9 | import com.anthonyla.paperize.data.settings.SettingsDataStore 10 | import com.anthonyla.paperize.feature.wallpaper.wallpaper_service.HomeWallpaperService 11 | import com.anthonyla.paperize.feature.wallpaper.wallpaper_service.LockWallpaperService 12 | import dagger.hilt.android.AndroidEntryPoint 13 | import kotlinx.coroutines.CoroutineScope 14 | import kotlinx.coroutines.Dispatchers 15 | import kotlinx.coroutines.launch 16 | import javax.inject.Inject 17 | 18 | /** 19 | * Receiver for the alarm manager to change wallpaper 20 | */ 21 | @AndroidEntryPoint 22 | class WallpaperReceiver : BroadcastReceiver() { 23 | @Inject 24 | lateinit var settingsDataStoreImpl: SettingsDataStore 25 | override fun onReceive(context: Context?, intent: Intent?) { 26 | if (context != null) { 27 | val refresh = intent?.getBooleanExtra("refresh", false) 28 | if (refresh == true) { 29 | val serviceIntent = Intent(context, HomeWallpaperService::class.java).apply { 30 | action = HomeWallpaperService.Actions.REFRESH.toString() 31 | } 32 | context.startService(serviceIntent) 33 | } 34 | else { 35 | val homeInterval = intent?.getIntExtra("homeInterval", WALLPAPER_CHANGE_INTERVAL_DEFAULT) ?: WALLPAPER_CHANGE_INTERVAL_DEFAULT 36 | val lockInterval = intent?.getIntExtra("lockInterval", WALLPAPER_CHANGE_INTERVAL_DEFAULT) ?: WALLPAPER_CHANGE_INTERVAL_DEFAULT 37 | val scheduleSeparately = intent?.getBooleanExtra("scheduleSeparately", false) ?: false 38 | val type = intent?.getIntExtra("type", Type.SINGLE.ordinal) ?: Type.SINGLE.ordinal 39 | val setHome = intent?.getBooleanExtra("setHome", false) ?: false 40 | val setLock = intent?.getBooleanExtra("setLock", false) ?: false 41 | val changeStartTime = intent?.getBooleanExtra("changeStartTime", false) ?: false 42 | val startTime = intent?.getIntArrayExtra("startTime") ?: intArrayOf(0, 0) 43 | 44 | when (type) { 45 | Type.SINGLE.ordinal -> { 46 | if (setLock) startService(context, LockWallpaperService::class.java, LockWallpaperService.Actions.START.toString(), homeInterval, lockInterval, scheduleSeparately, type) 47 | if (setHome) startService(context, HomeWallpaperService::class.java, HomeWallpaperService.Actions.START.toString(), homeInterval, lockInterval, scheduleSeparately, type) 48 | } 49 | Type.HOME.ordinal -> { 50 | startService(context, HomeWallpaperService::class.java, HomeWallpaperService.Actions.START.toString(), homeInterval, lockInterval, scheduleSeparately, type) 51 | } 52 | Type.LOCK.ordinal -> { 53 | startService(context, LockWallpaperService::class.java, LockWallpaperService.Actions.START.toString(), homeInterval, lockInterval, scheduleSeparately, type) 54 | } 55 | } 56 | 57 | // Schedule next alarm for next wallpaper change 58 | val origin = intent?.getIntExtra("origin", -1)?.takeIf { it != -1 } 59 | CoroutineScope(Dispatchers.IO).launch { 60 | WallpaperAlarmSchedulerImpl(context).scheduleWallpaperAlarm( 61 | wallpaperAlarmItem = WallpaperAlarmItem( 62 | homeInterval = homeInterval, 63 | lockInterval = lockInterval, 64 | scheduleSeparately = scheduleSeparately, 65 | setHome = setHome, 66 | setLock = setLock, 67 | changeStartTime = changeStartTime, 68 | startTime = Pair(startTime[0], startTime[1]), 69 | ), 70 | origin = origin, 71 | homeNextTime = settingsDataStoreImpl.getString(SettingsConstants.HOME_NEXT_SET_TIME), 72 | lockNextTime = settingsDataStoreImpl.getString(SettingsConstants.LOCK_NEXT_SET_TIME), 73 | ) 74 | } 75 | } 76 | } 77 | } 78 | 79 | private fun startService(context: Context, serviceClass: Class<*>, action: String, homeInterval: Int? = null, lockInterval: Int? = null, scheduleSeparately: Boolean? = null, type: Int? = null) { 80 | val serviceIntent = Intent(context, serviceClass).apply { 81 | this.action = action 82 | homeInterval?.let { putExtra("homeInterval", it) } 83 | lockInterval?.let { putExtra("lockInterval", it) } 84 | scheduleSeparately?.let { putExtra("scheduleSeparately", it) } 85 | type?.let { putExtra("type", it) } 86 | } 87 | context.startService(serviceIntent) 88 | } 89 | } -------------------------------------------------------------------------------- /app/src/main/java/com/anthonyla/paperize/feature/wallpaper/wallpaper_tile/ChangeWallpaperTileService.kt: -------------------------------------------------------------------------------- 1 | package com.anthonyla.paperize.feature.wallpaper.wallpaper_tile 2 | 3 | import android.content.Intent 4 | import android.service.quicksettings.TileService 5 | import com.anthonyla.paperize.data.settings.SettingsDataStore 6 | import com.anthonyla.paperize.feature.wallpaper.wallpaper_alarmmanager.WallpaperBootAndChangeReceiver 7 | import dagger.hilt.android.AndroidEntryPoint 8 | import javax.inject.Inject 9 | 10 | /** 11 | * Service for the quick settings tile to change the wallpaper 12 | */ 13 | @AndroidEntryPoint 14 | class ChangeWallpaperTileService: TileService() { 15 | companion object { 16 | const val ACTION_CHANGE_WALLPAPER = "com.anthonyla.paperize.SHORTCUT" 17 | } 18 | 19 | @Inject 20 | lateinit var settingsDataStoreImpl: SettingsDataStore 21 | 22 | override fun onTileAdded() { 23 | super.onTileAdded() 24 | } 25 | 26 | override fun onStartListening() { 27 | super.onStartListening() 28 | } 29 | 30 | override fun onStopListening() { 31 | super.onStopListening() 32 | } 33 | 34 | override fun onClick() { 35 | super.onClick() 36 | val intent = Intent(ACTION_CHANGE_WALLPAPER).apply { 37 | setClass(this@ChangeWallpaperTileService, WallpaperBootAndChangeReceiver::class.java) 38 | addFlags(Intent.FLAG_INCLUDE_STOPPED_PACKAGES) 39 | } 40 | sendBroadcast(intent) 41 | } 42 | 43 | override fun onTileRemoved() { 44 | super.onTileRemoved() 45 | } 46 | } -------------------------------------------------------------------------------- /app/src/main/res/app_icon-playstore.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Anthonyy232/Paperize/89450026526cb5c75e549a80a2a914602075817d/app/src/main/res/app_icon-playstore.png -------------------------------------------------------------------------------- /app/src/main/res/drawable-hdpi/notification_icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Anthonyy232/Paperize/89450026526cb5c75e549a80a2a914602075817d/app/src/main/res/drawable-hdpi/notification_icon.png -------------------------------------------------------------------------------- /app/src/main/res/drawable-mdpi/notification_icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Anthonyy232/Paperize/89450026526cb5c75e549a80a2a914602075817d/app/src/main/res/drawable-mdpi/notification_icon.png -------------------------------------------------------------------------------- /app/src/main/res/drawable-xhdpi/notification_icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Anthonyy232/Paperize/89450026526cb5c75e549a80a2a914602075817d/app/src/main/res/drawable-xhdpi/notification_icon.png -------------------------------------------------------------------------------- /app/src/main/res/drawable-xxhdpi/notification_icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Anthonyy232/Paperize/89450026526cb5c75e549a80a2a914602075817d/app/src/main/res/drawable-xxhdpi/notification_icon.png -------------------------------------------------------------------------------- /app/src/main/res/drawable-xxxhdpi/notification_icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Anthonyy232/Paperize/89450026526cb5c75e549a80a2a914602075817d/app/src/main/res/drawable-xxxhdpi/notification_icon.png -------------------------------------------------------------------------------- /app/src/main/res/drawable/app_icon_foreground.xml: -------------------------------------------------------------------------------- 1 | 6 | 10 | 13 | 16 | 19 | 20 | 21 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/fdroid.xml: -------------------------------------------------------------------------------- 1 | 6 | 9 | 10 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/fill.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/fit.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/github.xml: -------------------------------------------------------------------------------- 1 | 6 | 10 | 11 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/izzyondroid.xml: -------------------------------------------------------------------------------- 1 | 6 | 13 | 20 | 23 | 26 | 33 | 40 | 41 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/next_wallpaper_icon.xml: -------------------------------------------------------------------------------- 1 | 6 | 9 | 10 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/none.xml: -------------------------------------------------------------------------------- 1 | 6 | 9 | 12 | 15 | 18 | 21 | 24 | 27 | 28 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/splash_background.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/stretch.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | -------------------------------------------------------------------------------- /app/src/main/res/mipmap-anydpi-v26/app_icon.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /app/src/main/res/mipmap-anydpi-v26/app_icon_round.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /app/src/main/res/mipmap-hdpi/app_icon.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Anthonyy232/Paperize/89450026526cb5c75e549a80a2a914602075817d/app/src/main/res/mipmap-hdpi/app_icon.webp -------------------------------------------------------------------------------- /app/src/main/res/mipmap-hdpi/app_icon_round.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Anthonyy232/Paperize/89450026526cb5c75e549a80a2a914602075817d/app/src/main/res/mipmap-hdpi/app_icon_round.webp -------------------------------------------------------------------------------- /app/src/main/res/mipmap-mdpi/app_icon.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Anthonyy232/Paperize/89450026526cb5c75e549a80a2a914602075817d/app/src/main/res/mipmap-mdpi/app_icon.webp -------------------------------------------------------------------------------- /app/src/main/res/mipmap-mdpi/app_icon_round.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Anthonyy232/Paperize/89450026526cb5c75e549a80a2a914602075817d/app/src/main/res/mipmap-mdpi/app_icon_round.webp -------------------------------------------------------------------------------- /app/src/main/res/mipmap-xhdpi/app_icon.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Anthonyy232/Paperize/89450026526cb5c75e549a80a2a914602075817d/app/src/main/res/mipmap-xhdpi/app_icon.webp -------------------------------------------------------------------------------- /app/src/main/res/mipmap-xhdpi/app_icon_round.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Anthonyy232/Paperize/89450026526cb5c75e549a80a2a914602075817d/app/src/main/res/mipmap-xhdpi/app_icon_round.webp -------------------------------------------------------------------------------- /app/src/main/res/mipmap-xxhdpi/app_icon.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Anthonyy232/Paperize/89450026526cb5c75e549a80a2a914602075817d/app/src/main/res/mipmap-xxhdpi/app_icon.webp -------------------------------------------------------------------------------- /app/src/main/res/mipmap-xxhdpi/app_icon_round.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Anthonyy232/Paperize/89450026526cb5c75e549a80a2a914602075817d/app/src/main/res/mipmap-xxhdpi/app_icon_round.webp -------------------------------------------------------------------------------- /app/src/main/res/mipmap-xxxhdpi/app_icon.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Anthonyy232/Paperize/89450026526cb5c75e549a80a2a914602075817d/app/src/main/res/mipmap-xxxhdpi/app_icon.webp -------------------------------------------------------------------------------- /app/src/main/res/mipmap-xxxhdpi/app_icon_round.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Anthonyy232/Paperize/89450026526cb5c75e549a80a2a914602075817d/app/src/main/res/mipmap-xxxhdpi/app_icon_round.webp -------------------------------------------------------------------------------- /app/src/main/res/resources.properties: -------------------------------------------------------------------------------- 1 | unqualifiedResLocale=en-US -------------------------------------------------------------------------------- /app/src/main/res/values/app_icon_background.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | #FFD9E2 4 | -------------------------------------------------------------------------------- /app/src/main/res/values/colors.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | #FFBB86FC 4 | #FF6200EE 5 | #FF3700B3 6 | #FF03DAC5 7 | #FF018786 8 | #FF000000 9 | #FFFFFFFF 10 | #00FFFFFF 11 | #FFFBFE 12 | #1C1B1F 13 | -------------------------------------------------------------------------------- /app/src/main/res/values/themes.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 6 | -------------------------------------------------------------------------------- /app/src/main/res/xml/backup_rules.xml: -------------------------------------------------------------------------------- 1 | 8 | 9 | 13 | -------------------------------------------------------------------------------- /app/src/main/res/xml/data_extraction_rules.xml: -------------------------------------------------------------------------------- 1 | 6 | 7 | 8 | 12 | 13 | 19 | -------------------------------------------------------------------------------- /app/src/main/res/xml/paperize.xml: -------------------------------------------------------------------------------- 1 | 2 | -------------------------------------------------------------------------------- /app/src/main/res/xml/paperize_widget_info.xml: -------------------------------------------------------------------------------- 1 | 2 | -------------------------------------------------------------------------------- /app/src/main/res/xml/shortcuts.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 10 | 14 | 15 | -------------------------------------------------------------------------------- /build.gradle.kts: -------------------------------------------------------------------------------- 1 | import org.gradle.kotlin.dsl.libs 2 | 3 | buildscript { 4 | repositories { 5 | google() 6 | mavenCentral() 7 | } 8 | } 9 | 10 | plugins { 11 | alias(libs.plugins.androidApplication) apply false 12 | alias(libs.plugins.androidLibrary) apply false 13 | alias(libs.plugins.kotlinAndroid) apply false 14 | alias(libs.plugins.kotlinCompose) apply false 15 | alias(libs.plugins.hiltAndroid) apply false 16 | alias(libs.plugins.ksp) apply false 17 | alias(libs.plugins.kotlinSerialization) apply false 18 | alias(libs.plugins.androidTest) apply false 19 | } 20 | 21 | 22 | 23 | -------------------------------------------------------------------------------- /docs/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | Paperize Privacy Notice 5 | 14 | 15 | 16 |

Introduction

17 |

Welcome to Paperize! This privacy notice outlines how we collect, use, and protect your personal information when you use our mobile application. By using Paperize, you agree to the terms described in this notice.

18 | 19 |

Data Collection

20 |

Notification Access: Paperize requests notification access to provide you with personalized wallpaper recommendations based on your notifications. We do not store any notification content or personally identifiable information (PII) outside of your device.

21 |

Local Files: Paperize accesses files stored on your device to allow you to set them as wallpapers. We do not upload or transfer these files to any external servers.

22 | 23 |

Information Usage

24 |

Personal Data: We do not collect or store any personal data such as names, email addresses, or phone numbers.

25 |

Usage Analytics: Paperize does not track your usage behavior or collect analytics data.

26 | 27 |

Data Security

28 |

On-Device Storage: All data used by Paperize remains on your device. We do not transmit or store any data externally.

29 | 30 |

Third-Party Services

31 |

Advertisements: Paperize does not display third-party ads.

32 |

External Links: Our app may contain links to external websites. Please note that we are not responsible for the privacy practices of these sites.

33 | 34 |

Children’s Privacy

35 |

Paperize is not intended for children under the age of 13. We do not knowingly collect any information from children.

36 | 37 |

Changes to this notice

38 |

We may update this privacy notice from time to time. Any changes will be reflected in the app and on our website.

39 | 40 |

Contact Us

41 |

If you have any questions or concerns about this privacy notice, please contact me at anthonyyla.dev@gmail.com.

42 | 43 | 44 | 45 | 46 |

Android Permissions

47 |

SET_WALLPAPER

48 |

This permission allows an application to set the system wallpaper. With this permission, the app can change the background wallpaper of the your device.

49 |

POST_NOTIFICATIONS

50 |

This permission allows the app to notify the user when the wallpaper has been changed. It is optional and up to the user whether they want to be notified or not.

51 |

RECEIVE_BOOT_COMPLETED

52 |

This permission allows an application to receive a broadcast after the system finishes booting. This allows the application to reset its alarms after booting. If this permission is not granted, the wallpaper changer will not work after a reboot.

53 | 54 | 55 | -------------------------------------------------------------------------------- /gradle.properties: -------------------------------------------------------------------------------- 1 | # Project-wide Gradle settings. 2 | # IDE (e.g. Android Studio) users: 3 | # Gradle settings configured through the IDE *will override* 4 | # any settings specified in this file. 5 | # For more details on how to configure your build environment visit 6 | # http://www.gradle.org/docs/current/userguide/build_environment.html 7 | # Specifies the JVM arguments used for the daemon process. 8 | # The setting is particularly useful for tweaking memory settings. 9 | org.gradle.jvmargs=-Xmx2048m -Dfile.encoding=UTF-8 10 | # When configured, Gradle will run in incubating parallel mode. 11 | # This option should only be used with decoupled projects. More details, visit 12 | # http://www.gradle.org/docs/current/userguide/multi_project_builds.html#sec:decoupled_projects 13 | # org.gradle.parallel=true 14 | # AndroidX package structure to make it clearer which packages are bundled with the 15 | # Android operating system, and which are packaged with your app's APK 16 | # https://developer.android.com/topic/libraries/support-library/androidx-rn 17 | android.useAndroidX=true 18 | # Kotlin code style for this project: "official" or "obsolete": 19 | kotlin.code.style=official 20 | # Enables namespacing of each library's R class so that its R class includes only the 21 | # resources declared in the library itself and none from the library's dependencies, 22 | # thereby reducing the size of the R class for that library 23 | android.nonTransitiveRClass=true 24 | org.gradle.configuration-cache=true -------------------------------------------------------------------------------- /gradle/libs.versions.toml: -------------------------------------------------------------------------------- 1 | [versions] 2 | androidGradlePlugin = "8.10.0" 3 | kotlin = "2.1.21" 4 | hilt = "2.56.2" 5 | ksp = "2.1.21-2.0.1" 6 | 7 | [libraries] 8 | accompanist-permissions = { module = "com.google.accompanist:accompanist-permissions", version = "0.37.3" } 9 | androidx-activity-compose = { module = "androidx.activity:activity-compose", version = "1.10.1" } 10 | androidx-animation = { module = "androidx.compose.animation:animation", version = "1.8.1" } 11 | androidx-compose-bom = { module = "androidx.compose:compose-bom", version = "2025.05.00" } 12 | androidx-core-ktx = { module = "androidx.core:core-ktx", version = "1.16.0" } 13 | androidx-core-splashscreen = { module = "androidx.core:core-splashscreen", version = "1.2.0-beta02" } 14 | androidx-datastore-preferences = { module = "androidx.datastore:datastore-preferences", version = "1.1.6" } 15 | androidx-datastore = { module = "androidx.datastore:datastore", version = "1.1.6" } 16 | androidx-documentfile = { module = "androidx.documentfile:documentfile", version = "1.1.0" } 17 | androidx-espresso-core = { module = "androidx.test.espresso:espresso-core", version = "3.6.1" } 18 | androidx-foundation = { module = "androidx.compose.foundation:foundation", version = "1.8.1" } 19 | androidx-hilt-navigation-compose = { module = "androidx.hilt:hilt-navigation-compose", version = "1.2.0" } 20 | androidx-hilt-work = { module = "androidx.hilt:hilt-work", version = "1.2.0" } 21 | androidx-junit = { module = "androidx.test.ext:junit", version = "1.2.1" } 22 | androidx-lifecycle-runtime-compose = { module = "androidx.lifecycle:lifecycle-runtime-compose", version = "2.9.0" } 23 | androidx-lifecycle-runtime-ktx = { module = "androidx.lifecycle:lifecycle-runtime-ktx", version = "2.9.0" } 24 | androidx-material = { module = "androidx.compose.material:material", version = "1.8.1" } 25 | androidx-material-icons-extended = { module = "androidx.compose.material:material-icons-extended", version = "1.7.8" } 26 | androidx-material3 = { module = "androidx.compose.material3:material3", version = "1.4.0-alpha14" } 27 | androidx-navigation-compose = { module = "androidx.navigation:navigation-compose", version = "2.9.0" } 28 | androidx-profileinstaller = { module = "androidx.profileinstaller:profileinstaller", version = "1.4.1" } 29 | androidx-room-ktx = { module = "androidx.room:room-ktx", version = "2.7.1" } 30 | androidx-room-compiler = { module = "androidx.room:room-compiler", version = "2.7.1" } 31 | androidx-room-runtime = { module = "androidx.room:room-runtime", version = "2.7.1" } 32 | androidx-ui = { module = "androidx.compose.ui:ui", version = "1.8.1" } 33 | androidx-ui-graphics = { module = "androidx.compose.ui:ui-graphics", version = "1.8.1" } 34 | androidx-ui-test-junit4 = { module = "androidx.compose.ui:ui-test-junit4", version = "1.8.1" } 35 | androidx-ui-test-manifest = { module = "androidx.compose.ui:ui-test-manifest", version = "1.8.1" } 36 | androidx-ui-tooling = { module = "androidx.compose.ui:ui-tooling", version = "1.8.1" } 37 | androidx-ui-tooling-preview = { module = "androidx.compose.ui:ui-tooling-preview", version = "1.8.1" } 38 | androidx-work-runtime-ktx = { module = "androidx.work:work-runtime-ktx", version = "2.10.1" } 39 | dfc = { module = "com.lazygeniouz:dfc", version = "1.2" } 40 | gson = { module = "com.google.code.gson:gson", version = "2.13.1" } 41 | hilt-android-compiler = { module = "com.google.dagger:hilt-android-compiler", version = "2.56.2" } 42 | hilt-android = { module = "com.google.dagger:hilt-android", version = "2.56.2" } 43 | junit = { module = "junit:junit", version = "4.13.2" } 44 | kotlinx-serialization-json = { module = "org.jetbrains.kotlinx:kotlinx-serialization-json", version = "1.8.1" } 45 | landscapist-glide = { module = "com.github.skydoves:landscapist-glide", version = "2.4.8" } 46 | lazycolumnscrollbar = { module = "com.github.nanihadesuka:LazyColumnScrollbar", version = "2.2.0" } 47 | lottie-compose = { module = "com.airbnb.android:lottie-compose", version = "6.6.6" } 48 | taskerpluginlibrary = { module = "com.joaomgcd:taskerpluginlibrary", version = "0.4.10" } 49 | toolbar-compose = { module = "me.onebone:toolbar-compose", version = "2.3.5" } 50 | zoomable = { module = "net.engawapg.lib:zoomable", version = "2.7.0" } 51 | reorderable = { group = "sh.calvin.reorderable", name = "reorderable", version = "2.4.3" } 52 | androidx-glance-appwidget = { group = "androidx.glance", name = "glance-appwidget", version = "1.1.1" } 53 | androidx-glance-material3 = { group = "androidx.glance", name = "glance-material3", version = "1.1.1" } 54 | androidx-glance-material = { group = "androidx.glance", name = "glance-material", version = "1.1.1" } 55 | 56 | [plugins] 57 | androidApplication = { id = "com.android.application", version.ref = "androidGradlePlugin" } 58 | androidLibrary = { id = "com.android.library", version.ref = "androidGradlePlugin" } 59 | kotlinAndroid = { id = "org.jetbrains.kotlin.android", version.ref = "kotlin" } 60 | kotlinCompose = { id = "org.jetbrains.kotlin.plugin.compose", version.ref = "kotlin" } 61 | hiltAndroid = { id = "com.google.dagger.hilt.android", version.ref = "hilt" } 62 | ksp = { id = "com.google.devtools.ksp", version.ref = "ksp" } 63 | kotlinSerialization = { id = "org.jetbrains.kotlin.plugin.serialization", version.ref = "kotlin" } 64 | androidTest = { id = "com.android.test", version.ref = "androidGradlePlugin" } -------------------------------------------------------------------------------- /gradle/wrapper/gradle-wrapper.jar: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Anthonyy232/Paperize/89450026526cb5c75e549a80a2a914602075817d/gradle/wrapper/gradle-wrapper.jar -------------------------------------------------------------------------------- /gradle/wrapper/gradle-wrapper.properties: -------------------------------------------------------------------------------- 1 | #Wed May 14 17:15:30 PDT 2025 2 | distributionBase=GRADLE_USER_HOME 3 | distributionPath=wrapper/dists 4 | distributionSha256Sum=61ad310d3c7d3e5da131b76bbf22b5a4c0786e9d892dae8c1658d4b484de3caa 5 | distributionUrl=https\://services.gradle.org/distributions/gradle-8.14-bin.zip 6 | networkTimeout=10000 7 | validateDistributionUrl=true 8 | zipStoreBase=GRADLE_USER_HOME 9 | zipStorePath=wrapper/dists 10 | -------------------------------------------------------------------------------- /gradlew.bat: -------------------------------------------------------------------------------- 1 | @rem 2 | @rem Copyright 2015 the original author or authors. 3 | @rem 4 | @rem Licensed under the Apache License, Version 2.0 (the "License"); 5 | @rem you may not use this file except in compliance with the License. 6 | @rem You may obtain a copy of the License at 7 | @rem 8 | @rem https://www.apache.org/licenses/LICENSE-2.0 9 | @rem 10 | @rem Unless required by applicable law or agreed to in writing, software 11 | @rem distributed under the License is distributed on an "AS IS" BASIS, 12 | @rem WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | @rem See the License for the specific language governing permissions and 14 | @rem limitations under the License. 15 | @rem 16 | @rem SPDX-License-Identifier: Apache-2.0 17 | @rem 18 | 19 | @if "%DEBUG%"=="" @echo off 20 | @rem ########################################################################## 21 | @rem 22 | @rem Gradle startup script for Windows 23 | @rem 24 | @rem ########################################################################## 25 | 26 | @rem Set local scope for the variables with windows NT shell 27 | if "%OS%"=="Windows_NT" setlocal 28 | 29 | set DIRNAME=%~dp0 30 | if "%DIRNAME%"=="" set DIRNAME=. 31 | @rem This is normally unused 32 | set APP_BASE_NAME=%~n0 33 | set APP_HOME=%DIRNAME% 34 | 35 | @rem Resolve any "." and ".." in APP_HOME to make it shorter. 36 | for %%i in ("%APP_HOME%") do set APP_HOME=%%~fi 37 | 38 | @rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. 39 | set DEFAULT_JVM_OPTS="-Xmx64m" "-Xms64m" 40 | 41 | @rem Find java.exe 42 | if defined JAVA_HOME goto findJavaFromJavaHome 43 | 44 | set JAVA_EXE=java.exe 45 | %JAVA_EXE% -version >NUL 2>&1 46 | if %ERRORLEVEL% equ 0 goto execute 47 | 48 | echo. 1>&2 49 | echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. 1>&2 50 | echo. 1>&2 51 | echo Please set the JAVA_HOME variable in your environment to match the 1>&2 52 | echo location of your Java installation. 1>&2 53 | 54 | goto fail 55 | 56 | :findJavaFromJavaHome 57 | set JAVA_HOME=%JAVA_HOME:"=% 58 | set JAVA_EXE=%JAVA_HOME%/bin/java.exe 59 | 60 | if exist "%JAVA_EXE%" goto execute 61 | 62 | echo. 1>&2 63 | echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% 1>&2 64 | echo. 1>&2 65 | echo Please set the JAVA_HOME variable in your environment to match the 1>&2 66 | echo location of your Java installation. 1>&2 67 | 68 | goto fail 69 | 70 | :execute 71 | @rem Setup the command line 72 | 73 | set CLASSPATH= 74 | 75 | 76 | @rem Execute Gradle 77 | "%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" -jar "%APP_HOME%\gradle\wrapper\gradle-wrapper.jar" %* 78 | 79 | :end 80 | @rem End local scope for the variables with windows NT shell 81 | if %ERRORLEVEL% equ 0 goto mainEnd 82 | 83 | :fail 84 | rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of 85 | rem the _cmd.exe /c_ return code! 86 | set EXIT_CODE=%ERRORLEVEL% 87 | if %EXIT_CODE% equ 0 set EXIT_CODE=1 88 | if not ""=="%GRADLE_EXIT_CONSOLE%" exit %EXIT_CODE% 89 | exit /b %EXIT_CODE% 90 | 91 | :mainEnd 92 | if "%OS%"=="Windows_NT" endlocal 93 | 94 | :omega 95 | -------------------------------------------------------------------------------- /metadata/en-US/full_description.txt: -------------------------------------------------------------------------------- 1 | Paperize - A Wallpaper Changer is a dynamic and versatile app that breathes life into your device’s aesthetics. It’s not just a wallpaper changer; it’s a tool that allows you to personalize your device to reflect your style and mood. 2 | 3 | Dynamic Wallpaper Changer: With Paperize, you can set your wallpaper to change at specific time intervals. This means your device’s look will always be fresh and exciting. 4 | 5 | Advanced Image Manipulation: Paperize isn’t just about changing wallpapers; it’s about enhancing them. You can darken or blur your wallpapers to suit your visual preferences or to make your icons stand out. 6 | 7 | Home and Lock Screen Customization: With Paperize, you have the freedom to set your home screen and lock screen wallpapers together or separately, and at the same time or not. This gives you the ultimate control over your device’s look and feel. 8 | 9 | Supports Various Image Types: Paperize can handle a wide range of image types, offering you flexibility in your wallpaper choices. Whether it’s a JPEG, PNG, AVIF or a WEBP, Paperize has got you covered. 10 | 11 | Folder Support: Organize your wallpapers into folders for auto-updating. This feature allows you to categorize your wallpapers and have them change automatically, keeping your device’s look organized and cohesive. 12 | 13 | On-Device Storage: All your wallpapers and settings are stored on your device, ensuring quick loading times and reducing data usage. Your favorite wallpapers are always just a tap away. 14 | 15 | Experience the joy of a personalized device with Paperize - A Wallpaper Changer. Your device, your style. 16 | 17 | Privacy policy: https://anthonyy232.github.io/Paperize/ -------------------------------------------------------------------------------- /metadata/en-US/images/featureGraphic.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Anthonyy232/Paperize/89450026526cb5c75e549a80a2a914602075817d/metadata/en-US/images/featureGraphic.png -------------------------------------------------------------------------------- /metadata/en-US/images/icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Anthonyy232/Paperize/89450026526cb5c75e549a80a2a914602075817d/metadata/en-US/images/icon.png -------------------------------------------------------------------------------- /metadata/en-US/images/phoneScreenshots/1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Anthonyy232/Paperize/89450026526cb5c75e549a80a2a914602075817d/metadata/en-US/images/phoneScreenshots/1.png -------------------------------------------------------------------------------- /metadata/en-US/images/phoneScreenshots/2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Anthonyy232/Paperize/89450026526cb5c75e549a80a2a914602075817d/metadata/en-US/images/phoneScreenshots/2.png -------------------------------------------------------------------------------- /metadata/en-US/images/phoneScreenshots/3.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Anthonyy232/Paperize/89450026526cb5c75e549a80a2a914602075817d/metadata/en-US/images/phoneScreenshots/3.png -------------------------------------------------------------------------------- /metadata/en-US/images/phoneScreenshots/4.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Anthonyy232/Paperize/89450026526cb5c75e549a80a2a914602075817d/metadata/en-US/images/phoneScreenshots/4.png -------------------------------------------------------------------------------- /metadata/en-US/images/phoneScreenshots/5.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Anthonyy232/Paperize/89450026526cb5c75e549a80a2a914602075817d/metadata/en-US/images/phoneScreenshots/5.png -------------------------------------------------------------------------------- /metadata/en-US/short_description.txt: -------------------------------------------------------------------------------- 1 | Paperize is an efficient wallpaper changer application with a clean design -------------------------------------------------------------------------------- /metadata/en-US/title.txt: -------------------------------------------------------------------------------- 1 | Paperize - A Wallpaper Changer -------------------------------------------------------------------------------- /metadata/en-US/video.txt: -------------------------------------------------------------------------------- 1 | https://youtu.be/oQCAjHB2sIo -------------------------------------------------------------------------------- /renovate.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "https://docs.renovatebot.com/renovate-schema.json", 3 | "extends": [ 4 | "config:recommended" 5 | ], 6 | "packageRules": [ 7 | { 8 | "matchUpdateTypes": ["minor", "patch", "pin", "digest"], 9 | "automerge": true 10 | }, 11 | { 12 | "matchDepTypes": ["devDependencies"], 13 | "automerge": true 14 | } 15 | ], 16 | "platformAutomerge": true 17 | } 18 | -------------------------------------------------------------------------------- /settings.gradle.kts: -------------------------------------------------------------------------------- 1 | pluginManagement { 2 | repositories { 3 | google() 4 | mavenCentral() 5 | gradlePluginPortal() 6 | } 7 | } 8 | @Suppress("UnstableApiUsage") 9 | dependencyResolutionManagement { 10 | repositoriesMode.set(RepositoriesMode.FAIL_ON_PROJECT_REPOS) 11 | repositories { 12 | google() 13 | mavenCentral() 14 | maven { url = uri("https://www.jitpack.io" ) } 15 | } 16 | } 17 | rootProject.name = "Paperize" 18 | include(":app") 19 | --------------------------------------------------------------------------------