├── .github └── workflows │ ├── build-apk.yml │ └── unit-test.yml ├── .gitignore ├── .idea ├── .gitignore ├── compiler.xml ├── gradle.xml ├── inspectionProfiles │ └── Project_Default.xml ├── misc.xml └── vcs.xml ├── LICENSE ├── README.md ├── app ├── .gitignore ├── build.gradle.kts ├── proguard-rules.pro ├── schemas │ └── com.github.pakka_papad.data.AppDatabase │ │ ├── 1.json │ │ ├── 2.json │ │ └── 3.json └── src │ ├── debug │ └── res │ │ └── values │ │ └── strings.xml │ ├── main │ ├── AndroidManifest.xml │ ├── assets │ │ └── changelogs.json │ ├── ic_launcher-playstore.png │ ├── java │ │ └── com │ │ │ └── github │ │ │ └── pakka_papad │ │ │ ├── Constants.kt │ │ │ ├── CrashActivity.kt │ │ │ ├── MainActivity.kt │ │ │ ├── Screens.kt │ │ │ ├── Util.kt │ │ │ ├── ZenApp.kt │ │ │ ├── collection │ │ │ ├── CollectionActions.kt │ │ │ ├── CollectionContent.kt │ │ │ ├── CollectionFragment.kt │ │ │ ├── CollectionTopBar.kt │ │ │ ├── CollectionType.kt │ │ │ ├── CollectionUi.kt │ │ │ └── CollectionViewModel.kt │ │ │ ├── components │ │ │ ├── BlockingProgressIndicator.kt │ │ │ ├── BottomSheet.kt │ │ │ ├── Chips.kt │ │ │ ├── FullScreenSadMessage.kt │ │ │ ├── OutlinedBox.kt │ │ │ ├── PlaylistCards.kt │ │ │ ├── SelectableCards.kt │ │ │ ├── Snackbar.kt │ │ │ ├── SongCards.kt │ │ │ ├── SortOptions.kt │ │ │ ├── TopAppBars.kt │ │ │ └── more_options │ │ │ │ ├── FolderOptions.kt │ │ │ │ ├── GenreOptions.kt │ │ │ │ ├── MoreOptions.kt │ │ │ │ ├── OptionsAlertDialog.kt │ │ │ │ ├── OptionsDropDown.kt │ │ │ │ ├── PersonOptions.kt │ │ │ │ ├── PlaylistOptions.kt │ │ │ │ └── SongOptions.kt │ │ │ ├── data │ │ │ ├── AppDatabase.kt │ │ │ ├── CrashReporter.kt │ │ │ ├── QueueStateProvider.kt │ │ │ ├── QueueStateSerializer.kt │ │ │ ├── UserPreferencesSerializer.kt │ │ │ ├── ZenPreferenceProvider.kt │ │ │ ├── analytics │ │ │ │ ├── PlayHistory.kt │ │ │ │ └── PlayHistoryDao.kt │ │ │ ├── daos │ │ │ │ ├── AlbumArtistDao.kt │ │ │ │ ├── AlbumDao.kt │ │ │ │ ├── ArtistDao.kt │ │ │ │ ├── BlacklistDao.kt │ │ │ │ ├── BlacklistedFolderDao.kt │ │ │ │ ├── ComposerDao.kt │ │ │ │ ├── GenreDao.kt │ │ │ │ ├── LyricistDao.kt │ │ │ │ ├── PlaylistDao.kt │ │ │ │ └── SongDao.kt │ │ │ ├── music │ │ │ │ ├── Album.kt │ │ │ │ ├── AlbumArtist.kt │ │ │ │ ├── AlbumArtistWithSongs.kt │ │ │ │ ├── AlbumWithSongs.kt │ │ │ │ ├── Artist.kt │ │ │ │ ├── ArtistWithSongs.kt │ │ │ │ ├── BlacklistedFolder.kt │ │ │ │ ├── BlacklistedSong.kt │ │ │ │ ├── Composer.kt │ │ │ │ ├── ComposerWithSongs.kt │ │ │ │ ├── Genre.kt │ │ │ │ ├── GenreWithSongCount.kt │ │ │ │ ├── GenreWithSongs.kt │ │ │ │ ├── Lyricist.kt │ │ │ │ ├── LyricistWithSongs.kt │ │ │ │ ├── MetadataExtractor.kt │ │ │ │ ├── MiniSong.kt │ │ │ │ ├── PersonWithSongCount.kt │ │ │ │ ├── Playlist.kt │ │ │ │ ├── PlaylistSongCrossRef.kt │ │ │ │ ├── PlaylistWithSongCount.kt │ │ │ │ ├── PlaylistWithSongs.kt │ │ │ │ ├── ScanStatus.kt │ │ │ │ ├── Song.kt │ │ │ │ └── SongExtractor.kt │ │ │ ├── services │ │ │ │ ├── AnalyticsService.kt │ │ │ │ ├── BlacklistService.kt │ │ │ │ ├── PlayerService.kt │ │ │ │ ├── PlaylistService.kt │ │ │ │ ├── QueueService.kt │ │ │ │ ├── SearchService.kt │ │ │ │ ├── SleepTimerService.kt │ │ │ │ ├── SongService.kt │ │ │ │ └── ThumbnailService.kt │ │ │ └── thumbnails │ │ │ │ ├── Thumbnail.kt │ │ │ │ └── ThumbnailDao.kt │ │ │ ├── di │ │ │ └── AppModule.kt │ │ │ ├── home │ │ │ ├── Albums.kt │ │ │ ├── AllSongs.kt │ │ │ ├── Files.kt │ │ │ ├── Genres.kt │ │ │ ├── HomeBottomBar.kt │ │ │ ├── HomeFragment.kt │ │ │ ├── HomeNavHelper.kt │ │ │ ├── HomeTopBar.kt │ │ │ ├── HomeViewModel.kt │ │ │ ├── MiniPlayer.kt │ │ │ ├── Person.kt │ │ │ ├── Persons.kt │ │ │ ├── PlayShuffleCard.kt │ │ │ └── Playlists.kt │ │ │ ├── nowplaying │ │ │ ├── LazyListDragDrop.kt │ │ │ ├── MusicSlider.kt │ │ │ ├── NowPlayingOptions.kt │ │ │ ├── NowPlayingScreen.kt │ │ │ ├── NowPlayingTopBar.kt │ │ │ ├── PlayerHelper.kt │ │ │ ├── Queue.kt │ │ │ └── RepeatMode.kt │ │ │ ├── onboarding │ │ │ ├── Content.kt │ │ │ ├── GenericPermissionPage.kt │ │ │ ├── OnBoardingFragment.kt │ │ │ ├── OnBoardingViewModel.kt │ │ │ ├── Page.kt │ │ │ ├── PermissionPages.kt │ │ │ └── ScanningPage.kt │ │ │ ├── player │ │ │ ├── NotificationProvider.kt │ │ │ ├── SessionCallback.kt │ │ │ ├── ZenBroadcastReceiver.kt │ │ │ ├── ZenCommandButtons.kt │ │ │ ├── ZenCommands.kt │ │ │ └── ZenPlayer.kt │ │ │ ├── restore │ │ │ ├── RestoreContent.kt │ │ │ ├── RestoreFragment.kt │ │ │ └── RestoreViewModel.kt │ │ │ ├── restore_folder │ │ │ ├── RestoreFolderContent.kt │ │ │ ├── RestoreFolderFragment.kt │ │ │ └── RestoreFolderViewModel.kt │ │ │ ├── search │ │ │ ├── ResultContent.kt │ │ │ ├── SearchBar.kt │ │ │ ├── SearchFragment.kt │ │ │ ├── SearchResult.kt │ │ │ ├── SearchType.kt │ │ │ └── SearchViewModel.kt │ │ │ ├── select_playlist │ │ │ ├── SelectPlaylistContent.kt │ │ │ ├── SelectPlaylistFragment.kt │ │ │ └── SelectPlaylistViewModel.kt │ │ │ ├── settings │ │ │ ├── SettingsFragment.kt │ │ │ ├── SettingsList.kt │ │ │ └── SettingsViewModel.kt │ │ │ ├── splash │ │ │ └── SplashFragment.kt │ │ │ ├── storage_explorer │ │ │ ├── Directory.kt │ │ │ ├── DirectoryContents.kt │ │ │ └── FileExplorer.kt │ │ │ ├── ui │ │ │ ├── accent_colours │ │ │ │ ├── DefaultTheme.kt │ │ │ │ ├── ElmTheme.kt │ │ │ │ ├── JacksonsPurpleTheme.kt │ │ │ │ ├── MagentaTheme.kt │ │ │ │ ├── MalibuTheme.kt │ │ │ │ └── MelroseTheme.kt │ │ │ └── theme │ │ │ │ ├── Colors.kt │ │ │ │ ├── Shape.kt │ │ │ │ ├── Theme.kt │ │ │ │ ├── ThemePreference.kt │ │ │ │ └── Type.kt │ │ │ ├── util │ │ │ ├── MessageStore.kt │ │ │ └── Resource.kt │ │ │ ├── whatsnew │ │ │ ├── Changelog.kt │ │ │ ├── ChangelogUi.kt │ │ │ ├── WhatsNewFragment.kt │ │ │ └── WhatsNewViewModel.kt │ │ │ ├── widgets │ │ │ ├── MusicControlWidget.kt │ │ │ └── WidgetBroadcast.kt │ │ │ └── workers │ │ │ └── ThumbnailWorker.kt │ ├── proto │ │ └── com │ │ │ └── github │ │ │ └── pakka_papad │ │ │ └── data │ │ │ ├── QueueState.proto │ │ │ └── UserPreferences.proto │ └── res │ │ ├── anim │ │ ├── decelerate_interpolator.xml │ │ ├── enter_from_bottom.xml │ │ ├── exit_to_bottom.xml │ │ ├── fade_in_enter.xml │ │ ├── fade_out_exit.xml │ │ ├── grow_from_top.xml │ │ ├── no_change.xml │ │ ├── shrink_towards_top.xml │ │ ├── slide_left_enter.xml │ │ ├── slide_left_exit.xml │ │ ├── zen_close_enter.xml │ │ ├── zen_close_exit.xml │ │ ├── zen_open_enter.xml │ │ └── zen_open_exit.xml │ │ ├── animator │ │ └── logo_animator.xml │ │ ├── drawable │ │ ├── animated_logo.xml │ │ ├── baseline_arrow_forward_40.xml │ │ ├── baseline_bug_report_40.xml │ │ ├── baseline_colorize_40.xml │ │ ├── baseline_dark_mode_40.xml │ │ ├── baseline_drag_indicator_40.xml │ │ ├── baseline_light_mode_40.xml │ │ ├── baseline_palette_40.xml │ │ ├── baseline_repeat_40.xml │ │ ├── baseline_repeat_one_40.xml │ │ ├── baseline_send_40.xml │ │ ├── baseline_settings_backup_restore_40.xml │ │ ├── baseline_speed_24.xml │ │ ├── error.png │ │ ├── github_mark.xml │ │ ├── ic_baseline_album_40.xml │ │ ├── ic_baseline_close_30.xml │ │ ├── ic_baseline_close_40.xml │ │ ├── ic_baseline_favorite_24.xml │ │ ├── ic_baseline_favorite_border_24.xml │ │ ├── ic_baseline_folder_40.xml │ │ ├── ic_baseline_info_40.xml │ │ ├── ic_baseline_library_music_40.xml │ │ ├── ic_baseline_list_24.xml │ │ ├── ic_baseline_music_note_40.xml │ │ ├── ic_baseline_pause_40.xml │ │ ├── ic_baseline_person_40.xml │ │ ├── ic_baseline_piano_40.xml │ │ ├── ic_baseline_play_arrow_40.xml │ │ ├── ic_baseline_playlist_add_40.xml │ │ ├── ic_baseline_playlist_play_40.xml │ │ ├── ic_baseline_playlist_remove_40.xml │ │ ├── ic_baseline_queue_40.xml │ │ ├── ic_baseline_queue_music_40.xml │ │ ├── ic_baseline_remove_circle_40.xml │ │ ├── ic_baseline_sentiment_very_dissatisfied_40.xml │ │ ├── ic_baseline_shuffle_40.xml │ │ ├── ic_baseline_skip_next_40.xml │ │ ├── ic_baseline_skip_previous_40.xml │ │ ├── ic_baseline_sort_40.xml │ │ ├── ic_launcher_foreground.xml │ │ ├── ic_launcher_monochrome.xml │ │ ├── ic_outline_album_40.xml │ │ ├── ic_outline_folder_40.xml │ │ ├── ic_outline_library_music_40.xml │ │ ├── ic_outline_music_note_40.xml │ │ ├── ic_outline_person_40.xml │ │ ├── linkedin.png │ │ ├── music_widget_background.xml │ │ ├── outline_timer_24.xml │ │ ├── progress.xml │ │ └── seekbar_thumb.xml │ │ ├── ic_launcher-playstore.png │ │ ├── layout │ │ ├── activity_main.xml │ │ └── music_control_widget_preview_layout.xml │ │ ├── mipmap-anydpi-v26 │ │ ├── ic_launcher.xml │ │ └── ic_launcher_round.xml │ │ ├── mipmap-hdpi │ │ ├── ic_launcher.webp │ │ ├── ic_launcher_round.webp │ │ └── ic_notification.png │ │ ├── mipmap-mdpi │ │ ├── ic_launcher.webp │ │ ├── ic_launcher_round.webp │ │ └── ic_notification.png │ │ ├── mipmap-xhdpi │ │ ├── ic_launcher.webp │ │ ├── ic_launcher_round.webp │ │ └── ic_notification.png │ │ ├── mipmap-xxhdpi │ │ ├── ic_launcher.webp │ │ ├── ic_launcher_round.webp │ │ └── ic_notification.png │ │ ├── mipmap-xxxhdpi │ │ ├── ic_launcher.webp │ │ ├── ic_launcher_round.webp │ │ └── ic_notification.png │ │ ├── navigation │ │ └── app_nav.xml │ │ ├── raw │ │ ├── bug.json │ │ ├── database_search.json │ │ ├── notification_permission.json │ │ └── storage_permission.json │ │ ├── values-tr │ │ └── strings.xml │ │ ├── values-v31 │ │ └── splash_theme.xml │ │ ├── values │ │ ├── colors.xml │ │ ├── durations.xml │ │ ├── ic_launcher_background.xml │ │ ├── splash_theme.xml │ │ ├── strings.xml │ │ └── themes.xml │ │ └── xml │ │ ├── backup_rules.xml │ │ ├── data_extraction_rules.xml │ │ └── music_control_widget_info.xml │ └── test │ └── java │ └── com │ └── github │ └── pakka_papad │ ├── MainDispatcherRule.kt │ ├── Util.kt │ ├── collection │ └── CollectionViewModelTest.kt │ ├── data │ └── services │ │ ├── BlacklistServiceImplTest.kt │ │ ├── PlaylistServiceImplTest.kt │ │ └── QueueServiceImplTest.kt │ ├── restore │ └── RestoreViewModelTest.kt │ ├── restore_folder │ └── RestoreFolderViewModelTest.kt │ ├── search │ └── SearchViewModelTest.kt │ └── select_playlist │ └── SelectPlaylistViewModelTest.kt ├── build.gradle.kts ├── buildSrc ├── build.gradle.kts └── src │ └── main │ └── kotlin │ └── Dependencies.kt ├── gradle.properties ├── gradle └── wrapper │ ├── gradle-wrapper.jar │ └── gradle-wrapper.properties ├── gradlew ├── gradlew.bat ├── m3utils ├── .gitignore ├── build.gradle.kts └── src │ └── main │ └── java │ ├── LICENSE │ ├── blend │ └── Blend.java │ ├── contrast │ └── Contrast.java │ ├── dislike │ └── DislikeAnalyzer.java │ ├── dynamiccolor │ ├── ContrastCurve.java │ ├── DynamicColor.java │ ├── MaterialDynamicColors.java │ ├── ToneDeltaPair.java │ └── TonePolarity.java │ ├── hct │ ├── Cam16.java │ ├── Hct.java │ ├── HctSolver.java │ └── ViewingConditions.java │ ├── palettes │ ├── CorePalette.java │ └── TonalPalette.java │ ├── quantize │ ├── PointProvider.java │ ├── PointProviderLab.java │ ├── Quantizer.java │ ├── QuantizerCelebi.java │ ├── QuantizerMap.java │ ├── QuantizerResult.java │ ├── QuantizerWsmeans.java │ └── QuantizerWu.java │ ├── scheme │ ├── DynamicScheme.java │ ├── Scheme.java │ ├── SchemeContent.java │ ├── SchemeExpressive.java │ ├── SchemeFidelity.java │ ├── SchemeFruitSalad.java │ ├── SchemeMonochrome.java │ ├── SchemeNeutral.java │ ├── SchemeRainbow.java │ ├── SchemeTonalSpot.java │ ├── SchemeVibrant.java │ └── Variant.java │ ├── score │ └── Score.java │ ├── temperature │ └── TemperatureCache.java │ └── utils │ ├── ColorUtils.java │ ├── MathUtils.java │ └── StringUtils.java ├── privacy-policy.md ├── screenshots ├── Dark │ ├── album_collection.jpg │ ├── albums.jpg │ ├── all_songs_default.jpg │ ├── all_songs_elm.jpg │ ├── all_songs_jacksonspurple.jpg │ ├── all_songs_magenta.jpg │ ├── all_songs_malibu.jpg │ ├── all_songs_melrose.jpg │ ├── artists.jpg │ ├── genres.jpg │ ├── now_playing_magenta.jpg │ ├── now_playing_malibu.jpg │ ├── now_playing_melrose.jpg │ ├── playlists.jpg │ ├── search.jpg │ ├── storage_access.jpg │ └── storage_scan.jpg ├── Light │ ├── album_collection.jpg │ ├── albums.jpg │ ├── all_songs_default.jpg │ ├── all_songs_elm.jpg │ ├── all_songs_jacksonspurple.jpg │ ├── all_songs_magenta.jpg │ ├── all_songs_malibu.jpg │ ├── all_songs_melrose.jpg │ ├── artists.jpg │ ├── genres.jpg │ ├── now_playing_magenta.jpg │ ├── now_playing_malibu.jpg │ ├── now_playing_melrose.jpg │ ├── playlists.jpg │ ├── search.jpg │ ├── storage_access.jpg │ └── storage_scan.jpg └── banner.png └── settings.gradle.kts /.github/workflows/build-apk.yml: -------------------------------------------------------------------------------- 1 | name: build-apk 2 | 3 | on: 4 | push: 5 | branches: [main] 6 | 7 | jobs: 8 | build: 9 | name: Build IR apk 10 | runs-on: ubuntu-latest 11 | 12 | steps: 13 | - name: Checkout the code 14 | uses: actions/checkout@v4 15 | 16 | - name: Setup JDK 17 | uses: actions/setup-java@v4 18 | with: 19 | distribution: 'temurin' 20 | java-version: 17 21 | cache: 'gradle' 22 | 23 | - name: Load Google Service file 24 | env: 25 | DATA: ${{ secrets.GOOGLE_SERVICES_JSON }} 26 | run: echo $DATA | base64 -di > app/google-services.json 27 | 28 | - name: Setup Local properties 29 | env: 30 | STORE_FILE: ${{ secrets.STORE_FILE }} 31 | IR_STORE_FILE: ${{ secrets.IR_STORE_FILE }} 32 | run: mkdir keys && 33 | echo $STORE_FILE | base64 -di > keys/store_key.jks && 34 | echo $IR_STORE_FILE | base64 -di > keys/ir_store_key.jks && 35 | echo $'STORE_FILE=${{ github.workspace }}/keys/store_key.jks\n 36 | STORE_PASSWORD=${{ secrets.STORE_PASSWORD }}\n 37 | KEY_ALIAS=${{ secrets.KEY_ALIAS }}\n 38 | KEY_PASSWORD=${{ secrets.KEY_PASSWORD }}\n 39 | IR_STORE_FILE=${{ github.workspace }}/keys/ir_store_key.jks\n 40 | IR_STORE_PASSWORD=${{ secrets.IR_STORE_PASSWORD }}\n 41 | IR_KEY_ALIAS=${{ secrets.IR_KEY_ALIAS }}\n 42 | IR_KEY_PASSWORD=${{ secrets.IR_KEY_PASSWORD }}\n' > ./local.properties 43 | 44 | - name: Grant execute permission for gradlew 45 | run: chmod +x gradlew 46 | 47 | - name: Setup Gradle 48 | uses: gradle/actions/setup-gradle@v4 49 | 50 | - name: Build apk 51 | run: ./gradlew assembleInternalRelease 52 | 53 | - name: Rename apk 54 | run: mv ./app/build/outputs/apk/internalRelease/app-internalRelease.apk ./app/build/outputs/apk/internalRelease/zen-music-ir-build-${{ github.run_number }}.apk 55 | 56 | - name: Upload apk 57 | uses: actions/upload-artifact@v4 58 | with: 59 | name: zen-music-ir-build-${{ github.run_number }}.apk 60 | path: app/build/outputs/apk/internalRelease/ 61 | -------------------------------------------------------------------------------- /.github/workflows/unit-test.yml: -------------------------------------------------------------------------------- 1 | name: unit-test 2 | 3 | on: 4 | push: 5 | branches: [main, dev] 6 | pull_request: 7 | branches: [main, dev] 8 | 9 | jobs: 10 | test: 11 | name: Perform Unit Testing 12 | runs-on: ubuntu-latest 13 | steps: 14 | - name: Checkout the code 15 | uses: actions/checkout@v4 16 | 17 | - name: Setup JDK 18 | uses: actions/setup-java@v4 19 | with: 20 | distribution: 'temurin' 21 | java-version: 17 22 | cache: 'gradle' 23 | 24 | - name: Load Google Service file 25 | env: 26 | DATA: ${{ secrets.GOOGLE_SERVICES_JSON }} 27 | run: echo $DATA | base64 -di > app/google-services.json 28 | 29 | - name: Setup Local properties 30 | env: 31 | STORE_FILE: ${{ secrets.STORE_FILE }} 32 | IR_STORE_FILE: ${{ secrets.IR_STORE_FILE }} 33 | run: mkdir keys && 34 | echo $STORE_FILE | base64 -di > keys/store_key.jks && 35 | echo $IR_STORE_FILE | base64 -di > keys/ir_store_key.jks && 36 | echo $'STORE_FILE=${{ github.workspace }}/keys/store_key.jks\n 37 | STORE_PASSWORD=${{ secrets.STORE_PASSWORD }}\n 38 | KEY_ALIAS=${{ secrets.KEY_ALIAS }}\n 39 | KEY_PASSWORD=${{ secrets.KEY_PASSWORD }}\n 40 | IR_STORE_FILE=${{ github.workspace }}/keys/ir_store_key.jks\n 41 | IR_STORE_PASSWORD=${{ secrets.IR_STORE_PASSWORD }}\n 42 | IR_KEY_ALIAS=${{ secrets.IR_KEY_ALIAS }}\n 43 | IR_KEY_PASSWORD=${{ secrets.IR_KEY_PASSWORD }}\n' > ./local.properties 44 | 45 | - name: Grant execute permission for gradlew 46 | run: chmod +x gradlew 47 | 48 | - name: Setup Gradle 49 | uses: gradle/actions/setup-gradle@v4 50 | 51 | - name: Run unit tests 52 | run: ./gradlew testDebug -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | *.iml 2 | .gradle 3 | /local.properties 4 | /.idea/caches 5 | /.idea/libraries 6 | /.idea/modules.xml 7 | /.idea/workspace.xml 8 | /.idea/navEditor.xml 9 | /.idea/assetWizardSettings.xml 10 | /.idea/deploymentTargetDropDown.xml 11 | /.idea/kotlinc.xml 12 | /.idea/misc.xml 13 | /.idea 14 | /buildSrc/build 15 | .DS_Store 16 | /build 17 | /captures 18 | .externalNativeBuild 19 | .cxx 20 | local.properties 21 | /buildSrc/build/ 22 | -------------------------------------------------------------------------------- /.idea/.gitignore: -------------------------------------------------------------------------------- 1 | # Default ignored files 2 | /shelf/ 3 | /workspace.xml 4 | -------------------------------------------------------------------------------- /.idea/compiler.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /.idea/gradle.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 32 | 33 | -------------------------------------------------------------------------------- /.idea/misc.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 10 | -------------------------------------------------------------------------------- /.idea/vcs.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2023 Sumit Bera 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /app/.gitignore: -------------------------------------------------------------------------------- 1 | /build 2 | /release 3 | google-services.json -------------------------------------------------------------------------------- /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.kts. 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 | 23 | -keepclassmembers class * implements android.os.Parcelable { 24 | static ** CREATOR; 25 | } 26 | -keep class com.github.pakka_papad.collection.CollectionType 27 | 28 | -keep class * extends com.google.protobuf.GeneratedMessageLite { *; } 29 | 30 | -dontwarn org.bouncycastle.jsse.BCSSLParameters 31 | -dontwarn org.bouncycastle.jsse.BCSSLSocket 32 | -dontwarn org.bouncycastle.jsse.provider.BouncyCastleJsseProvider 33 | -dontwarn org.conscrypt.Conscrypt$Version 34 | -dontwarn org.conscrypt.Conscrypt 35 | -dontwarn org.conscrypt.ConscryptHostnameVerifier 36 | -dontwarn org.openjsse.javax.net.ssl.SSLParameters 37 | -dontwarn org.openjsse.javax.net.ssl.SSLSocket 38 | -dontwarn org.openjsse.net.ssl.OpenJSSE -------------------------------------------------------------------------------- /app/src/debug/res/values/strings.xml: -------------------------------------------------------------------------------- 1 | 2 | Zen Music - Debug 3 | -------------------------------------------------------------------------------- /app/src/main/assets/changelogs.json: -------------------------------------------------------------------------------- 1 | [ 2 | { 3 | "versionCode": 1, 4 | "versionName": "1.0", 5 | "date": "15 January, 2023", 6 | "changes": [ 7 | "Added multiple theming options", 8 | "Added playlist feature", 9 | "Added search feature", 10 | "Added song blacklist feature" 11 | ] 12 | }, 13 | { 14 | "versionCode": 2, 15 | "versionName": "1.1", 16 | "date": "21 March, 2023", 17 | "changes": [ 18 | "Bug fixes and improvements", 19 | "Themed icon support on Android 13 and above", 20 | "Support for swipe gesture to open and close music player", 21 | "Added player speed and pitch controller", 22 | "Added option to select repeat mode", 23 | "Some UI improvements" 24 | ] 25 | }, 26 | { 27 | "versionCode": 10200, 28 | "versionName": "1.2.0", 29 | "date": "2 December, 2023", 30 | "changes": [ 31 | "Added folder tab", 32 | "Added option to blacklist folder", 33 | "Updated settings screen UI", 34 | "Added sort option", 35 | "Added basic widget (experimental)" 36 | ] 37 | }, 38 | { 39 | "versionCode": 10202, 40 | "versionName": "1.2.2", 41 | "date": "6 January, 2024", 42 | "changes": [ 43 | "Added sleep timer", 44 | "Updated player screen UI", 45 | "Updated playlist tab UI" 46 | ] 47 | }, 48 | { 49 | "versionCode": 10203, 50 | "versionName": "1.2.3", 51 | "date": "29 January, 2024", 52 | "changes": [ 53 | "Updated logo", 54 | "Added support for playback resumption" 55 | ] 56 | } 57 | ] -------------------------------------------------------------------------------- /app/src/main/ic_launcher-playstore.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/pakka-papad/Zen/3111a3b1db486e80cb11a96a65c8f9ab9b850ecb/app/src/main/ic_launcher-playstore.png -------------------------------------------------------------------------------- /app/src/main/java/com/github/pakka_papad/Constants.kt: -------------------------------------------------------------------------------- 1 | package com.github.pakka_papad 2 | 3 | object Constants { 4 | const val QUEUE_STATE_FILE = "queue_state.pb" 5 | const val DATABASE_NAME = "zen_app_db" 6 | object Tables { 7 | const val SONG_TABLE = "song_table" 8 | const val ALBUM_TABLE = "album_table" 9 | const val ARTIST_TABLE = "artist_table" 10 | const val PLAYLIST_TABLE = "playlist_table" 11 | const val PLAYLIST_SONG_CROSS_REF_TABLE = "playlist_song_cross_ref_table" 12 | const val GENRE_TABLE = "genre_table" 13 | const val ALBUM_ARTIST_TABLE = "album_artist_table" 14 | const val COMPOSER_TABLE = "composer_table" 15 | const val LYRICIST_TABLE = "lyricist_table" 16 | const val BLACKLIST_TABLE = "blacklist_table" // for songs 17 | const val BLACKLISTED_FOLDER_TABLE = "blacklisted_folder_table" 18 | const val PLAY_HISTORY_TABLE = "play_history_table" 19 | const val THUMBNAIL_TABLE = "thumbnail_table" 20 | } 21 | const val PACKAGE_NAME = BuildConfig.APPLICATION_ID 22 | const val MESSAGE_DURATION = 3500L 23 | } -------------------------------------------------------------------------------- /app/src/main/java/com/github/pakka_papad/MainActivity.kt: -------------------------------------------------------------------------------- 1 | package com.github.pakka_papad 2 | 3 | import android.graphics.Color 4 | import android.os.Bundle 5 | import androidx.appcompat.app.AppCompatActivity 6 | import androidx.core.splashscreen.SplashScreen.Companion.installSplashScreen 7 | import androidx.core.view.WindowCompat 8 | import com.github.pakka_papad.data.ZenCrashReporter 9 | import com.github.pakka_papad.data.ZenPreferenceProvider 10 | import com.github.pakka_papad.databinding.ActivityMainBinding 11 | import com.google.android.play.core.appupdate.AppUpdateManager 12 | import com.google.android.play.core.appupdate.AppUpdateOptions 13 | import com.google.android.play.core.install.model.AppUpdateType 14 | import com.google.android.play.core.install.model.UpdateAvailability 15 | import dagger.hilt.android.AndroidEntryPoint 16 | import javax.inject.Inject 17 | 18 | @AndroidEntryPoint 19 | class MainActivity : AppCompatActivity() { 20 | 21 | private lateinit var binding: ActivityMainBinding 22 | 23 | @Inject lateinit var preferencesProvider: ZenPreferenceProvider 24 | 25 | @Inject lateinit var appUpdateManager: AppUpdateManager 26 | 27 | @Inject lateinit var crashReporter: ZenCrashReporter 28 | 29 | override fun onCreate(savedInstanceState: Bundle?) { 30 | super.onCreate(savedInstanceState) 31 | installSplashScreen().apply { 32 | setKeepOnScreenCondition { 33 | preferencesProvider.isOnBoardingComplete.value == null 34 | } 35 | } 36 | binding = ActivityMainBinding.inflate(layoutInflater) 37 | setContentView(binding.root) 38 | 39 | WindowCompat.setDecorFitsSystemWindows(window,false) 40 | window.statusBarColor = Color.TRANSPARENT 41 | } 42 | 43 | override fun onResume() { 44 | super.onResume() 45 | 46 | try { 47 | appUpdateManager.appUpdateInfo.addOnSuccessListener { 48 | if (it.updateAvailability() == UpdateAvailability.DEVELOPER_TRIGGERED_UPDATE_IN_PROGRESS){ 49 | appUpdateManager.startUpdateFlow( 50 | it, 51 | this, 52 | AppUpdateOptions.defaultOptions(AppUpdateType.IMMEDIATE) 53 | ) 54 | } 55 | } 56 | } catch (e: Exception) { 57 | crashReporter.logException(e) 58 | } 59 | } 60 | } -------------------------------------------------------------------------------- /app/src/main/java/com/github/pakka_papad/Screens.kt: -------------------------------------------------------------------------------- 1 | package com.github.pakka_papad 2 | 3 | import android.os.Parcelable 4 | import androidx.annotation.DrawableRes 5 | import kotlinx.parcelize.Parcelize 6 | 7 | /** 8 | * !! Dot not change order of already added objects 9 | */ 10 | @Parcelize 11 | enum class Screens(@DrawableRes val outlinedIcon: Int, @DrawableRes val filledIcon: Int): Parcelable { 12 | Songs(R.drawable.ic_outline_music_note_40,R.drawable.ic_baseline_music_note_40), 13 | Albums(R.drawable.ic_outline_album_40,R.drawable.ic_baseline_album_40), 14 | Artists(R.drawable.ic_outline_person_40,R.drawable.ic_baseline_person_40), 15 | Playlists(R.drawable.ic_outline_library_music_40,R.drawable.ic_baseline_library_music_40), 16 | Genres(R.drawable.ic_baseline_piano_40,R.drawable.ic_baseline_piano_40), 17 | Folders(R.drawable.ic_outline_folder_40,R.drawable.ic_baseline_folder_40) 18 | } 19 | -------------------------------------------------------------------------------- /app/src/main/java/com/github/pakka_papad/Util.kt: -------------------------------------------------------------------------------- 1 | package com.github.pakka_papad 2 | 3 | import androidx.media3.common.PlaybackParameters 4 | import com.github.pakka_papad.data.UserPreferences.PlaybackParams 5 | import com.github.pakka_papad.data.copy 6 | import java.text.SimpleDateFormat 7 | import java.util.* 8 | import kotlin.math.round 9 | 10 | fun Float.round(decimals: Int): Float { 11 | var multiplier = 1f 12 | repeat(decimals) { multiplier *= 10 } 13 | return round(this * multiplier) / multiplier 14 | } 15 | 16 | fun Float.toMBfromB(): String{ 17 | val mb = this/(1024*1024) 18 | return "${mb.round(2)} MB" 19 | } 20 | 21 | val dateFormat = SimpleDateFormat("dd/MM/yyyy", Locale.getDefault()) 22 | 23 | fun Long.formatToDate(): String { 24 | if (this < 0) return "" 25 | val calender = Calendar.getInstance().apply { 26 | timeInMillis = this@formatToDate 27 | } 28 | return dateFormat.format(calender.time) 29 | } 30 | 31 | fun Long.toMinutesAndSeconds(): String { 32 | val totalSeconds = this/1000 33 | val minutes = totalSeconds/60 34 | val seconds = totalSeconds%60 35 | return if (minutes == 0L) "$seconds secs" 36 | else if (seconds == 0L) "$minutes mins" 37 | else "$minutes mins $seconds secs" 38 | } 39 | 40 | fun Long.toMS(): String { 41 | val totalSeconds = this/1000 42 | val minutes = totalSeconds/60 43 | val seconds = totalSeconds%60 44 | return "${if(minutes < 10) "0" else ""}${minutes}:${if (seconds < 10) "0" else ""}${seconds}" 45 | } 46 | 47 | fun PlaybackParams.toCorrectedParams(): PlaybackParams { 48 | val correctedSpeed = if (this.playbackSpeed < 1 || this.playbackSpeed > 200) 100 else this.playbackSpeed 49 | val correctedPitch = if (this.playbackPitch < 1 || this.playbackPitch > 200) 100 else this.playbackPitch 50 | return PlaybackParams.getDefaultInstance().copy { 51 | playbackSpeed = correctedSpeed 52 | playbackPitch = correctedPitch 53 | } 54 | } 55 | 56 | /** 57 | * Call on corrected PlaybackParams 58 | */ 59 | fun PlaybackParams.toExoPlayerPlaybackParameters(): PlaybackParameters { 60 | return PlaybackParameters( 61 | this.playbackSpeed.toFloat()/100, 62 | this.playbackPitch.toFloat()/100 63 | ) 64 | } -------------------------------------------------------------------------------- /app/src/main/java/com/github/pakka_papad/ZenApp.kt: -------------------------------------------------------------------------------- 1 | package com.github.pakka_papad 2 | 3 | import android.app.Application 4 | import android.graphics.Bitmap 5 | import androidx.hilt.work.HiltWorkerFactory 6 | import androidx.work.Configuration 7 | import androidx.work.OneTimeWorkRequestBuilder 8 | import androidx.work.WorkManager 9 | import cat.ereza.customactivityoncrash.config.CaocConfig 10 | import cat.ereza.customactivityoncrash.config.CaocConfig.BACKGROUND_MODE_SILENT 11 | import coil.ImageLoader 12 | import coil.ImageLoaderFactory 13 | import com.github.pakka_papad.workers.ThumbnailWorker 14 | import com.google.firebase.FirebaseApp 15 | import dagger.hilt.android.HiltAndroidApp 16 | import timber.log.Timber 17 | import javax.inject.Inject 18 | 19 | @HiltAndroidApp 20 | class ZenApp: Application(), ImageLoaderFactory, Configuration.Provider { 21 | 22 | @Inject lateinit var hiltWorkerFactory: HiltWorkerFactory 23 | 24 | override fun onCreate() { 25 | super.onCreate() 26 | 27 | FirebaseApp.initializeApp(this) 28 | 29 | if (BuildConfig.DEBUG){ 30 | Timber.plant(Timber.DebugTree()) 31 | } 32 | 33 | CaocConfig.Builder.create().apply { 34 | restartActivity(MainActivity::class.java) 35 | errorActivity(CrashActivity::class.java) 36 | backgroundMode(BACKGROUND_MODE_SILENT) 37 | apply() 38 | } 39 | 40 | WorkManager.getInstance(this) 41 | .enqueue(OneTimeWorkRequestBuilder().build()) 42 | } 43 | 44 | override fun newImageLoader(): ImageLoader { 45 | return ImageLoader.Builder(this).apply { 46 | allowRgb565(true) 47 | bitmapConfig(Bitmap.Config.RGB_565) 48 | error(R.drawable.error) 49 | }.build() 50 | } 51 | 52 | override fun getWorkManagerConfiguration(): Configuration { 53 | return Configuration.Builder() 54 | .setWorkerFactory(hiltWorkerFactory) 55 | .build() 56 | } 57 | } -------------------------------------------------------------------------------- /app/src/main/java/com/github/pakka_papad/collection/CollectionActions.kt: -------------------------------------------------------------------------------- 1 | package com.github.pakka_papad.collection 2 | 3 | import androidx.annotation.DrawableRes 4 | import com.github.pakka_papad.R 5 | import com.github.pakka_papad.components.more_options.MoreOptions 6 | 7 | sealed class CollectionActions( 8 | override val onClick: () -> Unit, 9 | override val text: String, 10 | @DrawableRes override val icon: Int, 11 | ): MoreOptions( 12 | onClick = onClick, 13 | text = text, 14 | icon = icon, 15 | ) { 16 | data class AddToQueue(override val onClick: () -> Unit) : 17 | CollectionActions( 18 | onClick = onClick, 19 | text = "Add all to queue", 20 | icon = R.drawable.ic_baseline_queue_music_40 21 | ) 22 | 23 | data class AddToPlaylist(override val onClick: () -> Unit) : 24 | CollectionActions( 25 | onClick = onClick, 26 | text = "Add all to playlist", 27 | icon = R.drawable.ic_baseline_playlist_add_40 28 | ) 29 | 30 | data class Sort(override val onClick: () -> Unit) : 31 | CollectionActions( 32 | onClick = onClick, 33 | text = "Sort", 34 | icon = R.drawable.ic_baseline_sort_40 35 | ) 36 | } 37 | -------------------------------------------------------------------------------- /app/src/main/java/com/github/pakka_papad/collection/CollectionType.kt: -------------------------------------------------------------------------------- 1 | package com.github.pakka_papad.collection 2 | 3 | import android.os.Parcelable 4 | import kotlinx.parcelize.Parcelize 5 | 6 | @Parcelize 7 | class CollectionType(val type: Int, val id: String = ""): Parcelable { 8 | companion object { 9 | const val AlbumType = 0 10 | const val ArtistType = 1 11 | const val PlaylistType = 2 12 | const val AlbumArtistType = 3 13 | const val ComposerType = 4 14 | const val LyricistType = 5 15 | const val GenreType = 6 16 | const val FavouritesType = 7 17 | } 18 | } -------------------------------------------------------------------------------- /app/src/main/java/com/github/pakka_papad/collection/CollectionUi.kt: -------------------------------------------------------------------------------- 1 | package com.github.pakka_papad.collection 2 | 3 | import com.github.pakka_papad.data.music.Song 4 | 5 | data class CollectionUi( 6 | val error: String? = null, 7 | val songs: List = listOf(), 8 | val topBarTitle: String = "", 9 | val topBarBackgroundImageUri: String = "", 10 | ) 11 | -------------------------------------------------------------------------------- /app/src/main/java/com/github/pakka_papad/components/BlockingProgressIndicator.kt: -------------------------------------------------------------------------------- 1 | package com.github.pakka_papad.components 2 | 3 | import androidx.compose.foundation.background 4 | import androidx.compose.foundation.layout.Box 5 | import androidx.compose.foundation.layout.fillMaxWidth 6 | import androidx.compose.foundation.layout.height 7 | import androidx.compose.material3.CircularProgressIndicator 8 | import androidx.compose.material3.MaterialTheme 9 | import androidx.compose.runtime.Composable 10 | import androidx.compose.ui.Alignment 11 | import androidx.compose.ui.Modifier 12 | import androidx.compose.ui.draw.clip 13 | import androidx.compose.ui.unit.dp 14 | 15 | @Composable 16 | fun BlockingProgressIndicator(){ 17 | Box( 18 | modifier = Modifier 19 | .fillMaxWidth(0.75f) 20 | .height(80.dp) 21 | .clip(MaterialTheme.shapes.large) 22 | .background(MaterialTheme.colorScheme.primaryContainer), 23 | contentAlignment = Alignment.Center 24 | ){ 25 | CircularProgressIndicator( 26 | color = MaterialTheme.colorScheme.onPrimaryContainer 27 | ) 28 | } 29 | } -------------------------------------------------------------------------------- /app/src/main/java/com/github/pakka_papad/components/FullScreenSadMessage.kt: -------------------------------------------------------------------------------- 1 | package com.github.pakka_papad.components 2 | 3 | import androidx.compose.foundation.layout.* 4 | import androidx.compose.material3.Icon 5 | import androidx.compose.material3.MaterialTheme 6 | import androidx.compose.material3.Text 7 | import androidx.compose.runtime.Composable 8 | import androidx.compose.ui.Alignment 9 | import androidx.compose.ui.Modifier 10 | import androidx.compose.ui.draw.alpha 11 | import androidx.compose.ui.res.painterResource 12 | import androidx.compose.ui.res.stringResource 13 | import androidx.compose.ui.text.style.TextAlign 14 | import androidx.compose.ui.unit.dp 15 | import com.github.pakka_papad.R 16 | 17 | @Composable 18 | fun FullScreenSadMessage( 19 | message: String? = null, 20 | modifier: Modifier = Modifier, 21 | paddingValues: PaddingValues = PaddingValues(), 22 | ) = Column( 23 | modifier = modifier 24 | .sizeIn(minWidth = 200.dp, minHeight = 200.dp, maxWidth = 500.dp, maxHeight = 500.dp) 25 | .padding(paddingValues) 26 | .alpha(0.4f), 27 | horizontalAlignment = Alignment.CenterHorizontally, 28 | verticalArrangement = Arrangement.Center, 29 | content = { 30 | Icon( 31 | modifier = Modifier 32 | .weight(1f, false) 33 | .aspectRatio(1f, false) 34 | .fillMaxSize() 35 | .padding(24.dp), 36 | painter = painterResource(R.drawable.ic_baseline_sentiment_very_dissatisfied_40), 37 | contentDescription = stringResource(R.string.sad_face_image), 38 | tint = MaterialTheme.colorScheme.onSurface 39 | ) 40 | message?.let { 41 | Text( 42 | modifier = Modifier 43 | .fillMaxWidth() 44 | .padding(24.dp), 45 | text = it, 46 | style = MaterialTheme.typography.headlineSmall, 47 | textAlign = TextAlign.Center, 48 | color = MaterialTheme.colorScheme.onSurface 49 | ) 50 | } 51 | } 52 | ) -------------------------------------------------------------------------------- /app/src/main/java/com/github/pakka_papad/components/SelectableCards.kt: -------------------------------------------------------------------------------- 1 | package com.github.pakka_papad.components 2 | 3 | import androidx.compose.foundation.layout.Arrangement 4 | import androidx.compose.foundation.layout.Row 5 | import androidx.compose.foundation.layout.RowScope 6 | import androidx.compose.foundation.layout.fillMaxWidth 7 | import androidx.compose.foundation.layout.padding 8 | import androidx.compose.material3.Checkbox 9 | import androidx.compose.runtime.Composable 10 | import androidx.compose.ui.Alignment 11 | import androidx.compose.ui.Modifier 12 | import androidx.compose.ui.unit.dp 13 | 14 | @Composable 15 | fun SelectableCard( 16 | isSelected: Boolean, 17 | onSelectChange: (isSelected: Boolean) -> Unit, 18 | content: @Composable RowScope.() -> Unit, 19 | modifier: Modifier = Modifier, 20 | ){ 21 | Row( 22 | modifier = modifier 23 | .fillMaxWidth() 24 | .padding(horizontal = 12.dp, vertical = 12.dp), 25 | verticalAlignment = Alignment.CenterVertically, 26 | horizontalArrangement = Arrangement.spacedBy(12.dp) 27 | ){ 28 | Checkbox( 29 | checked = isSelected, 30 | onCheckedChange = onSelectChange 31 | ) 32 | content() 33 | } 34 | } -------------------------------------------------------------------------------- /app/src/main/java/com/github/pakka_papad/components/Snackbar.kt: -------------------------------------------------------------------------------- 1 | package com.github.pakka_papad.components 2 | 3 | import androidx.compose.material3.MaterialTheme 4 | import androidx.compose.material3.SnackbarData 5 | import androidx.compose.runtime.Composable 6 | import androidx.compose.material3.Snackbar as M3Snackbar 7 | 8 | @Composable 9 | fun Snackbar( 10 | snackbarData: SnackbarData 11 | ){ 12 | M3Snackbar( 13 | snackbarData = snackbarData, 14 | shape = MaterialTheme.shapes.medium, 15 | containerColor = MaterialTheme.colorScheme.secondaryContainer, 16 | contentColor = MaterialTheme.colorScheme.onSecondaryContainer, 17 | ) 18 | } -------------------------------------------------------------------------------- /app/src/main/java/com/github/pakka_papad/components/more_options/FolderOptions.kt: -------------------------------------------------------------------------------- 1 | package com.github.pakka_papad.components.more_options 2 | 3 | import androidx.annotation.DrawableRes 4 | import com.github.pakka_papad.R 5 | 6 | sealed class FolderOptions( 7 | override val onClick: () -> Unit, 8 | override val text: String, 9 | @DrawableRes override val icon: Int, 10 | ) : MoreOptions( 11 | onClick = onClick, 12 | text = text, 13 | icon = icon, 14 | ){ 15 | data class Blacklist(override val onClick: () -> Unit): 16 | FolderOptions( 17 | onClick = onClick, 18 | text = "Blacklist Folder", 19 | icon = R.drawable.ic_baseline_remove_circle_40, 20 | ) 21 | } -------------------------------------------------------------------------------- /app/src/main/java/com/github/pakka_papad/components/more_options/GenreOptions.kt: -------------------------------------------------------------------------------- 1 | package com.github.pakka_papad.components.more_options 2 | 3 | import androidx.annotation.DrawableRes 4 | 5 | sealed class GenreOptions( 6 | override val onClick: () -> Unit, 7 | override val text: String, 8 | @DrawableRes override val icon: Int, 9 | ) : MoreOptions( 10 | onClick = onClick, 11 | text = text, 12 | icon = icon, 13 | ) { 14 | 15 | } -------------------------------------------------------------------------------- /app/src/main/java/com/github/pakka_papad/components/more_options/MoreOptions.kt: -------------------------------------------------------------------------------- 1 | package com.github.pakka_papad.components.more_options 2 | 3 | import androidx.annotation.DrawableRes 4 | 5 | open class MoreOptions( 6 | open val onClick: () -> Unit, 7 | open val text: String, 8 | @DrawableRes open val icon: Int, 9 | ) -------------------------------------------------------------------------------- /app/src/main/java/com/github/pakka_papad/components/more_options/OptionsDropDown.kt: -------------------------------------------------------------------------------- 1 | package com.github.pakka_papad.components.more_options 2 | 3 | import androidx.compose.foundation.layout.Box 4 | import androidx.compose.foundation.layout.fillMaxSize 5 | import androidx.compose.foundation.layout.size 6 | import androidx.compose.material3.* 7 | import androidx.compose.runtime.Composable 8 | import androidx.compose.ui.Alignment 9 | import androidx.compose.ui.Modifier 10 | import androidx.compose.ui.res.painterResource 11 | import androidx.compose.ui.tooling.preview.Preview 12 | import androidx.compose.ui.unit.DpOffset 13 | import androidx.compose.ui.unit.dp 14 | 15 | @Composable 16 | fun OptionsDropDown( 17 | options: List, 18 | expanded: Boolean, 19 | onDismissRequest: () -> Unit, 20 | offset: DpOffset = DpOffset(0.dp, 0.dp), 21 | ) { 22 | if (options.isEmpty()) return 23 | MaterialTheme(shapes = MaterialTheme.shapes.copy(extraSmall = MaterialTheme.shapes.large)) { 24 | DropdownMenu( 25 | expanded = expanded, 26 | onDismissRequest = onDismissRequest, 27 | offset = offset, 28 | ) { 29 | options.forEach { option -> 30 | DropdownMenuItem( 31 | onClick = { 32 | onDismissRequest() 33 | option.onClick() 34 | }, 35 | text = { 36 | Text( 37 | text = option.text, 38 | style = MaterialTheme.typography.bodyMedium, 39 | ) 40 | }, 41 | leadingIcon = { 42 | Icon( 43 | painter = painterResource(option.icon), 44 | modifier = Modifier.size(24.dp), 45 | contentDescription = option.text 46 | ) 47 | } 48 | ) 49 | } 50 | } 51 | } 52 | } 53 | 54 | 55 | @Preview 56 | @Composable 57 | private fun OptionsDropDownPreview() { 58 | Box( 59 | modifier = Modifier.fillMaxSize(), 60 | contentAlignment = Alignment.Center 61 | ) { 62 | OptionsDropDown( 63 | onDismissRequest = { }, 64 | options = listOf( 65 | SongOptions.Info { }, 66 | SongOptions.AddToPlaylist { }, 67 | SongOptions.AddToQueue { }, 68 | SongOptions.RemoveFromQueue { }, 69 | SongOptions.RemoveFromPlaylist { }, 70 | ), 71 | expanded = true 72 | ) 73 | } 74 | } -------------------------------------------------------------------------------- /app/src/main/java/com/github/pakka_papad/components/more_options/PersonOptions.kt: -------------------------------------------------------------------------------- 1 | package com.github.pakka_papad.components.more_options 2 | 3 | import androidx.annotation.DrawableRes 4 | 5 | sealed class PersonOptions( 6 | override val onClick: () -> Unit, 7 | override val text: String, 8 | @DrawableRes override val icon: Int, 9 | ) : MoreOptions( 10 | onClick = onClick, 11 | text = text, 12 | icon = icon, 13 | ) { 14 | 15 | } -------------------------------------------------------------------------------- /app/src/main/java/com/github/pakka_papad/components/more_options/PlaylistOptions.kt: -------------------------------------------------------------------------------- 1 | package com.github.pakka_papad.components.more_options 2 | 3 | import androidx.annotation.DrawableRes 4 | import com.github.pakka_papad.R 5 | 6 | sealed class PlaylistOptions( 7 | override val onClick: () -> Unit, 8 | override val text: String, 9 | @DrawableRes override val icon: Int, 10 | ) : MoreOptions( 11 | onClick = onClick, 12 | text = text, 13 | icon = icon, 14 | ) { 15 | data class DeletePlaylist(override val onClick: () -> Unit) : 16 | PlaylistOptions( 17 | onClick = onClick, 18 | text = "Delete playlist", 19 | icon = R.drawable.ic_baseline_playlist_remove_40 20 | ) 21 | } -------------------------------------------------------------------------------- /app/src/main/java/com/github/pakka_papad/components/more_options/SongOptions.kt: -------------------------------------------------------------------------------- 1 | package com.github.pakka_papad.components.more_options 2 | 3 | import androidx.annotation.DrawableRes 4 | import com.github.pakka_papad.R 5 | 6 | sealed class SongOptions( 7 | override val onClick: () -> Unit, 8 | override val text: String, 9 | @DrawableRes override val icon: Int, 10 | ) : MoreOptions( 11 | onClick = onClick, 12 | text = text, 13 | icon = icon, 14 | ) { 15 | data class AddToQueue(override val onClick: () -> Unit) : 16 | SongOptions( 17 | onClick = onClick, 18 | text = "Add to queue", 19 | icon = R.drawable.ic_baseline_queue_music_40 20 | ) 21 | 22 | data class AddToPlaylist(override val onClick: () -> Unit) : 23 | SongOptions( 24 | onClick = onClick, 25 | text = "Add to playlist", 26 | icon = R.drawable.ic_baseline_playlist_add_40 27 | ) 28 | 29 | data class Info(override val onClick: () -> Unit) : 30 | SongOptions( 31 | onClick = onClick, 32 | text = "Info", 33 | icon = R.drawable.ic_baseline_info_40 34 | ) 35 | 36 | data class RemoveFromQueue(override val onClick: () -> Unit) : 37 | SongOptions( 38 | onClick = onClick, 39 | text = "Remove from queue", 40 | icon = R.drawable.ic_baseline_remove_circle_40 41 | ) 42 | 43 | data class RemoveFromPlaylist(override val onClick: () -> Unit) : 44 | SongOptions( 45 | onClick = onClick, 46 | text = "Remove from playlist", 47 | icon = R.drawable.ic_baseline_playlist_remove_40, 48 | ) 49 | 50 | data class Blacklist(override val onClick: () -> Unit) : 51 | SongOptions( 52 | onClick = onClick, 53 | text = "Add to blacklist", 54 | icon = R.drawable.ic_baseline_remove_circle_40, 55 | ) 56 | } 57 | -------------------------------------------------------------------------------- /app/src/main/java/com/github/pakka_papad/data/CrashReporter.kt: -------------------------------------------------------------------------------- 1 | package com.github.pakka_papad.data 2 | 3 | import com.google.firebase.crashlytics.FirebaseCrashlytics 4 | import javax.inject.Inject 5 | 6 | class ZenCrashReporter @Inject constructor(private val firebase: FirebaseCrashlytics) { 7 | 8 | fun logException(e: Exception?){ 9 | if (e != null) { 10 | firebase.recordException(e) 11 | } 12 | } 13 | 14 | fun logData(message: String) { 15 | firebase.log(message) 16 | } 17 | 18 | fun sendCrashData(reportData: Boolean){ 19 | firebase.setCrashlyticsCollectionEnabled(reportData) 20 | } 21 | } -------------------------------------------------------------------------------- /app/src/main/java/com/github/pakka_papad/data/QueueStateProvider.kt: -------------------------------------------------------------------------------- 1 | package com.github.pakka_papad.data 2 | 3 | import androidx.datastore.core.DataStore 4 | import kotlinx.coroutines.CoroutineScope 5 | import kotlinx.coroutines.flow.Flow 6 | import kotlinx.coroutines.launch 7 | import javax.inject.Inject 8 | import javax.inject.Singleton 9 | 10 | @Singleton 11 | class QueueStateProvider @Inject constructor( 12 | private val queueState: DataStore, 13 | private val coroutineScope: CoroutineScope, 14 | ) { 15 | val state: Flow 16 | get() = queueState.data 17 | 18 | fun saveState(queue: List, startIndex: Int, startPosition: Long) { 19 | coroutineScope.launch { 20 | queueState.updateData { 21 | it.copy { 22 | this.locations.apply { 23 | clear() 24 | addAll(queue) 25 | } 26 | this.startIndex = startIndex 27 | this.startPositionMs = startPosition 28 | } 29 | } 30 | } 31 | } 32 | } -------------------------------------------------------------------------------- /app/src/main/java/com/github/pakka_papad/data/QueueStateSerializer.kt: -------------------------------------------------------------------------------- 1 | package com.github.pakka_papad.data 2 | 3 | import androidx.datastore.core.Serializer 4 | import java.io.InputStream 5 | import java.io.OutputStream 6 | 7 | object QueueStateSerializer: Serializer { 8 | override val defaultValue: QueueState 9 | get() = QueueState.getDefaultInstance() 10 | 11 | override suspend fun readFrom(input: InputStream): QueueState = 12 | try { 13 | QueueState.parseFrom(input) 14 | } catch (_: Exception) { 15 | defaultValue 16 | } 17 | 18 | override suspend fun writeTo(t: QueueState, output: OutputStream) = t.writeTo(output) 19 | } -------------------------------------------------------------------------------- /app/src/main/java/com/github/pakka_papad/data/UserPreferencesSerializer.kt: -------------------------------------------------------------------------------- 1 | package com.github.pakka_papad.data 2 | 3 | import android.os.Build 4 | import androidx.datastore.core.Serializer 5 | import com.github.pakka_papad.Screens 6 | import com.github.pakka_papad.components.SortOptions 7 | import java.io.InputStream 8 | import java.io.OutputStream 9 | import javax.inject.Inject 10 | 11 | class UserPreferencesSerializer @Inject constructor() : Serializer { 12 | override val defaultValue: UserPreferences 13 | get() = UserPreferences.getDefaultInstance().copy { 14 | useMaterialYouTheme = (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) 15 | chosenTheme = UserPreferences.Theme.DARK_MODE 16 | chosenAccent = UserPreferences.Accent.Elm 17 | onBoardingComplete = false 18 | crashlyticsDisabled = false 19 | playbackParams = UserPreferences.PlaybackParams 20 | .getDefaultInstance().copy { 21 | playbackSpeed = 100 22 | playbackPitch = 100 23 | } 24 | selectedTabs.apply { 25 | clear() 26 | addAll(listOf(0,1,2,3,4)) 27 | } 28 | chosenSortOrder.apply { 29 | clear() 30 | put(Screens.Songs.ordinal, SortOptions.TitleASC.ordinal) 31 | put(Screens.Albums.ordinal, SortOptions.TitleASC.ordinal) 32 | put(Screens.Artists.ordinal, SortOptions.NameASC.ordinal) 33 | put(Screens.Genres.ordinal, SortOptions.NameASC.ordinal) 34 | put(Screens.Playlists.ordinal, SortOptions.NameASC.ordinal) 35 | put(Screens.Folders.ordinal, SortOptions.Default.ordinal) 36 | } 37 | } 38 | 39 | override suspend fun readFrom(input: InputStream): UserPreferences = 40 | try { 41 | UserPreferences.parseFrom(input) 42 | } catch (exception: Exception) { 43 | defaultValue 44 | } 45 | 46 | 47 | override suspend fun writeTo(t: UserPreferences, output: OutputStream) = t.writeTo(output) 48 | } -------------------------------------------------------------------------------- /app/src/main/java/com/github/pakka_papad/data/analytics/PlayHistory.kt: -------------------------------------------------------------------------------- 1 | package com.github.pakka_papad.data.analytics 2 | 3 | import androidx.room.ColumnInfo 4 | import androidx.room.Entity 5 | import androidx.room.ForeignKey 6 | import androidx.room.PrimaryKey 7 | import com.github.pakka_papad.Constants 8 | import com.github.pakka_papad.data.music.Song 9 | 10 | @Entity( 11 | tableName = Constants.Tables.PLAY_HISTORY_TABLE, 12 | foreignKeys = [ 13 | ForeignKey( 14 | entity = Song::class, 15 | parentColumns = ["location"], 16 | childColumns = ["songLocation"], 17 | onDelete = ForeignKey.CASCADE 18 | ) 19 | ] 20 | ) 21 | data class PlayHistory( 22 | @PrimaryKey(autoGenerate = true) val id: Long = 0, 23 | @ColumnInfo(index = true) val songLocation: String, 24 | val timestamp: Long, 25 | val playDuration: Long, 26 | ) 27 | -------------------------------------------------------------------------------- /app/src/main/java/com/github/pakka_papad/data/analytics/PlayHistoryDao.kt: -------------------------------------------------------------------------------- 1 | package com.github.pakka_papad.data.analytics 2 | 3 | import androidx.room.Dao 4 | import androidx.room.Insert 5 | import androidx.room.Query 6 | import androidx.room.Transaction 7 | import androidx.room.Update 8 | import com.github.pakka_papad.Constants 9 | import com.github.pakka_papad.data.music.Song 10 | 11 | @Dao 12 | interface PlayHistoryDao { 13 | 14 | @Query( 15 | "SELECT * FROM ${Constants.Tables.SONG_TABLE} WHERE location = :location" 16 | ) 17 | suspend fun getSongFromLocation(location: String): Song? 18 | 19 | @Update 20 | suspend fun updateSong(song: Song) 21 | 22 | @Insert 23 | suspend fun insertRecord(record: PlayHistory) 24 | 25 | @Transaction 26 | suspend fun addRecord(location: String, duration: Long) { 27 | val song = getSongFromLocation(location) ?: return 28 | val time = System.currentTimeMillis() 29 | val updatedSong = song.copy(playCount = 1 + song.playCount, lastPlayed = time) 30 | val record = PlayHistory( 31 | songLocation = location, 32 | timestamp = time, 33 | playDuration = duration, 34 | ) 35 | updateSong(updatedSong) 36 | insertRecord(record) 37 | } 38 | 39 | 40 | } -------------------------------------------------------------------------------- /app/src/main/java/com/github/pakka_papad/data/daos/AlbumArtistDao.kt: -------------------------------------------------------------------------------- 1 | package com.github.pakka_papad.data.daos 2 | 3 | import androidx.room.* 4 | import com.github.pakka_papad.Constants 5 | import com.github.pakka_papad.data.music.AlbumArtist 6 | import com.github.pakka_papad.data.music.AlbumArtistWithSongs 7 | import kotlinx.coroutines.flow.Flow 8 | 9 | @Dao 10 | interface AlbumArtistDao { 11 | 12 | @Insert(onConflict = OnConflictStrategy.IGNORE) 13 | suspend fun insertAllAlbumArtists(data: List) 14 | 15 | @Query("SELECT * FROM ${Constants.Tables.ALBUM_ARTIST_TABLE} WHERE name LIKE '%' || :query || '%'") 16 | suspend fun searchAlbumArtists(query: String): List 17 | 18 | @Transaction 19 | @Query("SELECT * FROM ${Constants.Tables.ALBUM_ARTIST_TABLE} WHERE name = :name") 20 | fun getAlbumArtistWithSongs(name: String): Flow 21 | 22 | @Transaction 23 | @Query("DELETE FROM ${Constants.Tables.ALBUM_ARTIST_TABLE} WHERE name IN " + 24 | "(SELECT albumArtist.name as name FROM ${Constants.Tables.ALBUM_ARTIST_TABLE} as albumArtist LEFT JOIN " + 25 | "${Constants.Tables.SONG_TABLE} as song ON albumArtist.name = song.albumArtist GROUP BY albumArtist.name " + 26 | "HAVING COUNT(song.location) = 0)") 27 | suspend fun cleanAlbumArtistTable() 28 | 29 | } -------------------------------------------------------------------------------- /app/src/main/java/com/github/pakka_papad/data/daos/AlbumDao.kt: -------------------------------------------------------------------------------- 1 | package com.github.pakka_papad.data.daos 2 | 3 | import androidx.room.* 4 | import com.github.pakka_papad.Constants 5 | import com.github.pakka_papad.data.music.Album 6 | import com.github.pakka_papad.data.music.AlbumWithSongs 7 | import kotlinx.coroutines.flow.Flow 8 | 9 | @Dao 10 | interface AlbumDao { 11 | 12 | @Insert(onConflict = OnConflictStrategy.IGNORE) 13 | suspend fun insertAllAlbums(data: List) 14 | 15 | @Query("SELECT * FROM ${Constants.Tables.ALBUM_TABLE} ORDER BY name ASC") 16 | fun getAllAlbums(): Flow> 17 | 18 | @Transaction 19 | @Query("SELECT * FROM ${Constants.Tables.ALBUM_TABLE} WHERE name = :albumName") 20 | fun getAlbumWithSongsByName(albumName: String): Flow 21 | 22 | @Query("DELETE FROM ${Constants.Tables.ALBUM_TABLE}") 23 | suspend fun deleteAllAlbums() 24 | 25 | @Query("SELECT * FROM ${Constants.Tables.ALBUM_TABLE} WHERE name LIKE '%' || :query || '%'") 26 | suspend fun searchAlbums(query: String): List 27 | 28 | @Transaction 29 | @Query("DELETE FROM ${Constants.Tables.ALBUM_TABLE} WHERE name IN " + 30 | "(SELECT album.name as name FROM ${Constants.Tables.ALBUM_TABLE} as album LEFT JOIN " + 31 | "${Constants.Tables.SONG_TABLE} as song ON album.name = song.album GROUP BY album.name " + 32 | "HAVING COUNT(song.location) = 0)") 33 | suspend fun cleanAlbumTable() 34 | } -------------------------------------------------------------------------------- /app/src/main/java/com/github/pakka_papad/data/daos/ArtistDao.kt: -------------------------------------------------------------------------------- 1 | package com.github.pakka_papad.data.daos 2 | 3 | import androidx.room.Dao 4 | import androidx.room.Insert 5 | import androidx.room.OnConflictStrategy 6 | import androidx.room.Query 7 | import androidx.room.Transaction 8 | import com.github.pakka_papad.Constants 9 | import com.github.pakka_papad.data.music.Artist 10 | import com.github.pakka_papad.data.music.ArtistWithSongs 11 | import kotlinx.coroutines.flow.Flow 12 | 13 | @Dao 14 | interface ArtistDao { 15 | 16 | @Insert(onConflict = OnConflictStrategy.IGNORE) 17 | suspend fun insertAllArtists(data: List) 18 | 19 | @Transaction 20 | @Query("SELECT * FROM ${Constants.Tables.ARTIST_TABLE} ORDER BY name ASC") 21 | fun getAllArtistsWithSongs(): Flow> 22 | 23 | @Transaction 24 | @Query("SELECT * FROM ${Constants.Tables.ARTIST_TABLE} WHERE name = :artistName") 25 | fun getArtistWithSongsByName(artistName: String): Flow 26 | 27 | @Query("DELETE FROM ${Constants.Tables.ARTIST_TABLE}") 28 | suspend fun deleteAllArtists() 29 | 30 | @Query("SELECT * FROM ${Constants.Tables.ARTIST_TABLE} WHERE name LIKE '%' || :query || '%'") 31 | suspend fun searchArtists(query: String): List 32 | 33 | @Transaction 34 | @Query("DELETE FROM ${Constants.Tables.ARTIST_TABLE} WHERE name IN " + 35 | "(SELECT artist.name as name FROM ${Constants.Tables.ARTIST_TABLE} as artist LEFT JOIN " + 36 | "${Constants.Tables.SONG_TABLE} as song ON artist.name = song.artist GROUP BY artist.name " + 37 | "HAVING COUNT(song.location) = 0)") 38 | suspend fun cleanArtistTable() 39 | 40 | } -------------------------------------------------------------------------------- /app/src/main/java/com/github/pakka_papad/data/daos/BlacklistDao.kt: -------------------------------------------------------------------------------- 1 | package com.github.pakka_papad.data.daos 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 com.github.pakka_papad.Constants 9 | import com.github.pakka_papad.data.music.BlacklistedSong 10 | import kotlinx.coroutines.flow.Flow 11 | 12 | @Dao 13 | interface BlacklistDao { 14 | 15 | @Insert(onConflict = OnConflictStrategy.IGNORE) 16 | suspend fun addSong(blacklistedSong: BlacklistedSong) 17 | 18 | @Query("SELECT * FROM ${Constants.Tables.BLACKLIST_TABLE}") 19 | suspend fun getBlacklistedSongs(): List 20 | 21 | @Query("SELECT * FROM ${Constants.Tables.BLACKLIST_TABLE}") 22 | fun getBlacklistedSongsFlow(): Flow> 23 | 24 | @Delete(entity = BlacklistedSong::class) 25 | suspend fun deleteBlacklistedSong(blacklistedSong: BlacklistedSong) 26 | } -------------------------------------------------------------------------------- /app/src/main/java/com/github/pakka_papad/data/daos/BlacklistedFolderDao.kt: -------------------------------------------------------------------------------- 1 | package com.github.pakka_papad.data.daos 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 com.github.pakka_papad.Constants 9 | import com.github.pakka_papad.data.music.BlacklistedFolder 10 | import kotlinx.coroutines.flow.Flow 11 | 12 | @Dao 13 | interface BlacklistedFolderDao { 14 | 15 | @Insert(onConflict = OnConflictStrategy.IGNORE) 16 | suspend fun insertFolder(folder: BlacklistedFolder) 17 | 18 | @Query("SELECT * FROM ${Constants.Tables.BLACKLISTED_FOLDER_TABLE}") 19 | fun getAllFolders(): Flow> 20 | 21 | @Delete 22 | suspend fun deleteFolder(folder: BlacklistedFolder) 23 | 24 | } -------------------------------------------------------------------------------- /app/src/main/java/com/github/pakka_papad/data/daos/ComposerDao.kt: -------------------------------------------------------------------------------- 1 | package com.github.pakka_papad.data.daos 2 | 3 | import androidx.room.* 4 | import com.github.pakka_papad.Constants 5 | import com.github.pakka_papad.data.music.Composer 6 | import com.github.pakka_papad.data.music.ComposerWithSongs 7 | import kotlinx.coroutines.flow.Flow 8 | 9 | @Dao 10 | interface ComposerDao { 11 | 12 | @Insert(onConflict = OnConflictStrategy.IGNORE) 13 | suspend fun insertAllComposers(data: List) 14 | 15 | @Query("SELECT * FROM ${Constants.Tables.COMPOSER_TABLE} WHERE name LIKE '%' || :query || '%'") 16 | suspend fun searchComposers(query: String): List 17 | 18 | @Transaction 19 | @Query("SELECT * FROM ${Constants.Tables.COMPOSER_TABLE} WHERE name = :name") 20 | fun getComposerWithSongs(name: String): Flow 21 | 22 | @Transaction 23 | @Query("DELETE FROM ${Constants.Tables.COMPOSER_TABLE} WHERE name IN " + 24 | "(SELECT composer.name as name FROM ${Constants.Tables.COMPOSER_TABLE} as composer LEFT JOIN " + 25 | "${Constants.Tables.SONG_TABLE} as song ON composer.name = song.composer GROUP BY composer.name " + 26 | "HAVING COUNT(song.location) = 0)") 27 | suspend fun cleanComposerTable() 28 | } -------------------------------------------------------------------------------- /app/src/main/java/com/github/pakka_papad/data/daos/GenreDao.kt: -------------------------------------------------------------------------------- 1 | package com.github.pakka_papad.data.daos 2 | 3 | import androidx.room.* 4 | import com.github.pakka_papad.Constants 5 | import com.github.pakka_papad.data.music.Genre 6 | import com.github.pakka_papad.data.music.GenreWithSongs 7 | import kotlinx.coroutines.flow.Flow 8 | 9 | @Dao 10 | interface GenreDao { 11 | 12 | @Insert(onConflict = OnConflictStrategy.IGNORE) 13 | suspend fun insertAllGenres(data: List) 14 | 15 | @Query("SELECT * FROM ${Constants.Tables.GENRE_TABLE} WHERE genre LIKE '%' || :query || '%'") 16 | suspend fun searchGenres(query: String): List 17 | 18 | @Transaction 19 | @Query("SELECT * FROM ${Constants.Tables.GENRE_TABLE} WHERE genre = :genreName") 20 | fun getGenreWithSongs(genreName: String): Flow 21 | 22 | @Query("DELETE FROM ${Constants.Tables.GENRE_TABLE} WHERE genre = :genre") 23 | suspend fun deleteGenre(genre: String) 24 | 25 | @Transaction 26 | @Query("DELETE FROM ${Constants.Tables.GENRE_TABLE} WHERE genre IN " + 27 | "(SELECT genre.genre as genre FROM ${Constants.Tables.GENRE_TABLE} as genre LEFT JOIN " + 28 | "${Constants.Tables.SONG_TABLE} as song ON genre.genre = song.genre GROUP BY genre.genre " + 29 | "HAVING COUNT(song.location) = 0)") 30 | suspend fun cleanGenreTable() 31 | } -------------------------------------------------------------------------------- /app/src/main/java/com/github/pakka_papad/data/daos/LyricistDao.kt: -------------------------------------------------------------------------------- 1 | package com.github.pakka_papad.data.daos 2 | 3 | import androidx.room.* 4 | import com.github.pakka_papad.Constants 5 | import com.github.pakka_papad.data.music.Lyricist 6 | import com.github.pakka_papad.data.music.LyricistWithSongs 7 | import kotlinx.coroutines.flow.Flow 8 | 9 | @Dao 10 | interface LyricistDao { 11 | 12 | @Insert(onConflict = OnConflictStrategy.IGNORE) 13 | suspend fun insertAllLyricists(data: List) 14 | 15 | @Query("SELECT * FROM ${Constants.Tables.LYRICIST_TABLE} WHERE name LIKE '%' || :query || '%'") 16 | suspend fun searchLyricists(query: String): List 17 | 18 | @Transaction 19 | @Query("SELECT * FROM ${Constants.Tables.LYRICIST_TABLE} WHERE name = :name") 20 | fun getLyricistWithSongs(name: String): Flow 21 | 22 | @Transaction 23 | @Query("DELETE FROM ${Constants.Tables.LYRICIST_TABLE} WHERE name IN " + 24 | "(SELECT lyricist.name as name FROM ${Constants.Tables.LYRICIST_TABLE} as lyricist LEFT JOIN " + 25 | "${Constants.Tables.SONG_TABLE} as song ON lyricist.name = song.lyricist GROUP BY lyricist.name " + 26 | "HAVING COUNT(song.location) = 0)") 27 | suspend fun cleanLyricistTable() 28 | } -------------------------------------------------------------------------------- /app/src/main/java/com/github/pakka_papad/data/daos/PlaylistDao.kt: -------------------------------------------------------------------------------- 1 | package com.github.pakka_papad.data.daos 2 | 3 | import androidx.room.* 4 | import com.github.pakka_papad.Constants 5 | import com.github.pakka_papad.data.music.* 6 | import kotlinx.coroutines.flow.Flow 7 | 8 | @Dao 9 | interface PlaylistDao { 10 | 11 | @Insert(entity = Playlist::class) 12 | suspend fun insertPlaylist(playlist: PlaylistExceptId): Long 13 | 14 | @Update(entity = Playlist::class) 15 | suspend fun updatePlaylist(playlist: Playlist) 16 | 17 | @Query("DELETE FROM ${Constants.Tables.PLAYLIST_TABLE} WHERE playlistId = :playlistId") 18 | suspend fun deletePlaylist(playlistId: Long) 19 | 20 | @Query("SELECT * FROM ${Constants.Tables.PLAYLIST_TABLE} WHERE playlistId = :playlistId") 21 | suspend fun getPlaylist(playlistId: Long): Playlist? 22 | 23 | @Insert(onConflict = OnConflictStrategy.IGNORE) 24 | suspend fun insertPlaylistSongCrossRef(playlistSongCrossRefs: List) 25 | 26 | @Delete 27 | suspend fun deletePlaylistSongCrossRef(playlistSongCrossRef: PlaylistSongCrossRef) 28 | 29 | @Query("SELECT * FROM ${Constants.Tables.PLAYLIST_TABLE}") 30 | fun getAllPlaylists(): Flow> 31 | 32 | @Transaction 33 | @Query("SELECT * FROM ${Constants.Tables.PLAYLIST_TABLE} WHERE playlistId = :playlistId") 34 | fun getPlaylistWithSongs(playlistId: Long): Flow 35 | 36 | @Query("SELECT * FROM ${Constants.Tables.PLAYLIST_TABLE} WHERE playlistName LIKE '%' || :query || '%'") 37 | suspend fun searchPlaylists(query: String): List 38 | 39 | @Transaction 40 | @Query("SELECT * FROM ${Constants.Tables.PLAYLIST_TABLE} NATURAL LEFT JOIN " + 41 | "(SELECT playlistId, COUNT(*) AS count FROM " + 42 | "${Constants.Tables.PLAYLIST_SONG_CROSS_REF_TABLE} GROUP BY playlistId)") 43 | fun getAllPlaylistWithSongCount(): Flow> 44 | 45 | } -------------------------------------------------------------------------------- /app/src/main/java/com/github/pakka_papad/data/music/Album.kt: -------------------------------------------------------------------------------- 1 | package com.github.pakka_papad.data.music 2 | 3 | import androidx.room.Entity 4 | import androidx.room.PrimaryKey 5 | import com.github.pakka_papad.Constants 6 | 7 | @Entity(tableName = Constants.Tables.ALBUM_TABLE) 8 | data class Album( 9 | @PrimaryKey val name: String, 10 | val albumArtUri: String? = null, 11 | ) 12 | -------------------------------------------------------------------------------- /app/src/main/java/com/github/pakka_papad/data/music/AlbumArtist.kt: -------------------------------------------------------------------------------- 1 | package com.github.pakka_papad.data.music 2 | 3 | import androidx.room.Entity 4 | import androidx.room.PrimaryKey 5 | import com.github.pakka_papad.Constants 6 | 7 | @Entity(tableName = Constants.Tables.ALBUM_ARTIST_TABLE) 8 | data class AlbumArtist( 9 | @PrimaryKey val name: String 10 | ) 11 | -------------------------------------------------------------------------------- /app/src/main/java/com/github/pakka_papad/data/music/AlbumArtistWithSongs.kt: -------------------------------------------------------------------------------- 1 | package com.github.pakka_papad.data.music 2 | 3 | import androidx.room.Embedded 4 | import androidx.room.Relation 5 | 6 | data class AlbumArtistWithSongs( 7 | @Embedded 8 | val albumArtist: AlbumArtist, 9 | @Relation( 10 | parentColumn = "name", 11 | entityColumn = "albumArtist" 12 | ) 13 | val songs: List, 14 | ) 15 | -------------------------------------------------------------------------------- /app/src/main/java/com/github/pakka_papad/data/music/AlbumWithSongs.kt: -------------------------------------------------------------------------------- 1 | package com.github.pakka_papad.data.music 2 | 3 | import androidx.room.Embedded 4 | import androidx.room.Relation 5 | 6 | data class AlbumWithSongs( 7 | @Embedded 8 | val album: Album, 9 | @Relation( 10 | parentColumn = "name", 11 | entityColumn = "album" 12 | ) 13 | val songs: List, 14 | ) 15 | -------------------------------------------------------------------------------- /app/src/main/java/com/github/pakka_papad/data/music/Artist.kt: -------------------------------------------------------------------------------- 1 | package com.github.pakka_papad.data.music 2 | 3 | import androidx.room.Entity 4 | import androidx.room.PrimaryKey 5 | import com.github.pakka_papad.Constants 6 | 7 | @Entity(tableName = Constants.Tables.ARTIST_TABLE) 8 | data class Artist( 9 | @PrimaryKey val name: String, 10 | ) 11 | -------------------------------------------------------------------------------- /app/src/main/java/com/github/pakka_papad/data/music/ArtistWithSongs.kt: -------------------------------------------------------------------------------- 1 | package com.github.pakka_papad.data.music 2 | 3 | import androidx.room.Embedded 4 | import androidx.room.Relation 5 | 6 | data class ArtistWithSongs( 7 | @Embedded 8 | val artist: Artist, 9 | @Relation( 10 | parentColumn = "name", 11 | entityColumn = "artist" 12 | ) 13 | val songs: List, 14 | ) 15 | -------------------------------------------------------------------------------- /app/src/main/java/com/github/pakka_papad/data/music/BlacklistedFolder.kt: -------------------------------------------------------------------------------- 1 | package com.github.pakka_papad.data.music 2 | 3 | import androidx.room.Entity 4 | import androidx.room.PrimaryKey 5 | import com.github.pakka_papad.Constants 6 | 7 | @Entity(tableName = Constants.Tables.BLACKLISTED_FOLDER_TABLE) 8 | data class BlacklistedFolder( 9 | @PrimaryKey val path: String, 10 | ) 11 | -------------------------------------------------------------------------------- /app/src/main/java/com/github/pakka_papad/data/music/BlacklistedSong.kt: -------------------------------------------------------------------------------- 1 | package com.github.pakka_papad.data.music 2 | 3 | import androidx.room.Entity 4 | import androidx.room.PrimaryKey 5 | import com.github.pakka_papad.Constants 6 | 7 | @Entity(tableName = Constants.Tables.BLACKLIST_TABLE) 8 | data class BlacklistedSong( 9 | @PrimaryKey val location: String, 10 | val title: String, 11 | val artist: String, 12 | ) 13 | -------------------------------------------------------------------------------- /app/src/main/java/com/github/pakka_papad/data/music/Composer.kt: -------------------------------------------------------------------------------- 1 | package com.github.pakka_papad.data.music 2 | 3 | import androidx.room.Entity 4 | import androidx.room.PrimaryKey 5 | import com.github.pakka_papad.Constants 6 | 7 | @Entity(tableName = Constants.Tables.COMPOSER_TABLE) 8 | data class Composer( 9 | @PrimaryKey val name: String, 10 | ) 11 | -------------------------------------------------------------------------------- /app/src/main/java/com/github/pakka_papad/data/music/ComposerWithSongs.kt: -------------------------------------------------------------------------------- 1 | package com.github.pakka_papad.data.music 2 | 3 | import androidx.room.Embedded 4 | import androidx.room.Relation 5 | 6 | data class ComposerWithSongs( 7 | @Embedded 8 | val composer: Composer, 9 | @Relation( 10 | parentColumn = "name", 11 | entityColumn = "composer" 12 | ) 13 | val songs: List, 14 | ) 15 | -------------------------------------------------------------------------------- /app/src/main/java/com/github/pakka_papad/data/music/Genre.kt: -------------------------------------------------------------------------------- 1 | package com.github.pakka_papad.data.music 2 | 3 | import androidx.room.Entity 4 | import androidx.room.PrimaryKey 5 | import com.github.pakka_papad.Constants 6 | 7 | @Entity(tableName = Constants.Tables.GENRE_TABLE) 8 | data class Genre( 9 | @PrimaryKey val genre: String, 10 | ) 11 | -------------------------------------------------------------------------------- /app/src/main/java/com/github/pakka_papad/data/music/GenreWithSongCount.kt: -------------------------------------------------------------------------------- 1 | package com.github.pakka_papad.data.music 2 | 3 | data class GenreWithSongCount( 4 | val genreName: String, 5 | val count: Int, 6 | ) -------------------------------------------------------------------------------- /app/src/main/java/com/github/pakka_papad/data/music/GenreWithSongs.kt: -------------------------------------------------------------------------------- 1 | package com.github.pakka_papad.data.music 2 | 3 | import androidx.room.Embedded 4 | import androidx.room.Relation 5 | 6 | data class GenreWithSongs( 7 | @Embedded 8 | val genre: Genre, 9 | @Relation( 10 | parentColumn = "genre", 11 | entityColumn = "genre" 12 | ) 13 | val songs: List 14 | ) 15 | -------------------------------------------------------------------------------- /app/src/main/java/com/github/pakka_papad/data/music/Lyricist.kt: -------------------------------------------------------------------------------- 1 | package com.github.pakka_papad.data.music 2 | 3 | import androidx.room.Entity 4 | import androidx.room.PrimaryKey 5 | import com.github.pakka_papad.Constants 6 | 7 | @Entity(tableName = Constants.Tables.LYRICIST_TABLE) 8 | data class Lyricist( 9 | @PrimaryKey val name: String, 10 | ) 11 | -------------------------------------------------------------------------------- /app/src/main/java/com/github/pakka_papad/data/music/LyricistWithSongs.kt: -------------------------------------------------------------------------------- 1 | package com.github.pakka_papad.data.music 2 | 3 | import androidx.room.Embedded 4 | import androidx.room.Relation 5 | 6 | data class LyricistWithSongs( 7 | @Embedded 8 | val lyricist: Lyricist, 9 | @Relation( 10 | parentColumn = "name", 11 | entityColumn = "lyricist" 12 | ) 13 | val songs: List, 14 | ) 15 | -------------------------------------------------------------------------------- /app/src/main/java/com/github/pakka_papad/data/music/MetadataExtractor.kt: -------------------------------------------------------------------------------- 1 | package com.github.pakka_papad.data.music 2 | 3 | import android.media.MediaMetadataRetriever 4 | 5 | class MetadataExtractor { 6 | 7 | fun getSongMetadata(path: String): Song.Metadata { 8 | val retriever = MediaMetadataRetriever() 9 | retriever.setDataSource(path) 10 | val mData = Song.Metadata( 11 | artist = retriever.extractMetadata(MediaMetadataRetriever.METADATA_KEY_ARTIST) ?: "Unknown", 12 | albumArtist = retriever.extractMetadata(MediaMetadataRetriever.METADATA_KEY_ALBUMARTIST) ?: "Unknown", 13 | composer = retriever.extractMetadata(MediaMetadataRetriever.METADATA_KEY_COMPOSER) ?: "Unknown", 14 | genre = retriever.extractMetadata(MediaMetadataRetriever.METADATA_KEY_GENRE) ?: "Unknown", 15 | lyricist = retriever.extractMetadata(MediaMetadataRetriever.METADATA_KEY_WRITER) ?: "Unknown", 16 | year = (retriever.extractMetadata(MediaMetadataRetriever.METADATA_KEY_YEAR) ?: "0").toInt(), 17 | duration = (retriever.extractMetadata(MediaMetadataRetriever.METADATA_KEY_DURATION) ?: "0").toLong(), 18 | bitrate = (retriever.extractMetadata(MediaMetadataRetriever.METADATA_KEY_BITRATE) ?: "0").toFloat(), 19 | mimeType = retriever.extractMetadata(MediaMetadataRetriever.METADATA_KEY_MIMETYPE) ?: "Unknown", 20 | sampleRate = 0f, 21 | bitsPerSample = 0 22 | ) 23 | retriever.release() 24 | return mData 25 | } 26 | 27 | fun getSongEmbeddedPicture(path: String): ByteArray? { 28 | val extractor = MediaMetadataRetriever() 29 | extractor.setDataSource(path) 30 | return extractor.embeddedPicture 31 | } 32 | } -------------------------------------------------------------------------------- /app/src/main/java/com/github/pakka_papad/data/music/MiniSong.kt: -------------------------------------------------------------------------------- 1 | package com.github.pakka_papad.data.music 2 | 3 | data class MiniSong( 4 | val title: String, 5 | val location: String, 6 | val artist: String, 7 | val artUri: String, 8 | ) 9 | -------------------------------------------------------------------------------- /app/src/main/java/com/github/pakka_papad/data/music/PersonWithSongCount.kt: -------------------------------------------------------------------------------- 1 | package com.github.pakka_papad.data.music 2 | 3 | sealed interface PersonWithSongCount { 4 | val name: String 5 | val count: Int 6 | } 7 | 8 | data class ArtistWithSongCount( 9 | override val name: String, 10 | override val count: Int 11 | ) : PersonWithSongCount 12 | 13 | data class AlbumArtistWithSongCount( 14 | override val name: String, 15 | override val count: Int 16 | ) : PersonWithSongCount 17 | 18 | data class ComposerWithSongCount( 19 | override val name: String, 20 | override val count: Int 21 | ) : PersonWithSongCount 22 | 23 | data class LyricistWithSongCount( 24 | override val name: String, 25 | override val count: Int 26 | ) : PersonWithSongCount -------------------------------------------------------------------------------- /app/src/main/java/com/github/pakka_papad/data/music/Playlist.kt: -------------------------------------------------------------------------------- 1 | package com.github.pakka_papad.data.music 2 | 3 | import androidx.room.ColumnInfo 4 | import androidx.room.Entity 5 | import androidx.room.PrimaryKey 6 | import com.github.pakka_papad.Constants 7 | 8 | @Entity(tableName = Constants.Tables.PLAYLIST_TABLE) 9 | data class Playlist( 10 | @PrimaryKey(autoGenerate = true) val playlistId: Long, 11 | val playlistName: String, 12 | val createdAt: Long, 13 | @ColumnInfo(defaultValue = "NULL") val artUri: String? = null, 14 | ) 15 | 16 | data class PlaylistExceptId( 17 | val playlistName: String, 18 | val createdAt: Long, 19 | val artUri: String? = null, 20 | ) 21 | -------------------------------------------------------------------------------- /app/src/main/java/com/github/pakka_papad/data/music/PlaylistSongCrossRef.kt: -------------------------------------------------------------------------------- 1 | package com.github.pakka_papad.data.music 2 | 3 | import androidx.room.ColumnInfo 4 | import androidx.room.Entity 5 | import androidx.room.ForeignKey 6 | import com.github.pakka_papad.Constants 7 | 8 | @Entity( 9 | primaryKeys = ["playlistId","location"], 10 | foreignKeys = [ 11 | ForeignKey( 12 | entity = Playlist::class, 13 | parentColumns = ["playlistId"], 14 | childColumns = ["playlistId"], 15 | onDelete = ForeignKey.CASCADE 16 | ), 17 | ForeignKey( 18 | entity = Song::class, 19 | parentColumns = ["location"], 20 | childColumns = ["location"], 21 | onDelete = ForeignKey.CASCADE 22 | ) 23 | ], 24 | tableName = Constants.Tables.PLAYLIST_SONG_CROSS_REF_TABLE, 25 | ) 26 | data class PlaylistSongCrossRef( 27 | val playlistId: Long, 28 | 29 | // refers to location of song 30 | @ColumnInfo(index = true) 31 | val location: String, 32 | ) 33 | -------------------------------------------------------------------------------- /app/src/main/java/com/github/pakka_papad/data/music/PlaylistWithSongCount.kt: -------------------------------------------------------------------------------- 1 | package com.github.pakka_papad.data.music 2 | 3 | data class PlaylistWithSongCount( 4 | val playlistId: Long, 5 | val playlistName: String, 6 | val createdAt: Long, 7 | val artUri: String?, 8 | val count: Int = 0, 9 | ) 10 | -------------------------------------------------------------------------------- /app/src/main/java/com/github/pakka_papad/data/music/PlaylistWithSongs.kt: -------------------------------------------------------------------------------- 1 | package com.github.pakka_papad.data.music 2 | 3 | import androidx.room.Embedded 4 | import androidx.room.Junction 5 | import androidx.room.Relation 6 | 7 | data class PlaylistWithSongs( 8 | @Embedded 9 | val playlist: Playlist, 10 | @Relation( 11 | parentColumn = "playlistId", 12 | entityColumn = "location", 13 | associateBy = Junction(PlaylistSongCrossRef::class) 14 | ) 15 | val songs: List 16 | ) 17 | -------------------------------------------------------------------------------- /app/src/main/java/com/github/pakka_papad/data/music/ScanStatus.kt: -------------------------------------------------------------------------------- 1 | package com.github.pakka_papad.data.music 2 | 3 | sealed class ScanStatus { 4 | object ScanStarted: ScanStatus() 5 | data class ScanProgress(val parsed: Int, val total: Int): ScanStatus() 6 | object ScanComplete: ScanStatus() 7 | object ScanNotRunning: ScanStatus() 8 | } 9 | -------------------------------------------------------------------------------- /app/src/main/java/com/github/pakka_papad/data/services/AnalyticsService.kt: -------------------------------------------------------------------------------- 1 | package com.github.pakka_papad.data.services 2 | 3 | import com.github.pakka_papad.data.analytics.PlayHistoryDao 4 | import kotlinx.coroutines.CoroutineScope 5 | import kotlinx.coroutines.launch 6 | 7 | interface AnalyticsService { 8 | fun logSongPlay(songLocation: String, playDuration: Long) 9 | } 10 | 11 | class AnalyticsServiceImpl( 12 | private val playHistoryDao: PlayHistoryDao, 13 | private val scope: CoroutineScope, 14 | ): AnalyticsService { 15 | override fun logSongPlay(songLocation: String, playDuration: Long) { 16 | scope.launch { 17 | playHistoryDao.addRecord(songLocation, playDuration) 18 | } 19 | } 20 | } -------------------------------------------------------------------------------- /app/src/main/java/com/github/pakka_papad/data/services/SleepTimerService.kt: -------------------------------------------------------------------------------- 1 | package com.github.pakka_papad.data.services 2 | 3 | import android.app.PendingIntent 4 | import kotlinx.coroutines.CoroutineScope 5 | import kotlinx.coroutines.Job 6 | import kotlinx.coroutines.delay 7 | import kotlinx.coroutines.flow.MutableStateFlow 8 | import kotlinx.coroutines.flow.StateFlow 9 | import kotlinx.coroutines.flow.asStateFlow 10 | import kotlinx.coroutines.flow.update 11 | import kotlinx.coroutines.launch 12 | import org.jetbrains.annotations.VisibleForTesting 13 | 14 | interface SleepTimerService { 15 | val isRunning: StateFlow 16 | val timeLeft: StateFlow 17 | 18 | fun cancel() 19 | fun begin(duration: Int) 20 | } 21 | 22 | class SleepTimerServiceImpl( 23 | private val scope: CoroutineScope, 24 | private val closeIntent: PendingIntent, 25 | ): SleepTimerService { 26 | 27 | private var timerJob: Job? = null 28 | 29 | @VisibleForTesting 30 | internal val _isRunning = MutableStateFlow(false) 31 | override val isRunning: StateFlow 32 | = _isRunning.asStateFlow() 33 | 34 | @VisibleForTesting 35 | internal val _timeLeft = MutableStateFlow(0) 36 | override val timeLeft: StateFlow 37 | = _timeLeft.asStateFlow() 38 | 39 | override fun cancel() { 40 | timerJob?.cancel() 41 | timerJob = null 42 | _isRunning.update { false } 43 | } 44 | 45 | override fun begin(duration: Int) { 46 | if (duration == 0) return 47 | if (timerJob != null) return 48 | timerJob = scope.launch { 49 | _isRunning.update { true } 50 | var left = duration 51 | _timeLeft.update { left } 52 | while (left >= 0){ 53 | delay(1000L) 54 | left-- 55 | _timeLeft.update { left } 56 | } 57 | closeIntent.send() 58 | _isRunning.update { false } 59 | } 60 | } 61 | } -------------------------------------------------------------------------------- /app/src/main/java/com/github/pakka_papad/data/thumbnails/Thumbnail.kt: -------------------------------------------------------------------------------- 1 | package com.github.pakka_papad.data.thumbnails 2 | 3 | import androidx.room.Entity 4 | import androidx.room.Index 5 | import androidx.room.PrimaryKey 6 | import com.github.pakka_papad.Constants 7 | 8 | @Entity( 9 | tableName = Constants.Tables.THUMBNAIL_TABLE, 10 | indices = [ 11 | Index(value = ["location"], unique = true) 12 | ] 13 | ) 14 | data class Thumbnail( 15 | @PrimaryKey val location: String, 16 | val addedOn: Long, 17 | val artCount: Int, 18 | val deleteThis: Boolean, 19 | ) 20 | -------------------------------------------------------------------------------- /app/src/main/java/com/github/pakka_papad/data/thumbnails/ThumbnailDao.kt: -------------------------------------------------------------------------------- 1 | package com.github.pakka_papad.data.thumbnails 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 com.github.pakka_papad.Constants 9 | 10 | @Dao 11 | interface ThumbnailDao { 12 | 13 | @Insert(onConflict = OnConflictStrategy.IGNORE, entity = Thumbnail::class) 14 | suspend fun insert(thumbnail: Thumbnail) 15 | 16 | @Query("SELECT * FROM ${Constants.Tables.THUMBNAIL_TABLE} WHERE location = :location") 17 | suspend fun getThumbnail(location: String): Thumbnail? 18 | 19 | @Delete(entity = Thumbnail::class) 20 | suspend fun delete(thumbnail: Thumbnail) 21 | 22 | @Query("UPDATE ${Constants.Tables.THUMBNAIL_TABLE} SET deleteThis = 1 WHERE location = :location") 23 | suspend fun markDelete(location: String) 24 | 25 | @Query("SELECT * FROM ${Constants.Tables.THUMBNAIL_TABLE} WHERE deleteThis = 1") 26 | suspend fun getPendingDeletions(): List 27 | } -------------------------------------------------------------------------------- /app/src/main/java/com/github/pakka_papad/home/HomeBottomBar.kt: -------------------------------------------------------------------------------- 1 | package com.github.pakka_papad.home 2 | 3 | import androidx.compose.foundation.background 4 | import androidx.compose.foundation.layout.* 5 | import androidx.compose.material3.* 6 | import androidx.compose.runtime.Composable 7 | import androidx.compose.ui.Modifier 8 | import androidx.compose.ui.graphics.Color 9 | import androidx.compose.ui.res.painterResource 10 | import androidx.compose.ui.text.font.FontWeight 11 | import androidx.compose.ui.text.style.TextOverflow 12 | import androidx.compose.ui.unit.dp 13 | import com.github.pakka_papad.Screens 14 | 15 | @Composable 16 | fun HomeBottomBar( 17 | currentScreen: Screens, 18 | onScreenChange: (Screens) -> Unit, 19 | bottomBarColor: Color, 20 | selectedTabs: List? 21 | ) { 22 | if (selectedTabs == null) return 23 | val screens = Screens.values() 24 | BottomAppBar( 25 | modifier = Modifier 26 | .background(bottomBarColor) 27 | .windowInsetsPadding(WindowInsets.systemBars.only(WindowInsetsSides.Horizontal + WindowInsetsSides.Bottom)) 28 | .height(88.dp), 29 | ) { 30 | selectedTabs.filter { it >= 0 && it < screens.size } 31 | .map { screens[it] }.forEach { screen -> 32 | NavigationBarItem( 33 | selected = (currentScreen == screen), 34 | onClick = { 35 | onScreenChange(screen) 36 | }, 37 | icon = { 38 | Icon( 39 | painter = painterResource(screen.filledIcon), 40 | contentDescription = null, 41 | modifier = Modifier.size(26.dp) 42 | ) 43 | }, 44 | label = { 45 | Text( 46 | text = screen.name, 47 | style = MaterialTheme.typography.bodySmall, 48 | fontWeight = if (currentScreen == screen) FontWeight.ExtraBold else FontWeight.Bold, 49 | maxLines = 1, 50 | overflow = TextOverflow.Ellipsis 51 | ) 52 | }, 53 | alwaysShowLabel = true 54 | ) 55 | } 56 | } 57 | } -------------------------------------------------------------------------------- /app/src/main/java/com/github/pakka_papad/home/Person.kt: -------------------------------------------------------------------------------- 1 | package com.github.pakka_papad.home 2 | 3 | enum class Person(val text: String) { 4 | Artist("Artist"), 5 | AlbumArtist("Album artist"), 6 | Composer("Composer"), 7 | Lyricist("Lyricist") 8 | } -------------------------------------------------------------------------------- /app/src/main/java/com/github/pakka_papad/nowplaying/NowPlayingOptions.kt: -------------------------------------------------------------------------------- 1 | package com.github.pakka_papad.nowplaying 2 | 3 | import androidx.annotation.DrawableRes 4 | import com.github.pakka_papad.R 5 | import com.github.pakka_papad.components.more_options.MoreOptions 6 | 7 | sealed class NowPlayingOptions( 8 | override val onClick: () -> Unit, 9 | override val text: String, 10 | @DrawableRes override val icon: Int, 11 | ) : MoreOptions( 12 | onClick = onClick, 13 | text = text, 14 | icon = icon 15 | ) { 16 | data class SaveToPlaylist(override val onClick: () -> Unit) : 17 | NowPlayingOptions( 18 | onClick = onClick, 19 | text = "Save queue", 20 | icon = R.drawable.ic_baseline_playlist_add_40 21 | ) 22 | } 23 | -------------------------------------------------------------------------------- /app/src/main/java/com/github/pakka_papad/nowplaying/NowPlayingTopBar.kt: -------------------------------------------------------------------------------- 1 | package com.github.pakka_papad.nowplaying 2 | 3 | import androidx.compose.foundation.clickable 4 | import androidx.compose.foundation.interaction.MutableInteractionSource 5 | import androidx.compose.foundation.layout.padding 6 | import androidx.compose.foundation.layout.size 7 | import androidx.compose.material.icons.Icons 8 | import androidx.compose.material.icons.outlined.MoreVert 9 | import androidx.compose.material.ripple.rememberRipple 10 | import androidx.compose.material3.Icon 11 | import androidx.compose.material3.MaterialTheme 12 | import androidx.compose.runtime.* 13 | import androidx.compose.ui.Modifier 14 | import androidx.compose.ui.draw.rotate 15 | import androidx.compose.ui.res.stringResource 16 | import androidx.compose.ui.unit.DpOffset 17 | import androidx.compose.ui.unit.dp 18 | import com.github.pakka_papad.R 19 | import com.github.pakka_papad.components.TopBarWithBackArrow 20 | import com.github.pakka_papad.components.more_options.OptionsDropDown 21 | 22 | @Composable 23 | fun NowPlayingTopBar( 24 | onBackArrowPressed: () -> Unit, 25 | title: String, 26 | options: List = listOf(), 27 | ) = TopBarWithBackArrow( 28 | onBackArrowPressed = onBackArrowPressed, 29 | title = title, 30 | actions = { 31 | var dropDownMenuExpanded by remember { mutableStateOf(false) } 32 | Icon( 33 | imageVector = Icons.Outlined.MoreVert, 34 | contentDescription = stringResource(R.string.more_menu_button), 35 | modifier = Modifier 36 | .padding(16.dp) 37 | .size(30.dp) 38 | .rotate(90f) 39 | .clickable( 40 | interactionSource = remember { MutableInteractionSource() }, 41 | indication = rememberRipple( 42 | bounded = false, 43 | radius = 25.dp, 44 | ), 45 | onClick = { dropDownMenuExpanded = true } 46 | ), 47 | tint = MaterialTheme.colorScheme.onSurface, 48 | ) 49 | OptionsDropDown( 50 | options = options, 51 | expanded = dropDownMenuExpanded, 52 | onDismissRequest = { dropDownMenuExpanded = false }, 53 | offset = DpOffset(x = 0.dp, y = (-46).dp) 54 | ) 55 | } 56 | ) -------------------------------------------------------------------------------- /app/src/main/java/com/github/pakka_papad/nowplaying/PlayerHelper.kt: -------------------------------------------------------------------------------- 1 | package com.github.pakka_papad.nowplaying 2 | 3 | import androidx.compose.runtime.Stable 4 | import androidx.media3.common.Player.Listener 5 | import androidx.media3.exoplayer.ExoPlayer 6 | 7 | @Stable 8 | class PlayerHelper( 9 | private val exoPlayer: ExoPlayer, 10 | ) { 11 | val currentPosition: Float 12 | get() = exoPlayer.currentPosition.toFloat() 13 | 14 | val duration: Float 15 | get() = exoPlayer.duration.toFloat() 16 | 17 | val currentMediaItemIndex: Int 18 | get() = exoPlayer.currentMediaItemIndex 19 | 20 | fun addListener(listener: Listener) = exoPlayer::addListener 21 | 22 | fun removeListener(listener: Listener) = exoPlayer::removeListener 23 | 24 | fun seekTo(mediaItemIndex: Int, positionMs: Long) { 25 | exoPlayer.seekTo(mediaItemIndex, positionMs) 26 | } 27 | 28 | fun seekTo(positionMs: Long) { 29 | exoPlayer.seekTo(positionMs) 30 | } 31 | } -------------------------------------------------------------------------------- /app/src/main/java/com/github/pakka_papad/nowplaying/Queue.kt: -------------------------------------------------------------------------------- 1 | package com.github.pakka_papad.nowplaying 2 | 3 | import androidx.compose.foundation.ExperimentalFoundationApi 4 | import androidx.compose.foundation.background 5 | import androidx.compose.foundation.layout.* 6 | import androidx.compose.foundation.lazy.LazyColumn 7 | import androidx.compose.foundation.lazy.itemsIndexed 8 | import androidx.compose.foundation.lazy.rememberLazyListState 9 | import androidx.compose.material3.* 10 | import androidx.compose.runtime.Composable 11 | import androidx.compose.runtime.LaunchedEffect 12 | import androidx.compose.ui.Alignment 13 | import androidx.compose.ui.Modifier 14 | import com.github.pakka_papad.components.SongCardV2 15 | import com.github.pakka_papad.data.music.Song 16 | import kotlinx.coroutines.delay 17 | 18 | @OptIn(ExperimentalFoundationApi::class) 19 | @Composable 20 | fun ColumnScope.Queue( 21 | queue: List, 22 | onFavouriteClicked: (Song) -> Unit, 23 | currentSong: Song?, 24 | expanded: Boolean, 25 | playerHelper: PlayerHelper, 26 | onDrag: (fromIndex: Int, toIndex: Int) -> Unit, 27 | ) { 28 | val listState = rememberLazyListState() 29 | LaunchedEffect(key1 = currentSong, key2 = expanded) { 30 | delay(600) 31 | if (!expanded) { 32 | listState.scrollToItem(playerHelper.currentMediaItemIndex) 33 | return@LaunchedEffect 34 | } 35 | if (!listState.isScrollInProgress) { 36 | listState.animateScrollToItem(playerHelper.currentMediaItemIndex) 37 | } 38 | } 39 | val dragDropState = rememberDragDropState(listState) { fromIndex, toIndex -> 40 | onDrag(fromIndex,toIndex) 41 | } 42 | LazyColumn( 43 | modifier = Modifier 44 | .fillMaxWidth() 45 | .fillMaxHeight(0.6f) 46 | .align(Alignment.CenterHorizontally) 47 | .background(MaterialTheme.colorScheme.secondaryContainer) 48 | .dragContainer(dragDropState), 49 | state = listState, 50 | contentPadding = WindowInsets.systemBars 51 | .only(WindowInsetsSides.Bottom) 52 | .asPaddingValues(), 53 | ) { 54 | itemsIndexed( 55 | items = queue, 56 | key = { _, song -> song.location } 57 | ) { index, song -> 58 | val isPlaying = currentSong?.location == song.location 59 | DraggableItem(dragDropState, index) { 60 | SongCardV2( 61 | song = song, 62 | onSongClicked = { if(!isPlaying){ playerHelper.seekTo(index,0) } }, 63 | onFavouriteClicked = onFavouriteClicked, 64 | currentlyPlaying = isPlaying, 65 | ) 66 | } 67 | } 68 | } 69 | } -------------------------------------------------------------------------------- /app/src/main/java/com/github/pakka_papad/nowplaying/RepeatMode.kt: -------------------------------------------------------------------------------- 1 | package com.github.pakka_papad.nowplaying 2 | 3 | import androidx.annotation.DrawableRes 4 | import androidx.media3.exoplayer.ExoPlayer 5 | import com.github.pakka_papad.R 6 | 7 | enum class RepeatMode(@DrawableRes val iconResource: Int) { 8 | NO_REPEAT(R.drawable.baseline_arrow_forward_40), 9 | REPEAT_ALL(R.drawable.baseline_repeat_40), 10 | REPEAT_ONE(R.drawable.baseline_repeat_one_40); 11 | 12 | fun next(): RepeatMode { 13 | return when(this){ 14 | NO_REPEAT -> REPEAT_ALL 15 | REPEAT_ALL -> REPEAT_ONE 16 | REPEAT_ONE -> NO_REPEAT 17 | } 18 | } 19 | 20 | fun toExoPlayerRepeatMode(): Int { 21 | return when(this){ 22 | NO_REPEAT -> ExoPlayer.REPEAT_MODE_OFF 23 | REPEAT_ALL -> ExoPlayer.REPEAT_MODE_ALL 24 | REPEAT_ONE -> ExoPlayer.REPEAT_MODE_ONE 25 | } 26 | } 27 | } -------------------------------------------------------------------------------- /app/src/main/java/com/github/pakka_papad/onboarding/OnBoardingViewModel.kt: -------------------------------------------------------------------------------- 1 | package com.github.pakka_papad.onboarding 2 | 3 | import androidx.lifecycle.ViewModel 4 | import androidx.lifecycle.viewModelScope 5 | import com.github.pakka_papad.data.music.ScanStatus 6 | import com.github.pakka_papad.data.music.SongExtractor 7 | import dagger.hilt.android.lifecycle.HiltViewModel 8 | import kotlinx.coroutines.flow.SharingStarted 9 | import kotlinx.coroutines.flow.stateIn 10 | import javax.inject.Inject 11 | 12 | @HiltViewModel 13 | class OnBoardingViewModel @Inject constructor( 14 | private val songExtractor: SongExtractor, 15 | ) : ViewModel() { 16 | 17 | val scanStatus = songExtractor.scanStatus 18 | .stateIn( 19 | scope = viewModelScope, 20 | started = SharingStarted.WhileSubscribed( 21 | stopTimeoutMillis = 300, 22 | replayExpirationMillis = 0 23 | ), 24 | initialValue = ScanStatus.ScanNotRunning 25 | ) 26 | 27 | fun scanForMusic() { 28 | songExtractor.scanForMusic() 29 | } 30 | 31 | } -------------------------------------------------------------------------------- /app/src/main/java/com/github/pakka_papad/onboarding/PermissionPages.kt: -------------------------------------------------------------------------------- 1 | package com.github.pakka_papad.onboarding 2 | 3 | import androidx.compose.foundation.layout.* 4 | import androidx.compose.material3.* 5 | import androidx.compose.runtime.Composable 6 | import androidx.compose.ui.res.stringResource 7 | import com.airbnb.lottie.compose.* 8 | import com.github.pakka_papad.R 9 | import com.google.accompanist.permissions.ExperimentalPermissionsApi 10 | import com.google.accompanist.permissions.PermissionState 11 | 12 | @OptIn(ExperimentalPermissionsApi::class) 13 | @Composable 14 | fun NotificationPermissionPage( 15 | permissionState: PermissionState, 16 | ) = PermissionPage( 17 | permissionState = permissionState, 18 | lottieRawRes = R.raw.notification_permission, 19 | header = stringResource(R.string.notification_access), 20 | description = stringResource(R.string.zen_needs_access_to_post_notifications_regarding_the_media_playing), 21 | grantedMessage = stringResource(R.string.notification_access_granted), 22 | notGrantedMessage = stringResource(R.string.grant_access_to_notification) 23 | ) 24 | 25 | @OptIn(ExperimentalPermissionsApi::class) 26 | @Composable 27 | fun ReadAudioPermissionPage( 28 | permissionState: PermissionState, 29 | ) = PermissionPage( 30 | permissionState = permissionState, 31 | lottieRawRes = R.raw.storage_permission, 32 | header = stringResource(R.string.audio_files_access), 33 | description = stringResource(R.string.zen_needs_access_to_read_audio_files_present_on_the_device), 34 | grantedMessage = stringResource(R.string.access_granted), 35 | notGrantedMessage = stringResource(R.string.grant_access_to_read_audio) 36 | ) 37 | 38 | @OptIn(ExperimentalPermissionsApi::class) 39 | @Composable 40 | fun ReadStoragePermissionPage( 41 | permissionState: PermissionState, 42 | ) = PermissionPage( 43 | permissionState = permissionState, 44 | lottieRawRes = R.raw.storage_permission, 45 | header = stringResource(R.string.storage_access), 46 | description = stringResource(R.string.zen_needs_storage_access_to_read_audio_files_present_on_the_device), 47 | grantedMessage = stringResource(R.string.access_granted), 48 | notGrantedMessage = stringResource(R.string.grant_access_to_read_storage) 49 | ) -------------------------------------------------------------------------------- /app/src/main/java/com/github/pakka_papad/player/ZenBroadcastReceiver.kt: -------------------------------------------------------------------------------- 1 | package com.github.pakka_papad.player 2 | 3 | import android.content.BroadcastReceiver 4 | import android.content.Context 5 | import android.content.Intent 6 | import com.github.pakka_papad.Constants 7 | import timber.log.Timber 8 | 9 | class ZenBroadcastReceiver: BroadcastReceiver() { 10 | 11 | companion object { 12 | const val AUDIO_CONTROL = "audio_control" 13 | const val ZEN_PLAYER_PAUSE_PLAY = Constants.PACKAGE_NAME + ".ACTION_PAUSE" 14 | const val ZEN_PLAYER_NEXT = Constants.PACKAGE_NAME + ".ACTION_NEXT" 15 | const val ZEN_PLAYER_PREVIOUS = Constants.PACKAGE_NAME + ".ACTION_PREVIOUS" 16 | const val ZEN_PLAYER_CANCEL = Constants.PACKAGE_NAME + ".ACTION_CANCEL" 17 | const val ZEN_PLAYER_LIKE = Constants.PACKAGE_NAME + ".ACTION_LIKE" 18 | const val PAUSE_PLAY_ACTION_REQUEST_CODE = 1001 19 | const val NEXT_ACTION_REQUEST_CODE = 1002 20 | const val PREVIOUS_ACTION_REQUEST_CODE = 1003 21 | const val CANCEL_ACTION_REQUEST_CODE = 1004 22 | const val LIKE_ACTION_REQUEST_CODE = 1005 23 | } 24 | 25 | private var callback: Callback? = null 26 | 27 | override fun onReceive(context: Context?, intent: Intent?) { 28 | val action = intent?.extras?.getString(AUDIO_CONTROL) ?: return 29 | when(action){ 30 | ZEN_PLAYER_NEXT -> callback?.onBroadcastNext() 31 | ZEN_PLAYER_PAUSE_PLAY -> callback?.onBroadcastPausePlay() 32 | ZEN_PLAYER_PREVIOUS -> callback?.onBroadcastPrevious() 33 | ZEN_PLAYER_LIKE -> callback?.onBroadcastLike() 34 | ZEN_PLAYER_CANCEL -> callback?.onBroadcastCancel() 35 | else -> { 36 | Timber.d("no action matched -> $action") 37 | } 38 | } 39 | } 40 | 41 | fun startListening(callback: Callback) { 42 | this.callback = callback 43 | } 44 | 45 | fun stopListening() { 46 | this.callback = null 47 | } 48 | 49 | interface Callback { 50 | fun onBroadcastPausePlay() 51 | fun onBroadcastNext() 52 | fun onBroadcastPrevious() 53 | fun onBroadcastLike() 54 | fun onBroadcastCancel() 55 | } 56 | } -------------------------------------------------------------------------------- /app/src/main/java/com/github/pakka_papad/player/ZenCommandButtons.kt: -------------------------------------------------------------------------------- 1 | package com.github.pakka_papad.player 2 | 3 | import android.os.Bundle 4 | import androidx.media3.common.Player 5 | import androidx.media3.session.CommandButton 6 | import androidx.media3.session.SessionCommand 7 | import com.github.pakka_papad.R 8 | 9 | object ZenCommandButtons { 10 | 11 | val liked by lazy { 12 | CommandButton.Builder() 13 | .apply { 14 | setSessionCommand(SessionCommand(ZenCommands.UNLIKE, Bundle())) 15 | setDisplayName("Unlike") 16 | setIconResId(R.drawable.ic_baseline_favorite_24) 17 | }.build() 18 | } 19 | 20 | val unliked by lazy { 21 | CommandButton.Builder() 22 | .apply { 23 | setSessionCommand(SessionCommand(ZenCommands.LIKE, Bundle())) 24 | setDisplayName("Like") 25 | setIconResId(R.drawable.ic_baseline_favorite_border_24) 26 | }.build() 27 | } 28 | 29 | val previous by lazy { 30 | CommandButton.Builder() 31 | .apply { 32 | setPlayerCommand(Player.COMMAND_SEEK_TO_PREVIOUS_MEDIA_ITEM) 33 | setDisplayName("Previous") 34 | setIconResId(R.drawable.ic_baseline_skip_previous_40) 35 | }.build() 36 | } 37 | 38 | 39 | val playPause by lazy { 40 | CommandButton.Builder() 41 | .apply { 42 | setPlayerCommand(Player.COMMAND_PLAY_PAUSE) 43 | setDisplayName("Previous") 44 | setIconResId(R.drawable.ic_baseline_skip_previous_40) 45 | }.build() 46 | } 47 | 48 | val next by lazy { 49 | CommandButton.Builder() 50 | .apply { 51 | setPlayerCommand(Player.COMMAND_SEEK_TO_NEXT) 52 | setDisplayName("Next") 53 | setIconResId(R.drawable.ic_baseline_skip_next_40) 54 | }.build() 55 | } 56 | 57 | val cancel by lazy { 58 | CommandButton.Builder() 59 | .apply { 60 | setSessionCommand(SessionCommand(ZenCommands.CLOSE, Bundle())) 61 | setDisplayName("Close") 62 | setIconResId(R.drawable.ic_baseline_close_40) 63 | }.build() 64 | } 65 | } -------------------------------------------------------------------------------- /app/src/main/java/com/github/pakka_papad/player/ZenCommands.kt: -------------------------------------------------------------------------------- 1 | package com.github.pakka_papad.player 2 | 3 | object ZenCommands { 4 | const val LIKE = "Like" 5 | const val UNLIKE = "Unlike" 6 | const val CLOSE = "Close" 7 | } -------------------------------------------------------------------------------- /app/src/main/java/com/github/pakka_papad/restore/RestoreContent.kt: -------------------------------------------------------------------------------- 1 | package com.github.pakka_papad.restore 2 | 3 | import androidx.compose.foundation.layout.* 4 | import androidx.compose.foundation.lazy.LazyColumn 5 | import androidx.compose.foundation.lazy.itemsIndexed 6 | import androidx.compose.material.Text 7 | import androidx.compose.material3.MaterialTheme 8 | import androidx.compose.runtime.Composable 9 | import androidx.compose.ui.Modifier 10 | import androidx.compose.ui.text.font.FontWeight 11 | import androidx.compose.ui.text.style.TextOverflow 12 | import com.github.pakka_papad.components.SelectableCard 13 | import com.github.pakka_papad.data.music.BlacklistedSong 14 | 15 | @Composable 16 | fun RestoreContent( 17 | songs: List, 18 | selectList: List, 19 | onSelectChanged: (index: Int, isSelected: Boolean) -> Unit, 20 | ){ 21 | if (songs.size != selectList.size) return 22 | LazyColumn( 23 | modifier = Modifier 24 | .fillMaxSize(), 25 | ) { 26 | itemsIndexed( 27 | items = songs, 28 | key = { index, song -> song.location } 29 | ) { index, song -> 30 | SelectableBlacklistedSong( 31 | song = song, 32 | isSelected = selectList[index], 33 | onSelectChange = { 34 | onSelectChanged(index,it) 35 | } 36 | ) 37 | } 38 | } 39 | } 40 | 41 | @Composable 42 | fun SelectableBlacklistedSong( 43 | song: BlacklistedSong, 44 | isSelected: Boolean, 45 | onSelectChange: (isSelected: Boolean) -> Unit, 46 | ) = SelectableCard( 47 | isSelected = isSelected, 48 | onSelectChange = onSelectChange, 49 | content = { 50 | Column { 51 | Text( 52 | text = song.title, 53 | style = MaterialTheme.typography.titleMedium, 54 | maxLines = 1, 55 | fontWeight = FontWeight.Bold, 56 | overflow = TextOverflow.Ellipsis, 57 | color = MaterialTheme.colorScheme.onSurface, 58 | ) 59 | Text( 60 | text = song.artist, 61 | style = MaterialTheme.typography.titleSmall, 62 | maxLines = 1, 63 | overflow = TextOverflow.Ellipsis, 64 | color = MaterialTheme.colorScheme.onSurface, 65 | ) 66 | } 67 | } 68 | ) -------------------------------------------------------------------------------- /app/src/main/java/com/github/pakka_papad/restore_folder/RestoreFolderContent.kt: -------------------------------------------------------------------------------- 1 | package com.github.pakka_papad.restore_folder 2 | 3 | import androidx.compose.foundation.layout.fillMaxSize 4 | import androidx.compose.foundation.lazy.LazyColumn 5 | import androidx.compose.foundation.lazy.itemsIndexed 6 | import androidx.compose.material.Text 7 | import androidx.compose.material3.MaterialTheme 8 | import androidx.compose.runtime.Composable 9 | import androidx.compose.ui.Modifier 10 | import com.github.pakka_papad.components.SelectableCard 11 | import com.github.pakka_papad.data.music.BlacklistedFolder 12 | 13 | @Composable 14 | fun RestoreFoldersContent( 15 | folders: List, 16 | selectList: List, 17 | onSelectChanged: (index: Int, isSelected: Boolean) -> Unit, 18 | ) { 19 | if (folders.size != selectList.size) return 20 | LazyColumn( 21 | modifier = Modifier 22 | .fillMaxSize(), 23 | ) { 24 | itemsIndexed( 25 | items = folders, 26 | key = { index, folder -> folder.path } 27 | ) { index, folder -> 28 | SelectableBlacklistedFolder( 29 | folder = folder, 30 | isSelected = selectList[index], 31 | onSelectChange = { onSelectChanged(index, it) } 32 | ) 33 | } 34 | } 35 | } 36 | 37 | @Composable 38 | fun SelectableBlacklistedFolder( 39 | folder: BlacklistedFolder, 40 | isSelected: Boolean, 41 | onSelectChange: (Boolean) -> Unit, 42 | ) = SelectableCard( 43 | isSelected = isSelected, 44 | onSelectChange = onSelectChange, 45 | content = { 46 | Text( 47 | text = folder.path, 48 | modifier = Modifier.weight(1f), 49 | color = MaterialTheme.colorScheme.onSurface, 50 | style = MaterialTheme.typography.titleMedium 51 | ) 52 | } 53 | ) 54 | -------------------------------------------------------------------------------- /app/src/main/java/com/github/pakka_papad/search/SearchResult.kt: -------------------------------------------------------------------------------- 1 | package com.github.pakka_papad.search 2 | 3 | import com.github.pakka_papad.data.music.* 4 | 5 | data class SearchResult( 6 | val songs: List = emptyList(), 7 | val albums: List = emptyList(), 8 | val artists: List = emptyList(), 9 | val albumArtists: List = emptyList(), 10 | val composers: List = emptyList(), 11 | val lyricists: List = emptyList(), 12 | val genres: List = emptyList(), 13 | val playlists: List = emptyList(), 14 | val errorMsg: String? = null 15 | ) -------------------------------------------------------------------------------- /app/src/main/java/com/github/pakka_papad/search/SearchType.kt: -------------------------------------------------------------------------------- 1 | package com.github.pakka_papad.search 2 | 3 | enum class SearchType(val text: String) { 4 | Songs("Songs"), 5 | Albums("Albums"), 6 | Artists("Artists"), 7 | AlbumArtists("Album artists"), 8 | Lyricists("Lyricists"), 9 | Composers("Composers"), 10 | Genres("Genres"), 11 | Playlists("Playlists") 12 | } -------------------------------------------------------------------------------- /app/src/main/java/com/github/pakka_papad/select_playlist/SelectPlaylistContent.kt: -------------------------------------------------------------------------------- 1 | package com.github.pakka_papad.select_playlist 2 | 3 | import androidx.compose.foundation.layout.fillMaxSize 4 | import androidx.compose.foundation.lazy.LazyColumn 5 | import androidx.compose.foundation.lazy.itemsIndexed 6 | import androidx.compose.runtime.Composable 7 | import androidx.compose.ui.Modifier 8 | import com.github.pakka_papad.components.SelectablePlaylistCard 9 | import com.github.pakka_papad.data.music.PlaylistWithSongCount 10 | 11 | @Composable 12 | fun SelectPlaylistContent( 13 | playlists: List, 14 | selectList: List, 15 | onSelectChanged: (index: Int) -> Unit 16 | ) { 17 | LazyColumn( 18 | modifier = Modifier 19 | .fillMaxSize(), 20 | ) { 21 | itemsIndexed( 22 | items = playlists, 23 | key = { index, playlist -> playlist.playlistId } 24 | ) { index, playlist -> 25 | SelectablePlaylistCard( 26 | playlist = playlist, 27 | isSelected = selectList[index], 28 | onSelectChange = { 29 | onSelectChanged(index) 30 | } 31 | ) 32 | } 33 | } 34 | } -------------------------------------------------------------------------------- /app/src/main/java/com/github/pakka_papad/splash/SplashFragment.kt: -------------------------------------------------------------------------------- 1 | package com.github.pakka_papad.splash 2 | 3 | import android.os.Bundle 4 | import android.view.LayoutInflater 5 | import android.view.View 6 | import android.view.ViewGroup 7 | import androidx.fragment.app.Fragment 8 | import androidx.lifecycle.lifecycleScope 9 | import androidx.navigation.NavController 10 | import androidx.navigation.fragment.findNavController 11 | import com.github.pakka_papad.R 12 | import com.github.pakka_papad.data.ZenPreferenceProvider 13 | import dagger.hilt.android.AndroidEntryPoint 14 | import kotlinx.coroutines.flow.collectLatest 15 | import kotlinx.coroutines.launch 16 | import javax.inject.Inject 17 | 18 | /** 19 | * Stub Fragment with no view 20 | * Used to decide if onboarding is to be shown or home screen 21 | */ 22 | @AndroidEntryPoint 23 | class SplashFragment : Fragment() { 24 | 25 | private lateinit var navController: NavController 26 | 27 | @Inject 28 | lateinit var preferenceProvider: ZenPreferenceProvider 29 | 30 | override fun onCreateView( 31 | inflater: LayoutInflater, 32 | container: ViewGroup?, 33 | savedInstanceState: Bundle? 34 | ): View? { 35 | navController = findNavController() 36 | lifecycleScope.launch { 37 | preferenceProvider.isOnBoardingComplete.collectLatest { 38 | if (it == null) return@collectLatest 39 | if (navController.currentDestination?.id != R.id.splashFragment) return@collectLatest 40 | val nextFragment = when(it){ 41 | true -> R.id.action_splashFragment_to_homeFragment 42 | false -> R.id.action_splashFragment_to_onBoardingFragment 43 | } 44 | navController.navigate(nextFragment) 45 | } 46 | } 47 | return null 48 | } 49 | } -------------------------------------------------------------------------------- /app/src/main/java/com/github/pakka_papad/storage_explorer/Directory.kt: -------------------------------------------------------------------------------- 1 | package com.github.pakka_papad.storage_explorer 2 | 3 | data class Directory( 4 | val name: String, 5 | val absolutePath: String, 6 | ) 7 | -------------------------------------------------------------------------------- /app/src/main/java/com/github/pakka_papad/storage_explorer/DirectoryContents.kt: -------------------------------------------------------------------------------- 1 | package com.github.pakka_papad.storage_explorer 2 | 3 | import com.github.pakka_papad.data.music.MiniSong 4 | 5 | data class DirectoryContents( 6 | val directories: List = listOf(), 7 | val songs: List = listOf() 8 | ) 9 | -------------------------------------------------------------------------------- /app/src/main/java/com/github/pakka_papad/ui/theme/Shape.kt: -------------------------------------------------------------------------------- 1 | package com.github.pakka_papad.ui.theme 2 | 3 | import androidx.compose.foundation.shape.RoundedCornerShape 4 | import androidx.compose.material.Shapes 5 | import androidx.compose.ui.unit.dp 6 | 7 | val Shapes = Shapes( 8 | small = RoundedCornerShape(4.dp), 9 | medium = RoundedCornerShape(4.dp), 10 | large = RoundedCornerShape(0.dp) 11 | ) -------------------------------------------------------------------------------- /app/src/main/java/com/github/pakka_papad/ui/theme/Type.kt: -------------------------------------------------------------------------------- 1 | package com.github.pakka_papad.ui.theme 2 | 3 | import androidx.compose.material3.Typography 4 | 5 | val ZenTypography = Typography() -------------------------------------------------------------------------------- /app/src/main/java/com/github/pakka_papad/util/MessageStore.kt: -------------------------------------------------------------------------------- 1 | package com.github.pakka_papad.util 2 | 3 | import android.content.Context 4 | import androidx.annotation.StringRes 5 | import javax.inject.Inject 6 | 7 | interface MessageStore { 8 | fun getString(@StringRes id: Int): String 9 | fun getString(@StringRes id: Int, vararg formatArgs: Any): String 10 | } 11 | 12 | class MessageStoreImpl @Inject constructor( 13 | private val context: Context 14 | ) : MessageStore { 15 | override fun getString(id: Int): String { 16 | return context.getString(id) 17 | } 18 | 19 | override fun getString(id: Int, vararg formatArgs: Any): String { 20 | return context.getString(id, *formatArgs) 21 | } 22 | } -------------------------------------------------------------------------------- /app/src/main/java/com/github/pakka_papad/util/Resource.kt: -------------------------------------------------------------------------------- 1 | package com.github.pakka_papad.util 2 | 3 | sealed class Resource(val data: T? = null, val message: String? = null) { 4 | class Success(data: T) : Resource(data) 5 | class Error(message: String, data: T? = null) : Resource(data, message) 6 | class Loading(data: T? = null) : Resource(data) 7 | class Idle() : Resource() 8 | } -------------------------------------------------------------------------------- /app/src/main/java/com/github/pakka_papad/whatsnew/Changelog.kt: -------------------------------------------------------------------------------- 1 | package com.github.pakka_papad.whatsnew 2 | 3 | data class Changelog( 4 | val versionCode: Int, 5 | val versionName: String, 6 | val changes: List, 7 | val date: String, 8 | ) -------------------------------------------------------------------------------- /app/src/main/java/com/github/pakka_papad/whatsnew/ChangelogUi.kt: -------------------------------------------------------------------------------- 1 | package com.github.pakka_papad.whatsnew 2 | 3 | import androidx.compose.foundation.background 4 | import androidx.compose.foundation.layout.* 5 | import androidx.compose.material.Text 6 | import androidx.compose.material3.MaterialTheme 7 | import androidx.compose.runtime.Composable 8 | import androidx.compose.ui.Modifier 9 | import androidx.compose.ui.draw.shadow 10 | import androidx.compose.ui.text.font.FontWeight 11 | import androidx.compose.ui.unit.dp 12 | 13 | @Composable 14 | fun ChangelogUi( 15 | changelog: Changelog 16 | ){ 17 | Column( 18 | modifier = Modifier 19 | .fillMaxWidth() 20 | .shadow(4.dp,MaterialTheme.shapes.extraLarge) 21 | .background(MaterialTheme.colorScheme.primaryContainer,MaterialTheme.shapes.extraLarge) 22 | .padding(16.dp), 23 | verticalArrangement = Arrangement.spacedBy(8.dp) 24 | ){ 25 | Text( 26 | text = "Version ${changelog.versionName}", 27 | style = MaterialTheme.typography.titleLarge, 28 | fontWeight = FontWeight.ExtraBold, 29 | color = MaterialTheme.colorScheme.onPrimaryContainer, 30 | ) 31 | Text( 32 | text = changelog.date, 33 | style = MaterialTheme.typography.titleLarge, 34 | fontWeight = FontWeight.Bold, 35 | color = MaterialTheme.colorScheme.onPrimaryContainer, 36 | ) 37 | Box( 38 | modifier = Modifier 39 | .fillMaxWidth() 40 | .shadow(6.dp,MaterialTheme.shapes.medium) 41 | .background(MaterialTheme.colorScheme.secondary, MaterialTheme.shapes.medium) 42 | .padding(12.dp) 43 | ){ 44 | Column( 45 | verticalArrangement = Arrangement.spacedBy(6.dp) 46 | ){ 47 | changelog.changes.forEach {change -> 48 | Text( 49 | text = "• $change", 50 | style = MaterialTheme.typography.titleMedium, 51 | color = MaterialTheme.colorScheme.onSecondary, 52 | ) 53 | } 54 | } 55 | } 56 | } 57 | } -------------------------------------------------------------------------------- /app/src/main/java/com/github/pakka_papad/widgets/WidgetBroadcast.kt: -------------------------------------------------------------------------------- 1 | package com.github.pakka_papad.widgets 2 | 3 | object WidgetBroadcast { 4 | const val WIDGET_BROADCAST = "widget_broadcast" 5 | 6 | const val SONG_CHANGED = "song_changed" 7 | const val IS_PLAYING_CHANGED = "is_playing_changed" 8 | } -------------------------------------------------------------------------------- /app/src/main/proto/com/github/pakka_papad/data/QueueState.proto: -------------------------------------------------------------------------------- 1 | syntax = "proto3"; 2 | 3 | option java_package = "com.github.pakka_papad.data"; 4 | option java_multiple_files = true; 5 | 6 | message QueueState { 7 | repeated string locations = 1; 8 | int32 startIndex = 2; 9 | int64 startPositionMs = 3; 10 | } -------------------------------------------------------------------------------- /app/src/main/proto/com/github/pakka_papad/data/UserPreferences.proto: -------------------------------------------------------------------------------- 1 | syntax = "proto3"; 2 | 3 | option java_package = "com.github.pakka_papad.data"; 4 | option java_multiple_files = true; 5 | 6 | message UserPreferences { 7 | bool useMaterialYouTheme = 1; 8 | enum Theme { 9 | LIGHT_MODE = 0; 10 | DARK_MODE = 1; 11 | USE_SYSTEM_MODE = 2; 12 | } 13 | Theme chosenTheme = 2; 14 | bool onBoardingComplete = 3; 15 | enum Accent { 16 | Default = 0; 17 | Malibu = 1; 18 | Melrose = 2; 19 | Elm = 3; 20 | Magenta = 4; 21 | JacksonsPurple = 5; 22 | } 23 | Accent chosenAccent = 4; 24 | bool crashlyticsDisabled = 5; 25 | message PlaybackParams { 26 | int32 playbackSpeed = 1; 27 | int32 playbackPitch = 2; 28 | } 29 | PlaybackParams playbackParams = 6; 30 | repeated int32 selectedTabs = 7; 31 | map chosenSortOrder = 8; 32 | } -------------------------------------------------------------------------------- /app/src/main/res/anim/decelerate_interpolator.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | -------------------------------------------------------------------------------- /app/src/main/res/anim/enter_from_bottom.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /app/src/main/res/anim/exit_to_bottom.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 7 | -------------------------------------------------------------------------------- /app/src/main/res/anim/fade_in_enter.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 7 | -------------------------------------------------------------------------------- /app/src/main/res/anim/fade_out_exit.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 7 | -------------------------------------------------------------------------------- /app/src/main/res/anim/grow_from_top.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 9 | -------------------------------------------------------------------------------- /app/src/main/res/anim/no_change.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 7 | -------------------------------------------------------------------------------- /app/src/main/res/anim/shrink_towards_top.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 9 | -------------------------------------------------------------------------------- /app/src/main/res/anim/slide_left_enter.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 10 | -------------------------------------------------------------------------------- /app/src/main/res/anim/slide_left_exit.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 10 | -------------------------------------------------------------------------------- /app/src/main/res/anim/zen_close_enter.xml: -------------------------------------------------------------------------------- 1 | 2 | 5 | 14 | 26 | -------------------------------------------------------------------------------- /app/src/main/res/anim/zen_close_exit.xml: -------------------------------------------------------------------------------- 1 | 2 | 4 | 13 | 25 | -------------------------------------------------------------------------------- /app/src/main/res/anim/zen_open_enter.xml: -------------------------------------------------------------------------------- 1 | 2 | 4 | 13 | 25 | -------------------------------------------------------------------------------- /app/src/main/res/anim/zen_open_exit.xml: -------------------------------------------------------------------------------- 1 | 2 | 5 | 14 | 26 | 27 | 28 | -------------------------------------------------------------------------------- /app/src/main/res/animator/logo_animator.xml: -------------------------------------------------------------------------------- 1 | 2 | 4 | 5 | 10 | 11 | 16 | 17 | 22 | 23 | 28 | 29 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/animated_logo.xml: -------------------------------------------------------------------------------- 1 | 2 | 4 | 5 | 8 | 9 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/baseline_arrow_forward_40.xml: -------------------------------------------------------------------------------- 1 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/baseline_bug_report_40.xml: -------------------------------------------------------------------------------- 1 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/baseline_colorize_40.xml: -------------------------------------------------------------------------------- 1 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/baseline_dark_mode_40.xml: -------------------------------------------------------------------------------- 1 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/baseline_drag_indicator_40.xml: -------------------------------------------------------------------------------- 1 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/baseline_light_mode_40.xml: -------------------------------------------------------------------------------- 1 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/baseline_palette_40.xml: -------------------------------------------------------------------------------- 1 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/baseline_repeat_40.xml: -------------------------------------------------------------------------------- 1 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/baseline_repeat_one_40.xml: -------------------------------------------------------------------------------- 1 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/baseline_send_40.xml: -------------------------------------------------------------------------------- 1 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/baseline_settings_backup_restore_40.xml: -------------------------------------------------------------------------------- 1 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/baseline_speed_24.xml: -------------------------------------------------------------------------------- 1 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/error.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/pakka-papad/Zen/3111a3b1db486e80cb11a96a65c8f9ab9b850ecb/app/src/main/res/drawable/error.png -------------------------------------------------------------------------------- /app/src/main/res/drawable/github_mark.xml: -------------------------------------------------------------------------------- 1 | 6 | 10 | 11 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/ic_baseline_album_40.xml: -------------------------------------------------------------------------------- 1 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/ic_baseline_close_30.xml: -------------------------------------------------------------------------------- 1 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/ic_baseline_close_40.xml: -------------------------------------------------------------------------------- 1 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/ic_baseline_favorite_24.xml: -------------------------------------------------------------------------------- 1 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/ic_baseline_favorite_border_24.xml: -------------------------------------------------------------------------------- 1 | 4 | 5 | 6 | 7 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/ic_baseline_folder_40.xml: -------------------------------------------------------------------------------- 1 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/ic_baseline_info_40.xml: -------------------------------------------------------------------------------- 1 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/ic_baseline_library_music_40.xml: -------------------------------------------------------------------------------- 1 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/ic_baseline_list_24.xml: -------------------------------------------------------------------------------- 1 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/ic_baseline_music_note_40.xml: -------------------------------------------------------------------------------- 1 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/ic_baseline_pause_40.xml: -------------------------------------------------------------------------------- 1 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/ic_baseline_person_40.xml: -------------------------------------------------------------------------------- 1 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/ic_baseline_piano_40.xml: -------------------------------------------------------------------------------- 1 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/ic_baseline_play_arrow_40.xml: -------------------------------------------------------------------------------- 1 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/ic_baseline_playlist_add_40.xml: -------------------------------------------------------------------------------- 1 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/ic_baseline_playlist_play_40.xml: -------------------------------------------------------------------------------- 1 | 4 | 5 | 6 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/ic_baseline_playlist_remove_40.xml: -------------------------------------------------------------------------------- 1 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/ic_baseline_queue_40.xml: -------------------------------------------------------------------------------- 1 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/ic_baseline_queue_music_40.xml: -------------------------------------------------------------------------------- 1 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/ic_baseline_remove_circle_40.xml: -------------------------------------------------------------------------------- 1 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/ic_baseline_sentiment_very_dissatisfied_40.xml: -------------------------------------------------------------------------------- 1 | 4 | 5 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/ic_baseline_shuffle_40.xml: -------------------------------------------------------------------------------- 1 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/ic_baseline_skip_next_40.xml: -------------------------------------------------------------------------------- 1 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/ic_baseline_skip_previous_40.xml: -------------------------------------------------------------------------------- 1 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/ic_baseline_sort_40.xml: -------------------------------------------------------------------------------- 1 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/ic_launcher_foreground.xml: -------------------------------------------------------------------------------- 1 | 6 | 12 | 16 | 17 | 18 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/ic_launcher_monochrome.xml: -------------------------------------------------------------------------------- 1 | 6 | 10 | 11 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/ic_outline_album_40.xml: -------------------------------------------------------------------------------- 1 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/ic_outline_folder_40.xml: -------------------------------------------------------------------------------- 1 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/ic_outline_library_music_40.xml: -------------------------------------------------------------------------------- 1 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/ic_outline_music_note_40.xml: -------------------------------------------------------------------------------- 1 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/ic_outline_person_40.xml: -------------------------------------------------------------------------------- 1 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/linkedin.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/pakka-papad/Zen/3111a3b1db486e80cb11a96a65c8f9ab9b850ecb/app/src/main/res/drawable/linkedin.png -------------------------------------------------------------------------------- /app/src/main/res/drawable/music_widget_background.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/outline_timer_24.xml: -------------------------------------------------------------------------------- 1 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/progress.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 29 | 30 | 31 | 32 | 33 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/seekbar_thumb.xml: -------------------------------------------------------------------------------- 1 | 2 | 4 | 8 | 9 | -------------------------------------------------------------------------------- /app/src/main/res/ic_launcher-playstore.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/pakka-papad/Zen/3111a3b1db486e80cb11a96a65c8f9ab9b850ecb/app/src/main/res/ic_launcher-playstore.png -------------------------------------------------------------------------------- /app/src/main/res/layout/activity_main.xml: -------------------------------------------------------------------------------- 1 | 2 | 8 | 9 | 16 | 17 | -------------------------------------------------------------------------------- /app/src/main/res/mipmap-anydpi-v26/ic_launcher.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /app/src/main/res/mipmap-anydpi-v26/ic_launcher_round.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /app/src/main/res/mipmap-hdpi/ic_launcher.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/pakka-papad/Zen/3111a3b1db486e80cb11a96a65c8f9ab9b850ecb/app/src/main/res/mipmap-hdpi/ic_launcher.webp -------------------------------------------------------------------------------- /app/src/main/res/mipmap-hdpi/ic_launcher_round.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/pakka-papad/Zen/3111a3b1db486e80cb11a96a65c8f9ab9b850ecb/app/src/main/res/mipmap-hdpi/ic_launcher_round.webp -------------------------------------------------------------------------------- /app/src/main/res/mipmap-hdpi/ic_notification.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/pakka-papad/Zen/3111a3b1db486e80cb11a96a65c8f9ab9b850ecb/app/src/main/res/mipmap-hdpi/ic_notification.png -------------------------------------------------------------------------------- /app/src/main/res/mipmap-mdpi/ic_launcher.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/pakka-papad/Zen/3111a3b1db486e80cb11a96a65c8f9ab9b850ecb/app/src/main/res/mipmap-mdpi/ic_launcher.webp -------------------------------------------------------------------------------- /app/src/main/res/mipmap-mdpi/ic_launcher_round.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/pakka-papad/Zen/3111a3b1db486e80cb11a96a65c8f9ab9b850ecb/app/src/main/res/mipmap-mdpi/ic_launcher_round.webp -------------------------------------------------------------------------------- /app/src/main/res/mipmap-mdpi/ic_notification.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/pakka-papad/Zen/3111a3b1db486e80cb11a96a65c8f9ab9b850ecb/app/src/main/res/mipmap-mdpi/ic_notification.png -------------------------------------------------------------------------------- /app/src/main/res/mipmap-xhdpi/ic_launcher.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/pakka-papad/Zen/3111a3b1db486e80cb11a96a65c8f9ab9b850ecb/app/src/main/res/mipmap-xhdpi/ic_launcher.webp -------------------------------------------------------------------------------- /app/src/main/res/mipmap-xhdpi/ic_launcher_round.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/pakka-papad/Zen/3111a3b1db486e80cb11a96a65c8f9ab9b850ecb/app/src/main/res/mipmap-xhdpi/ic_launcher_round.webp -------------------------------------------------------------------------------- /app/src/main/res/mipmap-xhdpi/ic_notification.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/pakka-papad/Zen/3111a3b1db486e80cb11a96a65c8f9ab9b850ecb/app/src/main/res/mipmap-xhdpi/ic_notification.png -------------------------------------------------------------------------------- /app/src/main/res/mipmap-xxhdpi/ic_launcher.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/pakka-papad/Zen/3111a3b1db486e80cb11a96a65c8f9ab9b850ecb/app/src/main/res/mipmap-xxhdpi/ic_launcher.webp -------------------------------------------------------------------------------- /app/src/main/res/mipmap-xxhdpi/ic_launcher_round.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/pakka-papad/Zen/3111a3b1db486e80cb11a96a65c8f9ab9b850ecb/app/src/main/res/mipmap-xxhdpi/ic_launcher_round.webp -------------------------------------------------------------------------------- /app/src/main/res/mipmap-xxhdpi/ic_notification.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/pakka-papad/Zen/3111a3b1db486e80cb11a96a65c8f9ab9b850ecb/app/src/main/res/mipmap-xxhdpi/ic_notification.png -------------------------------------------------------------------------------- /app/src/main/res/mipmap-xxxhdpi/ic_launcher.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/pakka-papad/Zen/3111a3b1db486e80cb11a96a65c8f9ab9b850ecb/app/src/main/res/mipmap-xxxhdpi/ic_launcher.webp -------------------------------------------------------------------------------- /app/src/main/res/mipmap-xxxhdpi/ic_launcher_round.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/pakka-papad/Zen/3111a3b1db486e80cb11a96a65c8f9ab9b850ecb/app/src/main/res/mipmap-xxxhdpi/ic_launcher_round.webp -------------------------------------------------------------------------------- /app/src/main/res/mipmap-xxxhdpi/ic_notification.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/pakka-papad/Zen/3111a3b1db486e80cb11a96a65c8f9ab9b850ecb/app/src/main/res/mipmap-xxxhdpi/ic_notification.png -------------------------------------------------------------------------------- /app/src/main/res/values-v31/splash_theme.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 12 | -------------------------------------------------------------------------------- /app/src/main/res/values/colors.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | #FFBB86FC 4 | #FF6200EE 5 | #FF3700B3 6 | #FF03DAC5 7 | #FF018786 8 | #FF000000 9 | #FFFFFFFF 10 | 11 | #FF17C379 12 | #FF469DA4 13 | #FFA7A4E7 14 | #FFA8F3C2 15 | #FFE0F7E3 16 | #33000000 17 | -------------------------------------------------------------------------------- /app/src/main/res/values/durations.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 300 4 | -------------------------------------------------------------------------------- /app/src/main/res/values/ic_launcher_background.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | #247881 4 | -------------------------------------------------------------------------------- /app/src/main/res/values/splash_theme.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 11 | -------------------------------------------------------------------------------- /app/src/main/res/values/themes.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 10 | -------------------------------------------------------------------------------- /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/music_control_widget_info.xml: -------------------------------------------------------------------------------- 1 | 2 | -------------------------------------------------------------------------------- /app/src/test/java/com/github/pakka_papad/MainDispatcherRule.kt: -------------------------------------------------------------------------------- 1 | package com.github.pakka_papad 2 | 3 | import kotlinx.coroutines.Dispatchers 4 | import kotlinx.coroutines.ExperimentalCoroutinesApi 5 | import kotlinx.coroutines.test.TestDispatcher 6 | import kotlinx.coroutines.test.UnconfinedTestDispatcher 7 | import kotlinx.coroutines.test.resetMain 8 | import kotlinx.coroutines.test.setMain 9 | import org.junit.rules.TestWatcher 10 | import org.junit.runner.Description 11 | 12 | class MainDispatcherRule @OptIn(ExperimentalCoroutinesApi::class) constructor( 13 | private val testDispatcher: TestDispatcher = UnconfinedTestDispatcher(), 14 | ) : TestWatcher() { 15 | @OptIn(ExperimentalCoroutinesApi::class) 16 | override fun starting(description: Description?) { 17 | super.starting(description) 18 | Dispatchers.setMain(testDispatcher) 19 | } 20 | 21 | @OptIn(ExperimentalCoroutinesApi::class) 22 | override fun finished(description: Description?) { 23 | super.finished(description) 24 | Dispatchers.resetMain() 25 | } 26 | } -------------------------------------------------------------------------------- /app/src/test/java/com/github/pakka_papad/Util.kt: -------------------------------------------------------------------------------- 1 | package com.github.pakka_papad 2 | 3 | import kotlin.test.assertContains 4 | import kotlin.test.assertEquals 5 | 6 | fun assertCollectionEquals(expected: Collection, actual: Collection) { 7 | assertEquals(expected.size, actual.size) 8 | actual.forEach { assertContains(expected, it) } 9 | expected.forEach { assertContains(actual, it) } 10 | } -------------------------------------------------------------------------------- /build.gradle.kts: -------------------------------------------------------------------------------- 1 | buildscript { 2 | dependencies { 3 | classpath(Plugins.hilt) 4 | classpath(Plugins.kotlin) 5 | classpath(Plugins.navSafeArgs) 6 | classpath(Plugins.googleServices) 7 | classpath(Plugins.crashlytics) 8 | } 9 | } 10 | 11 | plugins { 12 | id("com.android.application") version "8.2.1" apply false 13 | id("com.android.library") version "8.2.1" apply false 14 | id("org.jetbrains.kotlin.android") version Versions.kotlin apply false 15 | } -------------------------------------------------------------------------------- /buildSrc/build.gradle.kts: -------------------------------------------------------------------------------- 1 | plugins { 2 | `kotlin-dsl` 3 | } 4 | 5 | repositories { 6 | mavenCentral() 7 | } -------------------------------------------------------------------------------- /gradle.properties: -------------------------------------------------------------------------------- 1 | # Project-wide Gradle settings. 2 | # IDE (e.g. Android Studio) users: 3 | # Gradle settings configured through the IDE *will override* 4 | # any settings specified in this file. 5 | # For more details on how to configure your build environment visit 6 | # http://www.gradle.org/docs/current/userguide/build_environment.html 7 | # Specifies the JVM arguments used for the daemon process. 8 | # The setting is particularly useful for tweaking memory settings. 9 | org.gradle.jvmargs=-Xmx2048m -Dfile.encoding=UTF-8 10 | # When configured, Gradle will run in incubating parallel mode. 11 | # This option should only be used with decoupled projects. More details, visit 12 | # http://www.gradle.org/docs/current/userguide/multi_project_builds.html#sec:decoupled_projects 13 | # org.gradle.parallel=true 14 | # AndroidX package structure to make it clearer which packages are bundled with the 15 | # Android operating system, and which are packaged with your app's APK 16 | # https://developer.android.com/topic/libraries/support-library/androidx-rn 17 | android.useAndroidX=true 18 | # Kotlin code style for this project: "official" or "obsolete": 19 | kotlin.code.style=official 20 | # Enables namespacing of each library's R class so that its R class includes only the 21 | # resources declared in the library itself and none from the library's dependencies, 22 | # thereby reducing the size of the R class for that library 23 | android.nonTransitiveRClass=true 24 | android.nonFinalResIds=false -------------------------------------------------------------------------------- /gradle/wrapper/gradle-wrapper.jar: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/pakka-papad/Zen/3111a3b1db486e80cb11a96a65c8f9ab9b850ecb/gradle/wrapper/gradle-wrapper.jar -------------------------------------------------------------------------------- /gradle/wrapper/gradle-wrapper.properties: -------------------------------------------------------------------------------- 1 | #Mon Mar 06 03:00:59 IST 2023 2 | distributionBase=GRADLE_USER_HOME 3 | distributionUrl=https\://services.gradle.org/distributions/gradle-8.2-bin.zip 4 | distributionPath=wrapper/dists 5 | zipStorePath=wrapper/dists 6 | zipStoreBase=GRADLE_USER_HOME 7 | -------------------------------------------------------------------------------- /m3utils/.gitignore: -------------------------------------------------------------------------------- 1 | /build 2 | /release -------------------------------------------------------------------------------- /m3utils/build.gradle.kts: -------------------------------------------------------------------------------- 1 | plugins { 2 | java 3 | } 4 | 5 | dependencies { 6 | implementation("androidx.annotation:annotation:1.7.1") 7 | implementation("com.google.errorprone:error_prone_annotations:2.23.0") 8 | } 9 | -------------------------------------------------------------------------------- /m3utils/src/main/java/dislike/DislikeAnalyzer.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2022 Google LLC 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | package dislike; 18 | 19 | import hct.Hct; 20 | 21 | /** 22 | * Check and/or fix universally disliked colors. 23 | * 24 | *

