├── .github ├── FUNDING.yml ├── ISSUE_TEMPLATE │ ├── bug_report.md │ └── feature_request.md └── workflows │ ├── android-release.yml │ └── static.yml ├── .gitignore ├── LICENSE ├── README.md ├── app ├── .gitignore ├── build.gradle.kts ├── proguard-rules.pro └── src │ └── main │ ├── AndroidManifest.xml │ ├── java │ └── com │ │ └── anthonyla │ │ └── paperize │ │ ├── App.kt │ │ ├── core │ │ ├── ScalingConstants.kt │ │ ├── SettingsConstants.kt │ │ ├── VignetteBitmapTransformation.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 │ │ │ │ ├── SkipLandscapeSwitch.kt │ │ │ │ ├── SkipNonInteractiveSwitch.kt │ │ │ │ ├── TimeSliders.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/android-release.yml: -------------------------------------------------------------------------------- 1 | # =================================================================== 2 | # Paperize CI/CD Pipeline 3 | # 4 | # Automates the building and release process for the Paperize Android application. 5 | # 6 | # Workflow Architecture: 7 | # 1. Build: Compiles and signs the release APK. 8 | # 2. Release: Publishes the APK to a draft GitHub Release, but only for version tags. 9 | # 10 | # Triggering Conditions: 11 | # - On push to `master`: 12 | # - Executes the `build` job. 13 | # - Produces a versioned build artifact for testing and internal distribution. 14 | # - On push of a tag (e.g., `v1.2.3`): 15 | # - Executes both jobs: `build` and `release`. 16 | # - Creates a formal, versioned draft release on GitHub with the signed APK attached. 17 | # =================================================================== 18 | name: Paperize CI/CD Pipeline 19 | 20 | on: 21 | push: 22 | branches: [ "master" ] 23 | tags: [ 'v*' ] 24 | 25 | jobs: 26 | # =================================================================== 27 | # JOB: build 28 | # 29 | # Compiles, signs, and packages the release APK. This job produces a 30 | # versioned artifact that is used for both internal testing and the final release. 31 | # =================================================================== 32 | build: 33 | name: Build Signed APK 34 | runs-on: ubuntu-latest 35 | steps: 36 | - name: Checkout repository source code 37 | uses: actions/checkout@v5 38 | 39 | - name: Set up JDK 17 40 | uses: actions/setup-java@v5 41 | with: 42 | java-version: '17' 43 | distribution: 'temurin' 44 | cache: gradle 45 | 46 | - name: Grant execute permission for gradlew 47 | # The gradlew wrapper requires execute permissions to run on the Linux-based runner. 48 | run: chmod +x gradlew 49 | 50 | - name: Decode Base64 Keystore 51 | # Reconstructs the binary .jks keystore file from the Base64 encoded secret. 52 | env: 53 | SIGNING_KEYSTORE_BASE64: ${{ secrets.SIGNING_KEYSTORE_BASE64 }} 54 | run: | 55 | echo $SIGNING_KEYSTORE_BASE64 | base64 --decode > ${{ github.workspace }}/keystore.jks 56 | 57 | - name: Build Release APK 58 | env: 59 | SIGNING_KEYSTORE_PATH: ${{ github.workspace }}/keystore.jks 60 | SIGNING_KEY_ALIAS: ${{ secrets.SIGNING_KEY_ALIAS }} 61 | SIGNING_STORE_PASSWORD: ${{ secrets.SIGNING_STORE_PASSWORD }} 62 | SIGNING_KEY_PASSWORD: ${{ secrets.SIGNING_KEY_PASSWORD }} 63 | run: ./gradlew assembleRelease 64 | 65 | - name: Get Version Name 66 | # Determines the application version for artifact naming. 67 | # - For tag pushes (e.g., 'v1.2.3'), it uses the tag name directly. 68 | # - For master branch pushes, it parses the `versionName` from the app's build.gradle.kts file. 69 | id: get_version 70 | shell: bash 71 | run: | 72 | if [[ $GITHUB_REF == refs/tags/v* ]]; then 73 | VERSION_NAME=${GITHUB_REF_NAME} 74 | else 75 | RAW_VERSION=$(grep "versionName =" app/build.gradle.kts | awk -F'"' '{print $2}') 76 | VERSION_NAME="v${RAW_VERSION}" 77 | fi 78 | echo "VERSION_NAME=$VERSION_NAME" >> $GITHUB_OUTPUT 79 | 80 | - name: Upload APK as a build artifact 81 | # Archives the generated APK with a dynamic, versioned name. This artifact is essential 82 | # for passing the APK file to the subsequent `release` job. 83 | uses: actions/upload-artifact@v4 84 | with: 85 | name: paperize-${{ steps.get_version.outputs.VERSION_NAME }} 86 | path: app/build/outputs/apk/release/paperize-v*.apk 87 | if-no-files-found: error 88 | 89 | # =================================================================== 90 | # JOB: release 91 | # 92 | # Creates a draft GitHub Release and attaches the signed APK. This job is 93 | # conditional and will only execute for pushes that include a version tag. 94 | # =================================================================== 95 | release: 96 | name: Create GitHub Release 97 | runs-on: ubuntu-latest 98 | needs: build 99 | if: startsWith(github.ref, 'refs/tags/') 100 | steps: 101 | - name: Download signed APK artifact 102 | # Downloads the versioned artifact produced by the `build` job. The pattern ensures 103 | # it finds the correct artifact regardless of the specific version number. 104 | uses: actions/download-artifact@v5 105 | with: 106 | pattern: paperize-v* 107 | path: ./apk-artifact 108 | merge-multiple: true 109 | 110 | - name: Create Draft Release 111 | # Publishes a new release to GitHub. `draft: true` ensures the release is not made 112 | # public until it has been manually reviewed and published. 113 | uses: softprops/action-gh-release@v2 114 | with: 115 | files: ./apk-artifact/paperize-v*.apk 116 | draft: true 117 | generate_release_notes: true 118 | -------------------------------------------------------------------------------- /.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@v5 34 | - name: Setup Pages 35 | uses: actions/configure-pages@v5 36 | - name: Upload artifact 37 | uses: actions/upload-pages-artifact@v4 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 = 36 17 | 18 | signingConfigs { 19 | create("release") { 20 | val storeFilePath = System.getenv("SIGNING_KEYSTORE_PATH") 21 | if (storeFilePath != null) { 22 | storeFile = file(storeFilePath) 23 | storePassword = System.getenv("SIGNING_STORE_PASSWORD") 24 | keyAlias = System.getenv("SIGNING_KEY_ALIAS") 25 | keyPassword = System.getenv("SIGNING_KEY_PASSWORD") 26 | } 27 | } 28 | } 29 | 30 | defaultConfig { 31 | applicationId = "com.anthonyla.paperize" 32 | minSdk = 31 33 | targetSdk = 36 34 | versionCode = 42 35 | versionName = "3.2.1" 36 | 37 | testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner" 38 | vectorDrawables { 39 | useSupportLibrary = true 40 | } 41 | } 42 | 43 | buildTypes { 44 | release { 45 | isMinifyEnabled = true 46 | isShrinkResources = true 47 | proguardFiles( 48 | getDefaultProguardFile("proguard-android-optimize.txt"), 49 | "proguard-rules.pro" 50 | ) 51 | signingConfig = signingConfigs.getByName("release") 52 | } 53 | } 54 | 55 | compileOptions { 56 | sourceCompatibility = JavaVersion.VERSION_17 57 | targetCompatibility = JavaVersion.VERSION_17 58 | } 59 | 60 | @Suppress("DEPRECATION") 61 | kotlinOptions { 62 | jvmTarget = "17" 63 | } 64 | 65 | buildFeatures { 66 | compose = true 67 | viewBinding = true 68 | buildConfig = true 69 | } 70 | 71 | androidResources { 72 | @Suppress("UnstableApiUsage") 73 | generateLocaleConfig = true 74 | } 75 | 76 | buildToolsVersion = "35.0.1" 77 | dependenciesInfo { 78 | includeInApk = false 79 | includeInBundle = false 80 | } 81 | 82 | applicationVariants.all { 83 | this.outputs 84 | .map { it as com.android.build.gradle.internal.api.ApkVariantOutputImpl } 85 | .forEach { output -> 86 | val apkName = "paperize-v${this.versionName}.apk" 87 | output.outputFileName = apkName 88 | } 89 | } 90 | } 91 | 92 | dependencies { 93 | implementation(libs.androidx.core.ktx) 94 | implementation(libs.androidx.lifecycle.runtime.ktx) 95 | implementation(libs.androidx.activity.compose) 96 | implementation(platform(libs.androidx.compose.bom)) 97 | implementation(libs.androidx.ui) 98 | implementation(libs.androidx.ui.graphics) 99 | implementation(libs.androidx.ui.tooling.preview) 100 | implementation(libs.androidx.material3) 101 | implementation(libs.androidx.navigation.compose) 102 | implementation(libs.androidx.material) 103 | implementation(libs.androidx.datastore) 104 | implementation(libs.androidx.datastore.preferences) 105 | implementation(libs.androidx.material.icons.extended) 106 | implementation(libs.androidx.hilt.navigation.compose) 107 | implementation(libs.androidx.animation) 108 | implementation(libs.androidx.core.splashscreen) 109 | implementation(libs.androidx.lifecycle.runtime.compose) 110 | implementation(libs.gson) 111 | implementation(libs.androidx.documentfile) 112 | implementation(libs.zoomable) 113 | implementation(libs.landscapist.glide) 114 | implementation(libs.androidx.work.runtime.ktx) 115 | implementation(libs.androidx.hilt.work) 116 | implementation(libs.lottie.compose) 117 | implementation(libs.accompanist.permissions) 118 | implementation(libs.androidx.foundation) 119 | implementation(libs.lazycolumnscrollbar) 120 | implementation(libs.taskerpluginlibrary) 121 | implementation(libs.androidx.profileinstaller) 122 | implementation(libs.reorderable) 123 | implementation(libs.androidx.glance.appwidget) 124 | implementation(libs.androidx.glance.material3) 125 | implementation(libs.androidx.glance.material) 126 | testImplementation(libs.junit) 127 | androidTestImplementation(libs.androidx.junit) 128 | androidTestImplementation(libs.androidx.espresso.core) 129 | androidTestImplementation(libs.androidx.ui.test.junit4) 130 | debugImplementation(libs.androidx.ui.tooling) 131 | debugImplementation(libs.androidx.ui.test.manifest) 132 | implementation(libs.hilt.android) 133 | ksp(libs.hilt.android.compiler) 134 | implementation(libs.androidx.room.runtime) 135 | ksp(libs.androidx.room.compiler) 136 | implementation(libs.androidx.room.ktx) 137 | implementation(libs.dfc) 138 | implementation (libs.kotlinx.serialization.json) 139 | implementation(libs.toolbar.compose) 140 | } -------------------------------------------------------------------------------- /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 | 14 | 15 | 26 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 39 | 40 | 41 | 43 | 45 | 51 | 52 | 53 | 55 | 56 | 57 | 58 | 59 | 60 | 61 | 62 | 64 | 65 | 66 | 67 | 70 | 71 | 76 | 78 | 79 | 80 | 85 | 86 | 87 | 88 | 89 | 93 | 94 | 95 | 96 | 97 | 98 | 99 | 100 | -------------------------------------------------------------------------------- /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 | 20 | // Increase CursorWindow size to handle large datasets 21 | // Default is usually 2MB, increasing to 10MB 22 | try { 23 | val field: Field = CursorWindow::class.java.getDeclaredField("sCursorWindowSize") 24 | field.isAccessible = true 25 | field.set(null, 10 * 1024 * 1024) // 10MB 26 | } catch (e: Exception) { 27 | // Fallback for newer Android versions where field might be different 28 | try { 29 | val field: Field = CursorWindow::class.java.getDeclaredField("CURSOR_WINDOW_SIZE") 30 | field.isAccessible = true 31 | field.set(null, 10 * 1024 * 1024) // 10MB 32 | } catch (e2: Exception) { 33 | e2.printStackTrace() 34 | } 35 | } 36 | 37 | val channel = NotificationChannel("wallpaper_service_channel", "Paperize", NotificationManager.IMPORTANCE_LOW) 38 | val notificationManager = getSystemService(NOTIFICATION_SERVICE) as NotificationManager 39 | notificationManager.createNotificationChannel(channel) 40 | } 41 | } -------------------------------------------------------------------------------- /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 | const val SKIP_LANDSCAPE = "skip_landscape" 51 | const val SKIP_NON_INTERACTIVE = "skip_non_interactive" 52 | } -------------------------------------------------------------------------------- /app/src/main/java/com/anthonyla/paperize/core/VignetteBitmapTransformation.kt: -------------------------------------------------------------------------------- 1 | package com.anthonyla.paperize.core 2 | 3 | import android.graphics.Bitmap 4 | import com.bumptech.glide.load.engine.bitmap_recycle.BitmapPool 5 | import com.bumptech.glide.load.resource.bitmap.BitmapTransformation 6 | import com.bumptech.glide.util.Util 7 | import java.security.MessageDigest 8 | 9 | /** 10 | * A BitmapTransformation that applies a vignette effect to a bitmap to be used with Glide 11 | */ 12 | class VignetteBitmapTransformation(private val percent: Int): BitmapTransformation() { 13 | 14 | override fun transform( 15 | pool: BitmapPool, 16 | toTransform: Bitmap, 17 | outWidth: Int, 18 | outHeight: Int 19 | ): Bitmap { 20 | return vignetteBitmap(toTransform, percent) 21 | } 22 | 23 | override fun equals(other: Any?): Boolean { 24 | return other is VignetteBitmapTransformation && other.percent == percent 25 | } 26 | 27 | override fun hashCode(): Int { 28 | return Util.hashCode(percent) 29 | } 30 | 31 | override fun updateDiskCacheKey(messageDigest: MessageDigest) { 32 | messageDigest.update((ID + percent).toByteArray(CHARSET)) 33 | } 34 | 35 | companion object { 36 | private const val ID = "com.bumptech.glide.transformations.VignettedBitmapTransformation" 37 | } 38 | } -------------------------------------------------------------------------------- /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/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 dagger.Module 12 | import dagger.Provides 13 | import dagger.hilt.InstallIn 14 | import dagger.hilt.android.qualifiers.ApplicationContext 15 | import dagger.hilt.components.SingletonComponent 16 | import javax.inject.Singleton 17 | 18 | /** 19 | * AppModule provides dependencies for the application for Dagger Hilt to inject 20 | */ 21 | @Module 22 | @InstallIn(SingletonComponent::class) 23 | object AppModule { 24 | @Provides 25 | @Singleton 26 | fun provideAlbumDatabase(app: Application): AlbumDatabase { 27 | return Room.databaseBuilder( 28 | app, 29 | AlbumDatabase::class.java, 30 | AlbumDatabase.DATABASE_NAME 31 | ).fallbackToDestructiveMigration(true).build() 32 | } 33 | 34 | @Provides 35 | @Singleton 36 | fun provideAlbumDao(db: AlbumDatabase) = db.albumDao 37 | 38 | @Provides 39 | @Singleton 40 | fun provideAlbumRepository( 41 | db: AlbumDatabase 42 | ): AlbumRepository { 43 | return AlbumRepositoryImpl(db.albumDao) 44 | } 45 | 46 | @Provides 47 | @Singleton 48 | fun provideSettingsDataStore ( 49 | @ApplicationContext context: Context 50 | ): SettingsDataStore = SettingsDataStoreImpl(context) 51 | 52 | @Provides 53 | @Singleton 54 | fun provideContext( 55 | @ApplicationContext context: Context, 56 | ): Context { return context } 57 | } -------------------------------------------------------------------------------- /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 = 12 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.border 4 | import androidx.compose.foundation.layout.Arrangement 5 | import androidx.compose.foundation.layout.Box 6 | import androidx.compose.foundation.layout.Column 7 | import androidx.compose.foundation.layout.Spacer 8 | import androidx.compose.foundation.layout.aspectRatio 9 | import androidx.compose.foundation.layout.fillMaxSize 10 | import androidx.compose.foundation.layout.fillMaxWidth 11 | import androidx.compose.foundation.layout.height 12 | import androidx.compose.foundation.layout.padding 13 | import androidx.compose.foundation.shape.RoundedCornerShape 14 | import androidx.compose.material3.Card 15 | import androidx.compose.material3.CardDefaults 16 | import androidx.compose.material3.MaterialTheme 17 | import androidx.compose.material3.Text 18 | import androidx.compose.runtime.Composable 19 | import androidx.compose.runtime.getValue 20 | import androidx.compose.runtime.mutableStateOf 21 | import androidx.compose.runtime.remember 22 | import androidx.compose.ui.Alignment 23 | import androidx.compose.ui.Modifier 24 | import androidx.compose.ui.draw.clip 25 | import androidx.compose.ui.layout.ContentScale 26 | import androidx.compose.ui.platform.LocalContext 27 | import androidx.compose.ui.text.style.TextOverflow 28 | import androidx.compose.ui.unit.IntSize 29 | import androidx.compose.ui.unit.dp 30 | import androidx.core.net.toUri 31 | import com.anthonyla.paperize.core.decompress 32 | import com.anthonyla.paperize.core.isValidUri 33 | import com.anthonyla.paperize.feature.wallpaper.domain.model.Album 34 | import com.skydoves.landscapist.ImageOptions 35 | import com.skydoves.landscapist.glide.GlideImage 36 | 37 | /** 38 | * AlbumItem composable is a single item view for the Album list. Shows the thumbnail cover image. 39 | */ 40 | @Composable 41 | fun AlbumItem( 42 | album: Album, 43 | onAlbumViewClick: () -> Unit, 44 | modifier: Modifier = Modifier, 45 | ) { 46 | val context = LocalContext.current 47 | val showCoverUri by remember(album.coverUri) { 48 | mutableStateOf(!album.coverUri.isNullOrEmpty() && isValidUri(context, album.coverUri)) 49 | } 50 | 51 | Card( 52 | colors = CardDefaults.cardColors(containerColor = MaterialTheme.colorScheme.surfaceVariant), 53 | modifier = modifier.fillMaxSize(), 54 | shape = RoundedCornerShape(16.dp), 55 | onClick = onAlbumViewClick 56 | ) { 57 | Column( 58 | modifier = Modifier.padding(12.dp), 59 | verticalArrangement = Arrangement.Top, 60 | horizontalAlignment = Alignment.Start 61 | ) { 62 | Box( 63 | modifier = Modifier 64 | .fillMaxWidth() 65 | .aspectRatio(1f) 66 | .border( 67 | width = 1.dp, 68 | color = MaterialTheme.colorScheme.onSurfaceVariant.copy(alpha = 0.1f), 69 | shape = RoundedCornerShape(12.dp) 70 | ) 71 | .clip(RoundedCornerShape(12.dp)), 72 | contentAlignment = Alignment.Center 73 | ) { 74 | if (showCoverUri) { 75 | GlideImage( 76 | imageModel = { 77 | album.coverUri?.decompress("content://com.android.externalstorage.documents/")?.toUri() 78 | }, 79 | imageOptions = ImageOptions( 80 | contentScale = ContentScale.Crop, 81 | alignment = Alignment.Center, 82 | requestSize = IntSize(300, 300), 83 | ), 84 | modifier = Modifier.fillMaxSize() 85 | ) 86 | } 87 | } 88 | 89 | Spacer(modifier = Modifier.height(12.dp)) 90 | 91 | Text( 92 | text = album.displayedAlbumName, 93 | style = MaterialTheme.typography.titleSmall, 94 | maxLines = 1, 95 | overflow = TextOverflow.Ellipsis 96 | ) 97 | } 98 | } 99 | } -------------------------------------------------------------------------------- /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.aspectRatio 9 | import androidx.compose.foundation.layout.fillMaxSize 10 | import androidx.compose.foundation.layout.fillMaxWidth 11 | import androidx.compose.foundation.layout.padding 12 | import androidx.compose.foundation.lazy.grid.GridCells 13 | import androidx.compose.foundation.lazy.grid.LazyVerticalGrid 14 | import androidx.compose.foundation.lazy.grid.items 15 | import androidx.compose.foundation.lazy.grid.rememberLazyGridState 16 | import androidx.compose.foundation.shape.RoundedCornerShape 17 | import androidx.compose.material3.MaterialTheme 18 | import androidx.compose.material3.Scaffold 19 | import androidx.compose.runtime.Composable 20 | import androidx.compose.runtime.remember // Added remember import 21 | import androidx.compose.ui.Modifier 22 | import androidx.compose.ui.unit.dp 23 | import com.anthonyla.paperize.feature.wallpaper.domain.model.Folder 24 | import com.anthonyla.paperize.feature.wallpaper.presentation.album.components.WallpaperItem 25 | import com.anthonyla.paperize.feature.wallpaper.presentation.folder_view_screen.components.FolderViewTopBar 26 | import my.nanihadesuka.compose.LazyVerticalGridScrollbar 27 | import my.nanihadesuka.compose.ScrollbarSettings 28 | 29 | @Composable 30 | fun FolderViewScreen( 31 | folder: Folder, 32 | onBackClick: () -> Unit, 33 | onShowWallpaperView: (String, String) -> Unit, 34 | animate: Boolean 35 | ) { 36 | val lazyListState = rememberLazyGridState() 37 | val colorScheme = MaterialTheme.colorScheme 38 | 39 | val scrollbarSettings = remember { 40 | ScrollbarSettings.Default.copy( 41 | thumbUnselectedColor = colorScheme.primary, 42 | thumbSelectedColor = colorScheme.primary, 43 | thumbShape = RoundedCornerShape(16.dp), 44 | scrollbarPadding = 1.dp, 45 | ) 46 | } 47 | 48 | val noOpItemSelection: () -> Unit = remember { { /* Do nothing */ } } 49 | 50 | BackHandler { onBackClick() } 51 | 52 | Scaffold( 53 | topBar = { 54 | FolderViewTopBar( 55 | title = folder.folderName ?: "", 56 | onBackClick = onBackClick 57 | ) 58 | }, 59 | content = { paddingValues -> 60 | LazyVerticalGridScrollbar( 61 | state = lazyListState, 62 | settings = scrollbarSettings, 63 | modifier = Modifier 64 | .fillMaxSize() 65 | .padding(paddingValues) 66 | ) { 67 | LazyVerticalGrid( 68 | state = lazyListState, 69 | modifier = Modifier.fillMaxSize(), 70 | columns = GridCells.Adaptive(150.dp), 71 | contentPadding = PaddingValues(4.dp), 72 | horizontalArrangement = Arrangement.spacedBy(4.dp), 73 | verticalArrangement = Arrangement.spacedBy(4.dp) 74 | ) { 75 | items(items = folder.wallpapers, key = { wallpaper -> wallpaper.wallpaperUri }) { wallpaper -> 76 | val itemModifier = remember(animate) { 77 | Modifier 78 | .fillMaxWidth() 79 | .aspectRatio(9f / 16f) 80 | .then( 81 | if (animate) Modifier.animateItem( 82 | placementSpec = tween( 83 | durationMillis = 800, 84 | delayMillis = 0, 85 | easing = FastOutSlowInEasing 86 | ) 87 | ) else Modifier 88 | ) 89 | } 90 | 91 | WallpaperItem( 92 | wallpaperUri = wallpaper.wallpaperUri, 93 | itemSelected = false, 94 | selectionMode = false, 95 | allowHapticFeedback = false, 96 | onItemSelection = noOpItemSelection, 97 | onWallpaperViewClick = { 98 | onShowWallpaperView(wallpaper.wallpaperUri, wallpaper.fileName) 99 | }, 100 | modifier = itemModifier 101 | ) 102 | } 103 | } 104 | } 105 | } 106 | ) 107 | } -------------------------------------------------------------------------------- /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/notifications_screen/NotificationScreen.kt: -------------------------------------------------------------------------------- 1 | package com.anthonyla.paperize.feature.wallpaper.presentation.notifications_screen 2 | 3 | import android.Manifest 4 | import android.content.pm.PackageManager 5 | import android.os.Build 6 | import androidx.activity.compose.rememberLauncherForActivityResult 7 | import androidx.activity.result.contract.ActivityResultContracts 8 | import androidx.compose.foundation.layout.Column 9 | import androidx.compose.foundation.layout.PaddingValues 10 | import androidx.compose.foundation.layout.Spacer 11 | import androidx.compose.foundation.layout.fillMaxHeight 12 | import androidx.compose.foundation.layout.height 13 | import androidx.compose.foundation.layout.padding 14 | import androidx.compose.material.icons.Icons 15 | import androidx.compose.material.icons.outlined.ArrowCircleRight 16 | import androidx.compose.material.icons.outlined.Info 17 | import androidx.compose.material3.ExtendedFloatingActionButton 18 | import androidx.compose.material3.Icon 19 | import androidx.compose.material3.MaterialTheme 20 | import androidx.compose.material3.Scaffold 21 | import androidx.compose.material3.Text 22 | import androidx.compose.runtime.Composable 23 | import androidx.compose.runtime.LaunchedEffect 24 | import androidx.compose.runtime.getValue 25 | import androidx.compose.runtime.mutableStateOf 26 | import androidx.compose.runtime.saveable.rememberSaveable 27 | import androidx.compose.ui.Alignment 28 | import androidx.compose.ui.ExperimentalComposeUiApi 29 | import androidx.compose.ui.Modifier 30 | import androidx.compose.ui.platform.LocalContext 31 | import androidx.compose.ui.res.stringResource 32 | import androidx.compose.ui.semantics.contentDescription 33 | import androidx.compose.ui.semantics.semantics 34 | import androidx.compose.ui.unit.dp 35 | import androidx.core.content.ContextCompat 36 | import com.airbnb.lottie.compose.LottieAnimation 37 | import com.airbnb.lottie.compose.LottieCompositionSpec 38 | import com.airbnb.lottie.compose.LottieConstants 39 | import com.airbnb.lottie.compose.animateLottieCompositionAsState 40 | import com.airbnb.lottie.compose.rememberLottieComposition 41 | import com.anthonyla.paperize.R 42 | 43 | @OptIn(ExperimentalComposeUiApi::class) 44 | @Composable 45 | fun NotificationScreen( 46 | onAgree: () -> Unit 47 | ) { 48 | val context = LocalContext.current 49 | val composition by rememberLottieComposition(LottieCompositionSpec.RawRes(R.raw.notification_animation)) 50 | val progress by animateLottieCompositionAsState( 51 | composition = composition, 52 | iterations = LottieConstants.IterateForever, 53 | ) 54 | val askPermission = rememberSaveable { mutableStateOf(false) } 55 | val requestPermissionLauncher = rememberLauncherForActivityResult( 56 | contract = ActivityResultContracts.RequestPermission() 57 | ) { onAgree() } 58 | 59 | fun handleFabClick() { 60 | if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) { 61 | when { 62 | (ContextCompat.checkSelfPermission(context, Manifest.permission.POST_NOTIFICATIONS) == PackageManager.PERMISSION_GRANTED) -> { 63 | onAgree() 64 | } 65 | else -> { 66 | requestPermissionLauncher.launch(Manifest.permission.POST_NOTIFICATIONS) 67 | } 68 | } 69 | } else { onAgree() } 70 | } 71 | 72 | LaunchedEffect (askPermission.value) { 73 | if (askPermission.value) { 74 | handleFabClick() 75 | } 76 | } 77 | 78 | Scaffold( 79 | content = { it 80 | Column (modifier = Modifier.padding(32.dp)) { 81 | Spacer(modifier = Modifier.height(120.dp)) 82 | Text(text = stringResource(id = R.string.notifications), style = MaterialTheme.typography.headlineLarge) 83 | Spacer(modifier = Modifier.height(16.dp)) 84 | LottieAnimation( 85 | composition = composition, 86 | progress = { progress }, 87 | modifier = Modifier 88 | .fillMaxHeight(0.5f) 89 | .align(Alignment.CenterHorizontally) 90 | .semantics { contentDescription = context.getString(R.string.notification_bell_animation) }, 91 | safeMode = true, 92 | enableMergePaths = true 93 | ) 94 | Icon( 95 | imageVector = Icons.Outlined.Info, 96 | contentDescription = stringResource(R.string.agree), 97 | modifier = Modifier.padding(PaddingValues(vertical = 16.dp)) 98 | ) 99 | Text( 100 | text = stringResource(R.string.notification_info_message), 101 | style = MaterialTheme.typography.bodySmall 102 | ) 103 | } 104 | }, 105 | floatingActionButton = { 106 | ExtendedFloatingActionButton( 107 | modifier = Modifier.padding(8.dp), 108 | onClick = { askPermission.value = true }, 109 | icon = { 110 | Icon( 111 | imageVector = Icons.Outlined.ArrowCircleRight, 112 | contentDescription = stringResource(R.string.continue_button), 113 | ) 114 | }, 115 | text = { Text(text = stringResource(R.string.continue_button)) }, 116 | ) 117 | } 118 | ) 119 | } 120 | 121 | -------------------------------------------------------------------------------- /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 | data class SetSkipLandscape(val skipLandscape: Boolean): SettingsEvent() 38 | data class SetSkipNonInteractive(val skipNonInteractive: Boolean): SettingsEvent() 39 | } -------------------------------------------------------------------------------- /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 | val skipLandscape: Boolean = false, 45 | val skipNonInteractive: Boolean = false 46 | ) 47 | 48 | data class EffectSettings( 49 | val darken: Boolean = false, 50 | val homeDarkenPercentage: Int = 100, 51 | val lockDarkenPercentage: Int = 100, 52 | val blur: Boolean = false, 53 | val homeBlurPercentage: Int = 0, 54 | val lockBlurPercentage: Int = 0, 55 | val vignette: Boolean = false, 56 | val homeVignettePercentage: Int = 0, 57 | val lockVignettePercentage: Int = 0, 58 | val grayscale: Boolean = false, 59 | val homeGrayscalePercentage: Int = 0, 60 | val lockGrayscalePercentage: Int = 0 61 | ) 62 | 63 | data class ServiceSettings( 64 | val enableChanger: Boolean, 65 | val setHome: Boolean, 66 | val setLock: Boolean, 67 | val scaling: ScalingConstants, 68 | val darken: Boolean, 69 | val homeDarkenPercentage: Int, 70 | val lockDarkenPercentage: Int, 71 | val blur: Boolean, 72 | val homeBlurPercentage: Int, 73 | val lockBlurPercentage: Int, 74 | val vignette: Boolean, 75 | val homeVignettePercentage: Int, 76 | val lockVignettePercentage: Int, 77 | val grayscale: Boolean, 78 | val homeGrayscalePercentage: Int, 79 | val lockGrayscalePercentage: Int, 80 | val lockAlbumName: String, 81 | val homeAlbumName: String, 82 | val shuffle: Boolean, 83 | val skipLandscape: Boolean, 84 | val skipNonInteractive: Boolean 85 | ) 86 | } -------------------------------------------------------------------------------- /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.annotation.SuppressLint 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.platform.LocalWindowInfo 23 | import androidx.compose.ui.unit.IntSize 24 | import androidx.compose.ui.unit.dp 25 | import androidx.compose.ui.unit.times 26 | import androidx.core.net.toUri 27 | import com.anthonyla.paperize.core.ScalingConstants 28 | import com.anthonyla.paperize.core.VignetteBitmapTransformation 29 | import com.anthonyla.paperize.core.decompress 30 | import com.anthonyla.paperize.core.isValidUri 31 | import com.bumptech.glide.request.RequestOptions 32 | import com.skydoves.landscapist.ImageOptions 33 | import com.skydoves.landscapist.glide.GlideImage 34 | 35 | /** 36 | * A composable that displays a wallpaper for preview 37 | */ 38 | @SuppressLint("ConfigurationScreenWidthHeight") 39 | @Composable 40 | fun PreviewItem( 41 | wallpaperUri: String, 42 | darken: Boolean, 43 | darkenPercentage: Int, 44 | blur: Boolean, 45 | blurPercentage: Int, 46 | scaling: ScalingConstants, 47 | vignette: Boolean, 48 | vignettePercentage: Int, 49 | grayscale: Boolean, 50 | grayscalePercentage: Int 51 | ) { 52 | val context = LocalContext.current 53 | val showUri by remember { mutableStateOf(isValidUri(context, wallpaperUri)) } 54 | val configuration = LocalConfiguration.current 55 | val screenWidth = configuration.screenWidthDp.dp 56 | val screenHeight = configuration.screenHeightDp.dp 57 | if (showUri) { 58 | val uri = wallpaperUri.decompress("content://com.android.externalstorage.documents/").toUri() 59 | GlideImage( 60 | imageModel = { uri }, 61 | imageOptions = ImageOptions( 62 | contentScale = when (scaling) { 63 | ScalingConstants.FILL -> ContentScale.FillHeight 64 | ScalingConstants.FIT -> ContentScale.FillWidth 65 | ScalingConstants.STRETCH -> ContentScale.FillBounds 66 | ScalingConstants.NONE -> ContentScale.Crop 67 | }, 68 | requestSize = IntSize(300, 300), 69 | alignment = if (scaling == ScalingConstants.NONE) Alignment.CenterStart else Alignment.Center, 70 | colorFilter = if (darken && darkenPercentage < 100) { 71 | ColorFilter.tint( 72 | Color.Black.copy(alpha = (100 - darkenPercentage).toFloat().div(100f)), 73 | BlendMode.Darken 74 | ) 75 | } else { null }, 76 | tag = vignette.toString() + vignettePercentage.toString() + grayscale.toString() + grayscalePercentage.toString(), 77 | ), 78 | requestOptions = { 79 | when { 80 | grayscale && grayscalePercentage > 0 && vignette && vignettePercentage > 0 -> { 81 | RequestOptions().transform( 82 | VignetteBitmapTransformation(vignettePercentage), 83 | GrayscaleBitmapTransformation(grayscalePercentage) 84 | ) 85 | } 86 | grayscale && grayscalePercentage > 0 -> { 87 | RequestOptions().transform(GrayscaleBitmapTransformation(grayscalePercentage)) 88 | } 89 | vignette && vignettePercentage > 0 -> { 90 | RequestOptions().transform(VignetteBitmapTransformation(vignettePercentage)) 91 | } 92 | else -> RequestOptions() 93 | } 94 | }, 95 | modifier = Modifier 96 | .size(screenWidth * 0.35f, screenHeight * 0.35f) 97 | .clip(RoundedCornerShape(16.dp)) 98 | .border(3.dp, Color.Black, RoundedCornerShape(16.dp)) 99 | .background(Color.Black) 100 | .blur( 101 | if (blur && blurPercentage > 0) { 102 | blurPercentage 103 | .toFloat() 104 | .div(100f) * 1.5.dp 105 | } else { 106 | 0.dp 107 | } 108 | ) 109 | ) 110 | } 111 | } -------------------------------------------------------------------------------- /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.MaterialTheme 11 | import androidx.compose.material3.Surface 12 | import androidx.compose.material3.Switch 13 | import androidx.compose.material3.Text 14 | import androidx.compose.runtime.Composable 15 | import androidx.compose.ui.Alignment 16 | import androidx.compose.ui.Modifier 17 | import androidx.compose.ui.res.stringResource 18 | import androidx.compose.ui.text.font.FontWeight 19 | import androidx.compose.ui.unit.dp 20 | import com.anthonyla.paperize.R 21 | 22 | /** 23 | * Composable that toggles whether to allow album refresh (24 hrs) 24 | */ 25 | @Composable 26 | fun RefreshSwitch( 27 | refresh: Boolean, 28 | onRefreshChange: (Boolean) -> Unit, 29 | ) { 30 | Surface( 31 | tonalElevation = 10.dp, 32 | shape = RoundedCornerShape(16.dp), 33 | modifier = Modifier 34 | .fillMaxWidth() 35 | .padding(PaddingValues(horizontal = 16.dp, vertical = 8.dp)) 36 | ) { 37 | Row( 38 | modifier = Modifier 39 | .fillMaxWidth() 40 | .padding(16.dp), 41 | horizontalArrangement = Arrangement.SpaceBetween, 42 | verticalAlignment = Alignment.CenterVertically 43 | ) { 44 | Column( 45 | verticalArrangement = Arrangement.spacedBy(4.dp) 46 | ) { 47 | Text( 48 | text = stringResource(R.string.periodic_refresh), 49 | style = MaterialTheme.typography.titleMedium, 50 | fontWeight = FontWeight.W500 51 | ) 52 | Text( 53 | text = stringResource(R.string.check_folders_for_new_wallpapers), 54 | style = MaterialTheme.typography.bodyMedium, 55 | color = MaterialTheme.colorScheme.onSurfaceVariant 56 | ) 57 | } 58 | Switch( 59 | checked = refresh, 60 | onCheckedChange = onRefreshChange 61 | ) 62 | } 63 | } 64 | } -------------------------------------------------------------------------------- /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.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.MaterialTheme 11 | import androidx.compose.material3.Surface 12 | import androidx.compose.material3.Switch 13 | import androidx.compose.material3.Text 14 | import androidx.compose.runtime.Composable 15 | import androidx.compose.ui.Alignment 16 | import androidx.compose.ui.Modifier 17 | import androidx.compose.ui.res.stringResource 18 | import androidx.compose.ui.text.font.FontWeight 19 | import androidx.compose.ui.unit.dp 20 | import com.anthonyla.paperize.R 21 | 22 | @Composable 23 | fun ShuffleSwitch( 24 | shuffle: Boolean, 25 | onShuffleCheck: (Boolean) -> Unit 26 | ) { 27 | Surface( 28 | tonalElevation = 10.dp, 29 | shape = RoundedCornerShape(16.dp), 30 | modifier = Modifier 31 | .fillMaxWidth() 32 | .padding(PaddingValues(horizontal = 16.dp, vertical = 8.dp)) 33 | ) { 34 | Row( 35 | modifier = Modifier 36 | .fillMaxWidth() 37 | .padding(16.dp), 38 | horizontalArrangement = Arrangement.SpaceBetween, 39 | verticalAlignment = Alignment.CenterVertically 40 | ) { 41 | Column( 42 | verticalArrangement = Arrangement.spacedBy(4.dp) 43 | ) { 44 | Text( 45 | text = stringResource(R.string.shuffle), 46 | style = MaterialTheme.typography.titleMedium, 47 | fontWeight = FontWeight.W500 48 | ) 49 | Text( 50 | text = stringResource(R.string.shuffle_the_wallpapers), 51 | style = MaterialTheme.typography.bodyMedium, 52 | color = MaterialTheme.colorScheme.onSurfaceVariant 53 | ) 54 | } 55 | Switch( 56 | checked = shuffle, 57 | onCheckedChange = onShuffleCheck 58 | ) 59 | } 60 | } 61 | } -------------------------------------------------------------------------------- /app/src/main/java/com/anthonyla/paperize/feature/wallpaper/presentation/wallpaper_screen/components/SkipLandscapeSwitch.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.MaterialTheme 11 | import androidx.compose.material3.Surface 12 | import androidx.compose.material3.Switch 13 | import androidx.compose.material3.Text 14 | import androidx.compose.runtime.Composable 15 | import androidx.compose.ui.Alignment 16 | import androidx.compose.ui.Modifier 17 | import androidx.compose.ui.res.stringResource 18 | import androidx.compose.ui.text.font.FontWeight 19 | import androidx.compose.ui.unit.dp 20 | import com.anthonyla.paperize.R 21 | 22 | @Composable 23 | fun SkipLandscapeSwitch( 24 | skipLandscape: Boolean, 25 | onSkipLandscapeChange: (Boolean) -> Unit 26 | ) { 27 | Surface( 28 | tonalElevation = 10.dp, 29 | shape = RoundedCornerShape(16.dp), 30 | modifier = Modifier 31 | .fillMaxWidth() 32 | .padding(PaddingValues(horizontal = 16.dp, vertical = 8.dp)) 33 | ) { 34 | Row( 35 | modifier = Modifier 36 | .fillMaxWidth() 37 | .padding(16.dp), 38 | horizontalArrangement = Arrangement.SpaceBetween, 39 | verticalAlignment = Alignment.CenterVertically 40 | ) { 41 | Column( 42 | verticalArrangement = Arrangement.spacedBy(4.dp) 43 | ) { 44 | Text( 45 | text = stringResource(R.string.skip_landscape_mode), 46 | style = MaterialTheme.typography.titleMedium, 47 | fontWeight = FontWeight.W500 48 | ) 49 | Text( 50 | text = stringResource(R.string.prevent_changing_during_landscape_mode), 51 | style = MaterialTheme.typography.bodyMedium, 52 | color = MaterialTheme.colorScheme.onSurfaceVariant 53 | ) 54 | } 55 | Switch( 56 | checked = skipLandscape, 57 | onCheckedChange = onSkipLandscapeChange 58 | ) 59 | } 60 | } 61 | } -------------------------------------------------------------------------------- /app/src/main/java/com/anthonyla/paperize/feature/wallpaper/presentation/wallpaper_screen/components/SkipNonInteractiveSwitch.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.MaterialTheme 11 | import androidx.compose.material3.Surface 12 | import androidx.compose.material3.Switch 13 | import androidx.compose.material3.Text 14 | import androidx.compose.runtime.Composable 15 | import androidx.compose.ui.Alignment 16 | import androidx.compose.ui.Modifier 17 | import androidx.compose.ui.res.stringResource 18 | import androidx.compose.ui.text.font.FontWeight 19 | import androidx.compose.ui.unit.dp 20 | import com.anthonyla.paperize.R 21 | 22 | @Composable 23 | fun SkipNonInteractiveSwitch( 24 | skipNonInteractive: Boolean, 25 | onSkipNonInteractiveChange: (Boolean) -> Unit 26 | ) { 27 | Surface( 28 | tonalElevation = 10.dp, 29 | shape = RoundedCornerShape(16.dp), 30 | modifier = Modifier 31 | .fillMaxWidth() 32 | .padding(PaddingValues(horizontal = 16.dp, vertical = 8.dp)) 33 | ) { 34 | Row( 35 | modifier = Modifier 36 | .fillMaxWidth() 37 | .padding(16.dp), 38 | horizontalArrangement = Arrangement.SpaceBetween, 39 | verticalAlignment = Alignment.CenterVertically 40 | ) { 41 | Column( 42 | verticalArrangement = Arrangement.spacedBy(4.dp) 43 | ) { 44 | Text( 45 | text = stringResource(R.string.skip_non_interactive_state), 46 | style = MaterialTheme.typography.titleMedium, 47 | fontWeight = FontWeight.W500 48 | ) 49 | Text( 50 | text = stringResource(R.string.prevent_changing_during_non_interactive_state), 51 | style = MaterialTheme.typography.bodyMedium, 52 | color = MaterialTheme.colorScheme.onSurfaceVariant 53 | ) 54 | } 55 | Switch( 56 | checked = skipNonInteractive, 57 | onCheckedChange = onSkipNonInteractiveChange 58 | ) 59 | } 60 | } 61 | } -------------------------------------------------------------------------------- /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_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/66c3a4bb2e5cecd83b9d6539ad034d02905668c6/app/src/main/res/app_icon-playstore.png -------------------------------------------------------------------------------- /app/src/main/res/drawable-hdpi/notification_icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Anthonyy232/Paperize/66c3a4bb2e5cecd83b9d6539ad034d02905668c6/app/src/main/res/drawable-hdpi/notification_icon.png -------------------------------------------------------------------------------- /app/src/main/res/drawable-mdpi/notification_icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Anthonyy232/Paperize/66c3a4bb2e5cecd83b9d6539ad034d02905668c6/app/src/main/res/drawable-mdpi/notification_icon.png -------------------------------------------------------------------------------- /app/src/main/res/drawable-xhdpi/notification_icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Anthonyy232/Paperize/66c3a4bb2e5cecd83b9d6539ad034d02905668c6/app/src/main/res/drawable-xhdpi/notification_icon.png -------------------------------------------------------------------------------- /app/src/main/res/drawable-xxhdpi/notification_icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Anthonyy232/Paperize/66c3a4bb2e5cecd83b9d6539ad034d02905668c6/app/src/main/res/drawable-xxhdpi/notification_icon.png -------------------------------------------------------------------------------- /app/src/main/res/drawable-xxxhdpi/notification_icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Anthonyy232/Paperize/66c3a4bb2e5cecd83b9d6539ad034d02905668c6/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/66c3a4bb2e5cecd83b9d6539ad034d02905668c6/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/66c3a4bb2e5cecd83b9d6539ad034d02905668c6/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/66c3a4bb2e5cecd83b9d6539ad034d02905668c6/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/66c3a4bb2e5cecd83b9d6539ad034d02905668c6/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/66c3a4bb2e5cecd83b9d6539ad034d02905668c6/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/66c3a4bb2e5cecd83b9d6539ad034d02905668c6/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/66c3a4bb2e5cecd83b9d6539ad034d02905668c6/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/66c3a4bb2e5cecd83b9d6539ad034d02905668c6/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/66c3a4bb2e5cecd83b9d6539ad034d02905668c6/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/66c3a4bb2e5cecd83b9d6539ad034d02905668c6/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.11.1" 3 | kotlin = "2.2.20" 4 | hilt = "2.57.1" 5 | ksp = "2.2.20-2.0.3" 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.11.0" } 10 | androidx-animation = { module = "androidx.compose.animation:animation", version = "1.9.1" } 11 | androidx-compose-bom = { module = "androidx.compose:compose-bom", version = "2025.09.00" } 12 | androidx-core-ktx = { module = "androidx.core:core-ktx", version = "1.17.0" } 13 | androidx-core-splashscreen = { module = "androidx.core:core-splashscreen", version = "1.2.0-rc01" } 14 | androidx-datastore-preferences = { module = "androidx.datastore:datastore-preferences", version = "1.1.7" } 15 | androidx-datastore = { module = "androidx.datastore:datastore", version = "1.1.7" } 16 | androidx-documentfile = { module = "androidx.documentfile:documentfile", version = "1.1.0" } 17 | androidx-espresso-core = { module = "androidx.test.espresso:espresso-core", version = "3.7.0" } 18 | androidx-foundation = { module = "androidx.compose.foundation:foundation", version = "1.9.1" } 19 | androidx-hilt-navigation-compose = { module = "androidx.hilt:hilt-navigation-compose", version = "1.3.0" } 20 | androidx-hilt-work = { module = "androidx.hilt:hilt-work", version = "1.3.0" } 21 | androidx-junit = { module = "androidx.test.ext:junit", version = "1.3.0" } 22 | androidx-lifecycle-runtime-compose = { module = "androidx.lifecycle:lifecycle-runtime-compose", version = "2.9.3" } 23 | androidx-lifecycle-runtime-ktx = { module = "androidx.lifecycle:lifecycle-runtime-ktx", version = "2.9.3" } 24 | androidx-material = { module = "androidx.compose.material:material", version = "1.9.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-rc01" } 27 | androidx-navigation-compose = { module = "androidx.navigation:navigation-compose", version = "2.9.4" } 28 | androidx-profileinstaller = { module = "androidx.profileinstaller:profileinstaller", version = "1.4.1" } 29 | androidx-room-ktx = { module = "androidx.room:room-ktx", version = "2.8.0" } 30 | androidx-room-compiler = { module = "androidx.room:room-compiler", version = "2.8.0" } 31 | androidx-room-runtime = { module = "androidx.room:room-runtime", version = "2.8.0" } 32 | androidx-ui = { module = "androidx.compose.ui:ui", version = "1.9.1" } 33 | androidx-ui-graphics = { module = "androidx.compose.ui:ui-graphics", version = "1.9.1" } 34 | androidx-ui-test-junit4 = { module = "androidx.compose.ui:ui-test-junit4", version = "1.9.1" } 35 | androidx-ui-test-manifest = { module = "androidx.compose.ui:ui-test-manifest", version = "1.9.1" } 36 | androidx-ui-tooling = { module = "androidx.compose.ui:ui-tooling", version = "1.9.1" } 37 | androidx-ui-tooling-preview = { module = "androidx.compose.ui:ui-tooling-preview", version = "1.9.1" } 38 | androidx-work-runtime-ktx = { module = "androidx.work:work-runtime-ktx", version = "2.10.4" } 39 | dfc = { module = "com.lazygeniouz:dfc", version = "1.2" } 40 | gson = { module = "com.google.code.gson:gson", version = "2.13.2" } 41 | hilt-android = { group = "com.google.dagger", name = "hilt-android", version.ref = "hilt" } 42 | hilt-android-compiler = { group = "com.google.dagger", name = "hilt-android-compiler", version.ref = "hilt" } 43 | junit = { module = "junit:junit", version = "4.13.2" } 44 | kotlinx-serialization-json = { module = "org.jetbrains.kotlinx:kotlinx-serialization-json", version = "1.9.0" } 45 | landscapist-glide = { module = "com.github.skydoves:landscapist-glide", version = "2.5.2" } 46 | lazycolumnscrollbar = { module = "com.github.nanihadesuka:LazyColumnScrollbar", version = "2.2.0" } 47 | lottie-compose = { module = "com.airbnb.android:lottie-compose", version = "6.6.7" } 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.8.1" } 51 | reorderable = { group = "sh.calvin.reorderable", name = "reorderable", version = "3.0.0" } 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/66c3a4bb2e5cecd83b9d6539ad034d02905668c6/gradle/wrapper/gradle-wrapper.jar -------------------------------------------------------------------------------- /gradle/wrapper/gradle-wrapper.properties: -------------------------------------------------------------------------------- 1 | distributionBase=GRADLE_USER_HOME 2 | distributionPath=wrapper/dists 3 | distributionSha256Sum=8fad3d78296ca518113f3d29016617c7f9367dc005f932bd9d93bf45ba46072b 4 | distributionUrl=https\://services.gradle.org/distributions/gradle-9.0.0-bin.zip 5 | networkTimeout=10000 6 | validateDistributionUrl=true 7 | zipStoreBase=GRADLE_USER_HOME 8 | zipStorePath=wrapper/dists 9 | -------------------------------------------------------------------------------- /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/66c3a4bb2e5cecd83b9d6539ad034d02905668c6/metadata/en-US/images/featureGraphic.png -------------------------------------------------------------------------------- /metadata/en-US/images/icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Anthonyy232/Paperize/66c3a4bb2e5cecd83b9d6539ad034d02905668c6/metadata/en-US/images/icon.png -------------------------------------------------------------------------------- /metadata/en-US/images/phoneScreenshots/1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Anthonyy232/Paperize/66c3a4bb2e5cecd83b9d6539ad034d02905668c6/metadata/en-US/images/phoneScreenshots/1.png -------------------------------------------------------------------------------- /metadata/en-US/images/phoneScreenshots/2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Anthonyy232/Paperize/66c3a4bb2e5cecd83b9d6539ad034d02905668c6/metadata/en-US/images/phoneScreenshots/2.png -------------------------------------------------------------------------------- /metadata/en-US/images/phoneScreenshots/3.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Anthonyy232/Paperize/66c3a4bb2e5cecd83b9d6539ad034d02905668c6/metadata/en-US/images/phoneScreenshots/3.png -------------------------------------------------------------------------------- /metadata/en-US/images/phoneScreenshots/4.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Anthonyy232/Paperize/66c3a4bb2e5cecd83b9d6539ad034d02905668c6/metadata/en-US/images/phoneScreenshots/4.png -------------------------------------------------------------------------------- /metadata/en-US/images/phoneScreenshots/5.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Anthonyy232/Paperize/66c3a4bb2e5cecd83b9d6539ad034d02905668c6/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 | --------------------------------------------------------------------------------