Color science studies of color preference indicate universal distaste for dark yellow-greens, 25 | * and also show this is correlated to distate for biological waste and rotting food. 26 | * 27 | *

See Palmer and Schloss, 2010 or Schloss and Palmer's Chapter 21 in Handbook of Color 28 | * Psychology (2015). 29 | */ 30 | public final class DislikeAnalyzer { 31 | 32 | private DislikeAnalyzer() { 33 | throw new UnsupportedOperationException(); 34 | } 35 | 36 | /** 37 | * Returns true if color is disliked. 38 | * 39 | *

Disliked is defined as a dark yellow-green that is not neutral. 40 | */ 41 | public static boolean isDisliked(Hct hct) { 42 | final boolean huePasses = Math.round(hct.getHue()) >= 90.0 && Math.round(hct.getHue()) <= 111.0; 43 | final boolean chromaPasses = Math.round(hct.getChroma()) > 16.0; 44 | final boolean tonePasses = Math.round(hct.getTone()) < 65.0; 45 | 46 | return huePasses && chromaPasses && tonePasses; 47 | } 48 | 49 | /** If color is disliked, lighten it to make it likable. */ 50 | public static Hct fixIfDisliked(Hct hct) { 51 | if (isDisliked(hct)) { 52 | return Hct.from(hct.getHue(), hct.getChroma(), 70.0); 53 | } 54 | 55 | return hct; 56 | } 57 | } 58 | -------------------------------------------------------------------------------- /m3utils/src/main/java/dynamiccolor/TonePolarity.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2022 Google LLC 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | package dynamiccolor; 18 | 19 | /** 20 | * Describes the relationship in lightness between two colors. 21 | * 22 | *

'nearer' and 'farther' describes closeness to the surface roles. For instance, 23 | * ToneDeltaPair(A, B, 10, 'nearer', stayTogether) states that A should be 10 lighter than B in 24 | * light mode, and 10 darker than B in dark mode. 25 | * 26 | *

See `ToneDeltaPair` for details. 27 | */ 28 | public enum TonePolarity { 29 | DARKER, 30 | LIGHTER, 31 | NEARER, 32 | FARTHER; 33 | } 34 | -------------------------------------------------------------------------------- /m3utils/src/main/java/palettes/CorePalette.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2021 Google LLC 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | package palettes; 18 | 19 | import static java.lang.Math.max; 20 | import static java.lang.Math.min; 21 | 22 | import hct.Hct; 23 | 24 | /** 25 | * An intermediate concept between the key color for a UI theme, and a full color scheme. 5 sets of 26 | * tones are generated, all except one use the same hue as the key color, and all vary in chroma. 27 | */ 28 | public final class CorePalette { 29 | public TonalPalette a1; 30 | public TonalPalette a2; 31 | public TonalPalette a3; 32 | public TonalPalette n1; 33 | public TonalPalette n2; 34 | public TonalPalette error; 35 | 36 | /** 37 | * Create key tones from a color. 38 | * 39 | * @param argb ARGB representation of a color 40 | */ 41 | public static CorePalette of(int argb) { 42 | return new CorePalette(argb, false); 43 | } 44 | 45 | /** 46 | * Create content key tones from a color. 47 | * 48 | * @param argb ARGB representation of a color 49 | */ 50 | public static CorePalette contentOf(int argb) { 51 | return new CorePalette(argb, true); 52 | } 53 | 54 | private CorePalette(int argb, boolean isContent) { 55 | Hct hct = Hct.fromInt(argb); 56 | double hue = hct.getHue(); 57 | double chroma = hct.getChroma(); 58 | if (isContent) { 59 | this.a1 = TonalPalette.fromHueAndChroma(hue, chroma); 60 | this.a2 = TonalPalette.fromHueAndChroma(hue, chroma / 3.); 61 | this.a3 = TonalPalette.fromHueAndChroma(hue + 60., chroma / 2.); 62 | this.n1 = TonalPalette.fromHueAndChroma(hue, min(chroma / 12., 4.)); 63 | this.n2 = TonalPalette.fromHueAndChroma(hue, min(chroma / 6., 8.)); 64 | } else { 65 | this.a1 = TonalPalette.fromHueAndChroma(hue, max(48., chroma)); 66 | this.a2 = TonalPalette.fromHueAndChroma(hue, 16.); 67 | this.a3 = TonalPalette.fromHueAndChroma(hue + 60., 24.); 68 | this.n1 = TonalPalette.fromHueAndChroma(hue, 4.); 69 | this.n2 = TonalPalette.fromHueAndChroma(hue, 8.); 70 | } 71 | this.error = TonalPalette.fromHueAndChroma(25, 84.); 72 | } 73 | } 74 | -------------------------------------------------------------------------------- /m3utils/src/main/java/quantize/PointProvider.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2021 Google LLC 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | package quantize; 18 | 19 | /** An interface to allow use of different color spaces by quantizers. */ 20 | public interface PointProvider { 21 | /** The four components in the color space of an sRGB color. */ 22 | public double[] fromInt(int argb); 23 | 24 | /** The ARGB (i.e. hex code) representation of this color. */ 25 | public int toInt(double[] point); 26 | 27 | /** 28 | * Squared distance between two colors. Distance is defined by scientific color spaces and 29 | * referred to as delta E. 30 | */ 31 | public double distance(double[] a, double[] b); 32 | } 33 | -------------------------------------------------------------------------------- /m3utils/src/main/java/quantize/PointProviderLab.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2021 Google LLC 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | package quantize; 18 | 19 | import utils.ColorUtils; 20 | 21 | /** 22 | * Provides conversions needed for K-Means quantization. Converting input to points, and converting 23 | * the final state of the K-Means algorithm to colors. 24 | */ 25 | public final class PointProviderLab implements PointProvider { 26 | /** 27 | * Convert a color represented in ARGB to a 3-element array of L*a*b* coordinates of the color. 28 | */ 29 | @Override 30 | public double[] fromInt(int argb) { 31 | double[] lab = ColorUtils.labFromArgb(argb); 32 | return new double[] {lab[0], lab[1], lab[2]}; 33 | } 34 | 35 | /** Convert a 3-element array to a color represented in ARGB. */ 36 | @Override 37 | public int toInt(double[] lab) { 38 | return ColorUtils.argbFromLab(lab[0], lab[1], lab[2]); 39 | } 40 | 41 | /** 42 | * Standard CIE 1976 delta E formula also takes the square root, unneeded here. This method is 43 | * used by quantization algorithms to compare distance, and the relative ordering is the same, 44 | * with or without a square root. 45 | * 46 | *

This relatively minor optimization is helpful because this method is called at least once 47 | * for each pixel in an image. 48 | */ 49 | @Override 50 | public double distance(double[] one, double[] two) { 51 | double dL = (one[0] - two[0]); 52 | double dA = (one[1] - two[1]); 53 | double dB = (one[2] - two[2]); 54 | return (dL * dL + dA * dA + dB * dB); 55 | } 56 | } 57 | -------------------------------------------------------------------------------- /m3utils/src/main/java/quantize/Quantizer.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2021 Google LLC 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | package quantize; 18 | 19 | interface Quantizer { 20 | public QuantizerResult quantize(int[] pixels, int maxColors); 21 | } 22 | -------------------------------------------------------------------------------- /m3utils/src/main/java/quantize/QuantizerCelebi.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2021 Google LLC 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | package quantize; 18 | 19 | import java.util.Map; 20 | import java.util.Set; 21 | 22 | /** 23 | * An image quantizer that improves on the quality of a standard K-Means algorithm by setting the 24 | * K-Means initial state to the output of a Wu quantizer, instead of random centroids. Improves on 25 | * speed by several optimizations, as implemented in Wsmeans, or Weighted Square Means, K-Means with 26 | * those optimizations. 27 | * 28 | *

This algorithm was designed by M. Emre Celebi, and was found in their 2011 paper, Improving 29 | * the Performance of K-Means for Color Quantization. https://arxiv.org/abs/1101.0395 30 | */ 31 | public final class QuantizerCelebi { 32 | private QuantizerCelebi() {} 33 | 34 | /** 35 | * Reduce the number of colors needed to represented the input, minimizing the difference between 36 | * the original image and the recolored image. 37 | * 38 | * @param pixels Colors in ARGB format. 39 | * @param maxColors The number of colors to divide the image into. A lower number of colors may be 40 | * returned. 41 | * @return Map with keys of colors in ARGB format, and values of number of pixels in the original 42 | * image that correspond to the color in the quantized image. 43 | */ 44 | public static Map quantize(int[] pixels, int maxColors) { 45 | QuantizerWu wu = new QuantizerWu(); 46 | QuantizerResult wuResult = wu.quantize(pixels, maxColors); 47 | 48 | Set wuClustersAsObjects = wuResult.colorToCount.keySet(); 49 | int index = 0; 50 | int[] wuClusters = new int[wuClustersAsObjects.size()]; 51 | for (Integer argb : wuClustersAsObjects) { 52 | wuClusters[index++] = argb; 53 | } 54 | 55 | return QuantizerWsmeans.quantize(pixels, wuClusters, maxColors); 56 | } 57 | } 58 | -------------------------------------------------------------------------------- /m3utils/src/main/java/quantize/QuantizerMap.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2021 Google LLC 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | package quantize; 18 | 19 | import java.util.LinkedHashMap; 20 | import java.util.Map; 21 | 22 | /** Creates a dictionary with keys of colors, and values of count of the color */ 23 | public final class QuantizerMap implements Quantizer { 24 | Map colorToCount; 25 | 26 | @Override 27 | public QuantizerResult quantize(int[] pixels, int colorCount) { 28 | final Map pixelByCount = new LinkedHashMap<>(); 29 | for (int pixel : pixels) { 30 | final Integer currentPixelCount = pixelByCount.get(pixel); 31 | final int newPixelCount = currentPixelCount == null ? 1 : currentPixelCount + 1; 32 | pixelByCount.put(pixel, newPixelCount); 33 | } 34 | colorToCount = pixelByCount; 35 | return new QuantizerResult(pixelByCount); 36 | } 37 | 38 | public Map getColorToCount() { 39 | return colorToCount; 40 | } 41 | } 42 | -------------------------------------------------------------------------------- /m3utils/src/main/java/quantize/QuantizerResult.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2021 Google LLC 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | package quantize; 18 | 19 | import java.util.Map; 20 | 21 | /** Represents result of a quantizer run */ 22 | public final class QuantizerResult { 23 | public final Map colorToCount; 24 | 25 | QuantizerResult(Map colorToCount) { 26 | this.colorToCount = colorToCount; 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /m3utils/src/main/java/scheme/SchemeContent.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2022 Google LLC 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | package scheme; 18 | 19 | import static java.lang.Math.max; 20 | 21 | import dislike.DislikeAnalyzer; 22 | import hct.Hct; 23 | import palettes.TonalPalette; 24 | import temperature.TemperatureCache; 25 | 26 | /** 27 | * A scheme that places the source color in Scheme.primaryContainer. 28 | * 29 | *

Primary Container is the source color, adjusted for color relativity. It maintains constant 30 | * appearance in light mode and dark mode. This adds ~5 tone in light mode, and subtracts ~5 tone in 31 | * dark mode. 32 | * 33 | *

Tertiary Container is an analogous color, specifically, the analog of a color wheel divided 34 | * into 6, and the precise analog is the one found by increasing hue. This is a scientifically 35 | * grounded equivalent to rotating hue clockwise by 60 degrees. It also maintains constant 36 | * appearance. 37 | */ 38 | public class SchemeContent extends DynamicScheme { 39 | public SchemeContent(Hct sourceColorHct, boolean isDark, double contrastLevel) { 40 | super( 41 | sourceColorHct, 42 | Variant.CONTENT, 43 | isDark, 44 | contrastLevel, 45 | TonalPalette.fromHueAndChroma(sourceColorHct.getHue(), sourceColorHct.getChroma()), 46 | TonalPalette.fromHueAndChroma( 47 | sourceColorHct.getHue(), 48 | max(sourceColorHct.getChroma() - 32.0, sourceColorHct.getChroma() * 0.5)), 49 | TonalPalette.fromHct( 50 | DislikeAnalyzer.fixIfDisliked( 51 | new TemperatureCache(sourceColorHct) 52 | .getAnalogousColors(/* count= */ 3, /* divisions= */ 6) 53 | .get(2))), 54 | TonalPalette.fromHueAndChroma(sourceColorHct.getHue(), sourceColorHct.getChroma() / 8.0), 55 | TonalPalette.fromHueAndChroma( 56 | sourceColorHct.getHue(), (sourceColorHct.getChroma() / 8.0) + 4.0)); 57 | } 58 | } 59 | -------------------------------------------------------------------------------- /m3utils/src/main/java/scheme/SchemeExpressive.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2022 Google LLC 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | package scheme; 18 | 19 | import hct.Hct; 20 | import palettes.TonalPalette; 21 | import utils.MathUtils; 22 | 23 | /** A playful theme - the source color's hue does not appear in the theme. */ 24 | public class SchemeExpressive extends DynamicScheme { 25 | // NOMUTANTS--arbitrary increments/decrements, correctly, still passes tests. 26 | private static final double[] HUES = {0, 21, 51, 121, 151, 191, 271, 321, 360}; 27 | private static final double[] SECONDARY_ROTATIONS = {45, 95, 45, 20, 45, 90, 45, 45, 45}; 28 | private static final double[] TERTIARY_ROTATIONS = {120, 120, 20, 45, 20, 15, 20, 120, 120}; 29 | 30 | public SchemeExpressive(Hct sourceColorHct, boolean isDark, double contrastLevel) { 31 | super( 32 | sourceColorHct, 33 | Variant.EXPRESSIVE, 34 | isDark, 35 | contrastLevel, 36 | TonalPalette.fromHueAndChroma( 37 | MathUtils.sanitizeDegreesDouble(sourceColorHct.getHue() + 240.0), 40.0), 38 | TonalPalette.fromHueAndChroma( 39 | DynamicScheme.getRotatedHue(sourceColorHct, HUES, SECONDARY_ROTATIONS), 24.0), 40 | TonalPalette.fromHueAndChroma( 41 | DynamicScheme.getRotatedHue(sourceColorHct, HUES, TERTIARY_ROTATIONS), 32.0), 42 | TonalPalette.fromHueAndChroma( 43 | MathUtils.sanitizeDegreesDouble(sourceColorHct.getHue() + 15.0), 8.0), 44 | TonalPalette.fromHueAndChroma( 45 | MathUtils.sanitizeDegreesDouble(sourceColorHct.getHue() + 15.0), 12.0)); 46 | } 47 | } 48 | -------------------------------------------------------------------------------- /m3utils/src/main/java/scheme/SchemeFidelity.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2022 Google LLC 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | package scheme; 18 | 19 | import dislike.DislikeAnalyzer; 20 | import hct.Hct; 21 | import palettes.TonalPalette; 22 | import temperature.TemperatureCache; 23 | 24 | /** 25 | * A scheme that places the source color in Scheme.primaryContainer. 26 | * 27 | *

Primary Container is the source color, adjusted for color relativity. It maintains constant 28 | * appearance in light mode and dark mode. This adds ~5 tone in light mode, and subtracts ~5 tone in 29 | * dark mode. 30 | * 31 | *

Tertiary Container is the complement to the source color, using TemperatureCache. It also 32 | * maintains constant appearance. 33 | */ 34 | public class SchemeFidelity extends DynamicScheme { 35 | public SchemeFidelity(Hct sourceColorHct, boolean isDark, double contrastLevel) { 36 | super( 37 | sourceColorHct, 38 | Variant.FIDELITY, 39 | isDark, 40 | contrastLevel, 41 | TonalPalette.fromHueAndChroma(sourceColorHct.getHue(), sourceColorHct.getChroma()), 42 | TonalPalette.fromHueAndChroma( 43 | sourceColorHct.getHue(), 44 | Math.max(sourceColorHct.getChroma() - 32.0, sourceColorHct.getChroma() * 0.5)), 45 | TonalPalette.fromHct( 46 | DislikeAnalyzer.fixIfDisliked(new TemperatureCache(sourceColorHct).getComplement())), 47 | TonalPalette.fromHueAndChroma(sourceColorHct.getHue(), sourceColorHct.getChroma() / 8.0), 48 | TonalPalette.fromHueAndChroma( 49 | sourceColorHct.getHue(), (sourceColorHct.getChroma() / 8.0) + 4.0)); 50 | } 51 | } 52 | -------------------------------------------------------------------------------- /m3utils/src/main/java/scheme/SchemeFruitSalad.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2023 Google LLC 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | package scheme; 17 | 18 | import hct.Hct; 19 | import palettes.TonalPalette; 20 | import utils.MathUtils; 21 | 22 | /** A playful theme - the source color's hue does not appear in the theme. */ 23 | public class SchemeFruitSalad extends DynamicScheme { 24 | public SchemeFruitSalad(Hct sourceColorHct, boolean isDark, double contrastLevel) { 25 | super( 26 | sourceColorHct, 27 | Variant.FRUIT_SALAD, 28 | isDark, 29 | contrastLevel, 30 | TonalPalette.fromHueAndChroma( 31 | MathUtils.sanitizeDegreesDouble(sourceColorHct.getHue() - 50.0), 48.0), 32 | TonalPalette.fromHueAndChroma( 33 | MathUtils.sanitizeDegreesDouble(sourceColorHct.getHue() - 50.0), 36.0), 34 | TonalPalette.fromHueAndChroma(sourceColorHct.getHue(), 36.0), 35 | TonalPalette.fromHueAndChroma(sourceColorHct.getHue(), 10.0), 36 | TonalPalette.fromHueAndChroma(sourceColorHct.getHue(), 16.0)); 37 | } 38 | } -------------------------------------------------------------------------------- /m3utils/src/main/java/scheme/SchemeMonochrome.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2022 Google LLC 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | package scheme; 18 | 19 | import hct.Hct; 20 | import palettes.TonalPalette; 21 | 22 | /** A monochrome theme, colors are purely black / white / gray. */ 23 | public class SchemeMonochrome extends DynamicScheme { 24 | public SchemeMonochrome(Hct sourceColorHct, boolean isDark, double contrastLevel) { 25 | super( 26 | sourceColorHct, 27 | Variant.MONOCHROME, 28 | isDark, 29 | contrastLevel, 30 | TonalPalette.fromHueAndChroma(sourceColorHct.getHue(), 0.0), 31 | TonalPalette.fromHueAndChroma(sourceColorHct.getHue(), 0.0), 32 | TonalPalette.fromHueAndChroma(sourceColorHct.getHue(), 0.0), 33 | TonalPalette.fromHueAndChroma(sourceColorHct.getHue(), 0.0), 34 | TonalPalette.fromHueAndChroma(sourceColorHct.getHue(), 0.0)); 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /m3utils/src/main/java/scheme/SchemeNeutral.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2022 Google LLC 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | package scheme; 17 | 18 | import hct.Hct; 19 | import palettes.TonalPalette; 20 | 21 | /** A theme that's slightly more chromatic than monochrome, which is purely black / white / gray. */ 22 | public class SchemeNeutral extends DynamicScheme { 23 | public SchemeNeutral(Hct sourceColorHct, boolean isDark, double contrastLevel) { 24 | super( 25 | sourceColorHct, 26 | Variant.NEUTRAL, 27 | isDark, 28 | contrastLevel, 29 | TonalPalette.fromHueAndChroma(sourceColorHct.getHue(), 12.0), 30 | TonalPalette.fromHueAndChroma(sourceColorHct.getHue(), 8.0), 31 | TonalPalette.fromHueAndChroma(sourceColorHct.getHue(), 16.0), 32 | TonalPalette.fromHueAndChroma(sourceColorHct.getHue(), 2.0), 33 | TonalPalette.fromHueAndChroma(sourceColorHct.getHue(), 2.0)); 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /m3utils/src/main/java/scheme/SchemeRainbow.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2023 Google LLC 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | package scheme; 17 | 18 | import hct.Hct; 19 | import palettes.TonalPalette; 20 | import utils.MathUtils; 21 | 22 | /** A playful theme - the source color's hue does not appear in the theme. */ 23 | public class SchemeRainbow extends DynamicScheme { 24 | public SchemeRainbow(Hct sourceColorHct, boolean isDark, double contrastLevel) { 25 | super( 26 | sourceColorHct, 27 | Variant.RAINBOW, 28 | isDark, 29 | contrastLevel, 30 | TonalPalette.fromHueAndChroma(sourceColorHct.getHue(), 48.0), 31 | TonalPalette.fromHueAndChroma(sourceColorHct.getHue(), 16.0), 32 | TonalPalette.fromHueAndChroma( 33 | MathUtils.sanitizeDegreesDouble(sourceColorHct.getHue() + 60.0), 24.0), 34 | TonalPalette.fromHueAndChroma(sourceColorHct.getHue(), 0.0), 35 | TonalPalette.fromHueAndChroma(sourceColorHct.getHue(), 0.0)); 36 | } 37 | } -------------------------------------------------------------------------------- /m3utils/src/main/java/scheme/SchemeTonalSpot.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2022 Google LLC 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | package scheme; 17 | 18 | import hct.Hct; 19 | import palettes.TonalPalette; 20 | import utils.MathUtils; 21 | 22 | /** A calm theme, sedated colors that aren't particularly chromatic. */ 23 | public class SchemeTonalSpot extends DynamicScheme { 24 | public SchemeTonalSpot(Hct sourceColorHct, boolean isDark, double contrastLevel) { 25 | super( 26 | sourceColorHct, 27 | Variant.TONAL_SPOT, 28 | isDark, 29 | contrastLevel, 30 | TonalPalette.fromHueAndChroma(sourceColorHct.getHue(), 36.0), 31 | TonalPalette.fromHueAndChroma(sourceColorHct.getHue(), 16.0), 32 | TonalPalette.fromHueAndChroma( 33 | MathUtils.sanitizeDegreesDouble(sourceColorHct.getHue() + 60.0), 24.0), 34 | TonalPalette.fromHueAndChroma(sourceColorHct.getHue(), 6.0), 35 | TonalPalette.fromHueAndChroma(sourceColorHct.getHue(), 8.0)); 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /m3utils/src/main/java/scheme/SchemeVibrant.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2022 Google LLC 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | package scheme; 17 | 18 | import hct.Hct; 19 | import palettes.TonalPalette; 20 | 21 | /** A loud theme, colorfulness is maximum for Primary palette, increased for others. */ 22 | public class SchemeVibrant extends DynamicScheme { 23 | private static final double[] HUES = {0, 41, 61, 101, 131, 181, 251, 301, 360}; 24 | private static final double[] SECONDARY_ROTATIONS = {18, 15, 10, 12, 15, 18, 15, 12, 12}; 25 | private static final double[] TERTIARY_ROTATIONS = {35, 30, 20, 25, 30, 35, 30, 25, 25}; 26 | 27 | public SchemeVibrant(Hct sourceColorHct, boolean isDark, double contrastLevel) { 28 | super( 29 | sourceColorHct, 30 | Variant.VIBRANT, 31 | isDark, 32 | contrastLevel, 33 | TonalPalette.fromHueAndChroma(sourceColorHct.getHue(), 200.0), 34 | TonalPalette.fromHueAndChroma( 35 | DynamicScheme.getRotatedHue(sourceColorHct, HUES, SECONDARY_ROTATIONS), 24.0), 36 | TonalPalette.fromHueAndChroma( 37 | DynamicScheme.getRotatedHue(sourceColorHct, HUES, TERTIARY_ROTATIONS), 32.0), 38 | TonalPalette.fromHueAndChroma(sourceColorHct.getHue(), 10.0), 39 | TonalPalette.fromHueAndChroma(sourceColorHct.getHue(), 12.0)); 40 | } 41 | } 42 | -------------------------------------------------------------------------------- /m3utils/src/main/java/scheme/Variant.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2022 Google LLC 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | package scheme; 17 | 18 | /** Themes for Dynamic Color. */ 19 | public enum Variant { 20 | MONOCHROME, 21 | NEUTRAL, 22 | TONAL_SPOT, 23 | VIBRANT, 24 | EXPRESSIVE, 25 | FIDELITY, 26 | CONTENT, 27 | RAINBOW, 28 | FRUIT_SALAD 29 | } 30 | -------------------------------------------------------------------------------- /m3utils/src/main/java/utils/StringUtils.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2021 Google LLC 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | package utils; 18 | 19 | /** Utility methods for string representations of colors. */ 20 | final class StringUtils { 21 | private StringUtils() {} 22 | 23 | /** 24 | * Hex string representing color, ex. #ff0000 for red. 25 | * 26 | * @param argb ARGB representation of a color. 27 | */ 28 | public static String hexFromArgb(int argb) { 29 | int red = ColorUtils.redFromArgb(argb); 30 | int blue = ColorUtils.blueFromArgb(argb); 31 | int green = ColorUtils.greenFromArgb(argb); 32 | return String.format("#%02x%02x%02x", red, green, blue); 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /privacy-policy.md: -------------------------------------------------------------------------------- 1 | 2 | # Privacy Policy 3 | 4 | **General** 5 | Zen Music Player is developed and maintained by only one developer. 6 | This Privacy Policy aims to describe how information obtained from users is collected, used and disclosed. 7 | By using Zen, you agree that your personal information will be handled as described in this Policy. 8 | 9 | **Information being collected** 10 | For better user experience, Zen collects log data in cases of error or app crashes and reports them automatically (**Users can opt out of automatic reporting from app's settings**). 11 | Log data may include information such as the brand and model of device used, the operating system version and other statistics. 12 | Zen uses Firebase Crashlytics for enabling the log reporting service. Read about it [here](https://firebase.google.com/support/privacy) and [here](https://firebase.google.com/terms/crashlytics-app-distribution-data-processing-terms). 13 | 14 | **How is the information used** 15 | The reported log data is used to fix bugs or any other technical issues that may occur and improve Zen. 16 | 17 | **Disclosure of your information** 18 | We do not sell, trade, or otherwise transfer your information to outside parties. We may share your information with trusted third-party service providers such as Firebase Crashlytics. 19 | 20 | **Your Choices** 21 | You may opt out of the automatic reporting of crash logs data through Firebase Crashlytics by disabling the feature in the app's settings. 22 | However, please note that this may impact our ability to provide certain features and services. 23 | 24 | **Changes to the Policy** 25 | If the Policy changes, the modification date below will be updated. The Policy may change from time to time, so please be sure to check back periodically. 26 | 27 | **Last modified: 21 March, 2023** 28 | 29 | **Contact** 30 | If you have any questions about the Policy, please contact me via music.zen@outlook.com -------------------------------------------------------------------------------- /screenshots/Dark/album_collection.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/pakka-papad/Zen/3111a3b1db486e80cb11a96a65c8f9ab9b850ecb/screenshots/Dark/album_collection.jpg -------------------------------------------------------------------------------- /screenshots/Dark/albums.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/pakka-papad/Zen/3111a3b1db486e80cb11a96a65c8f9ab9b850ecb/screenshots/Dark/albums.jpg -------------------------------------------------------------------------------- /screenshots/Dark/all_songs_default.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/pakka-papad/Zen/3111a3b1db486e80cb11a96a65c8f9ab9b850ecb/screenshots/Dark/all_songs_default.jpg -------------------------------------------------------------------------------- /screenshots/Dark/all_songs_elm.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/pakka-papad/Zen/3111a3b1db486e80cb11a96a65c8f9ab9b850ecb/screenshots/Dark/all_songs_elm.jpg -------------------------------------------------------------------------------- /screenshots/Dark/all_songs_jacksonspurple.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/pakka-papad/Zen/3111a3b1db486e80cb11a96a65c8f9ab9b850ecb/screenshots/Dark/all_songs_jacksonspurple.jpg -------------------------------------------------------------------------------- /screenshots/Dark/all_songs_magenta.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/pakka-papad/Zen/3111a3b1db486e80cb11a96a65c8f9ab9b850ecb/screenshots/Dark/all_songs_magenta.jpg -------------------------------------------------------------------------------- /screenshots/Dark/all_songs_malibu.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/pakka-papad/Zen/3111a3b1db486e80cb11a96a65c8f9ab9b850ecb/screenshots/Dark/all_songs_malibu.jpg -------------------------------------------------------------------------------- /screenshots/Dark/all_songs_melrose.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/pakka-papad/Zen/3111a3b1db486e80cb11a96a65c8f9ab9b850ecb/screenshots/Dark/all_songs_melrose.jpg -------------------------------------------------------------------------------- /screenshots/Dark/artists.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/pakka-papad/Zen/3111a3b1db486e80cb11a96a65c8f9ab9b850ecb/screenshots/Dark/artists.jpg -------------------------------------------------------------------------------- /screenshots/Dark/genres.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/pakka-papad/Zen/3111a3b1db486e80cb11a96a65c8f9ab9b850ecb/screenshots/Dark/genres.jpg -------------------------------------------------------------------------------- /screenshots/Dark/now_playing_magenta.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/pakka-papad/Zen/3111a3b1db486e80cb11a96a65c8f9ab9b850ecb/screenshots/Dark/now_playing_magenta.jpg -------------------------------------------------------------------------------- /screenshots/Dark/now_playing_malibu.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/pakka-papad/Zen/3111a3b1db486e80cb11a96a65c8f9ab9b850ecb/screenshots/Dark/now_playing_malibu.jpg -------------------------------------------------------------------------------- /screenshots/Dark/now_playing_melrose.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/pakka-papad/Zen/3111a3b1db486e80cb11a96a65c8f9ab9b850ecb/screenshots/Dark/now_playing_melrose.jpg -------------------------------------------------------------------------------- /screenshots/Dark/playlists.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/pakka-papad/Zen/3111a3b1db486e80cb11a96a65c8f9ab9b850ecb/screenshots/Dark/playlists.jpg -------------------------------------------------------------------------------- /screenshots/Dark/search.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/pakka-papad/Zen/3111a3b1db486e80cb11a96a65c8f9ab9b850ecb/screenshots/Dark/search.jpg -------------------------------------------------------------------------------- /screenshots/Dark/storage_access.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/pakka-papad/Zen/3111a3b1db486e80cb11a96a65c8f9ab9b850ecb/screenshots/Dark/storage_access.jpg -------------------------------------------------------------------------------- /screenshots/Dark/storage_scan.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/pakka-papad/Zen/3111a3b1db486e80cb11a96a65c8f9ab9b850ecb/screenshots/Dark/storage_scan.jpg -------------------------------------------------------------------------------- /screenshots/Light/album_collection.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/pakka-papad/Zen/3111a3b1db486e80cb11a96a65c8f9ab9b850ecb/screenshots/Light/album_collection.jpg -------------------------------------------------------------------------------- /screenshots/Light/albums.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/pakka-papad/Zen/3111a3b1db486e80cb11a96a65c8f9ab9b850ecb/screenshots/Light/albums.jpg -------------------------------------------------------------------------------- /screenshots/Light/all_songs_default.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/pakka-papad/Zen/3111a3b1db486e80cb11a96a65c8f9ab9b850ecb/screenshots/Light/all_songs_default.jpg -------------------------------------------------------------------------------- /screenshots/Light/all_songs_elm.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/pakka-papad/Zen/3111a3b1db486e80cb11a96a65c8f9ab9b850ecb/screenshots/Light/all_songs_elm.jpg -------------------------------------------------------------------------------- /screenshots/Light/all_songs_jacksonspurple.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/pakka-papad/Zen/3111a3b1db486e80cb11a96a65c8f9ab9b850ecb/screenshots/Light/all_songs_jacksonspurple.jpg -------------------------------------------------------------------------------- /screenshots/Light/all_songs_magenta.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/pakka-papad/Zen/3111a3b1db486e80cb11a96a65c8f9ab9b850ecb/screenshots/Light/all_songs_magenta.jpg -------------------------------------------------------------------------------- /screenshots/Light/all_songs_malibu.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/pakka-papad/Zen/3111a3b1db486e80cb11a96a65c8f9ab9b850ecb/screenshots/Light/all_songs_malibu.jpg -------------------------------------------------------------------------------- /screenshots/Light/all_songs_melrose.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/pakka-papad/Zen/3111a3b1db486e80cb11a96a65c8f9ab9b850ecb/screenshots/Light/all_songs_melrose.jpg -------------------------------------------------------------------------------- /screenshots/Light/artists.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/pakka-papad/Zen/3111a3b1db486e80cb11a96a65c8f9ab9b850ecb/screenshots/Light/artists.jpg -------------------------------------------------------------------------------- /screenshots/Light/genres.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/pakka-papad/Zen/3111a3b1db486e80cb11a96a65c8f9ab9b850ecb/screenshots/Light/genres.jpg -------------------------------------------------------------------------------- /screenshots/Light/now_playing_magenta.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/pakka-papad/Zen/3111a3b1db486e80cb11a96a65c8f9ab9b850ecb/screenshots/Light/now_playing_magenta.jpg -------------------------------------------------------------------------------- /screenshots/Light/now_playing_malibu.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/pakka-papad/Zen/3111a3b1db486e80cb11a96a65c8f9ab9b850ecb/screenshots/Light/now_playing_malibu.jpg -------------------------------------------------------------------------------- /screenshots/Light/now_playing_melrose.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/pakka-papad/Zen/3111a3b1db486e80cb11a96a65c8f9ab9b850ecb/screenshots/Light/now_playing_melrose.jpg -------------------------------------------------------------------------------- /screenshots/Light/playlists.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/pakka-papad/Zen/3111a3b1db486e80cb11a96a65c8f9ab9b850ecb/screenshots/Light/playlists.jpg -------------------------------------------------------------------------------- /screenshots/Light/search.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/pakka-papad/Zen/3111a3b1db486e80cb11a96a65c8f9ab9b850ecb/screenshots/Light/search.jpg -------------------------------------------------------------------------------- /screenshots/Light/storage_access.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/pakka-papad/Zen/3111a3b1db486e80cb11a96a65c8f9ab9b850ecb/screenshots/Light/storage_access.jpg -------------------------------------------------------------------------------- /screenshots/Light/storage_scan.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/pakka-papad/Zen/3111a3b1db486e80cb11a96a65c8f9ab9b850ecb/screenshots/Light/storage_scan.jpg -------------------------------------------------------------------------------- /screenshots/banner.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/pakka-papad/Zen/3111a3b1db486e80cb11a96a65c8f9ab9b850ecb/screenshots/banner.png -------------------------------------------------------------------------------- /settings.gradle.kts: -------------------------------------------------------------------------------- 1 | pluginManagement { 2 | repositories { 3 | gradlePluginPortal() 4 | google() 5 | mavenCentral() 6 | } 7 | } 8 | dependencyResolutionManagement { 9 | repositoriesMode.set(RepositoriesMode.FAIL_ON_PROJECT_REPOS) 10 | repositories { 11 | google() 12 | mavenCentral() 13 | } 14 | } 15 | rootProject.name = "Zen" 16 | include (":app") 17 | include(":m3utils") 18 | --------------------------------------------------------------------------------