├── .github ├── ISSUE_TEMPLATE │ ├── bug-report.yml │ └── feature-request.yml ├── PULL_REQUEST_TEMPLATE │ └── pull_request_template.md ├── dependabot.yml └── workflows │ ├── canary.yml │ ├── code-analysis.yml │ ├── fastlane-changelogs-analysis.yml │ ├── i18n-summary.yml │ ├── nightly.yml │ ├── stable.yml │ └── website.yml ├── .gitignore ├── .gitmodules ├── .idea ├── codeStyles │ ├── Project.xml │ └── codeStyleConfig.xml └── dictionaries │ └── zyrouge.xml ├── .phrasey ├── additional-locales.json ├── config.toml ├── hooks │ ├── kt-sync.js │ ├── locales.js │ └── utils.js └── schema.toml ├── .prettierrc ├── .run └── Test Metaphony.run.xml ├── .vscode └── settings.json ├── LICENSE ├── README.md ├── app ├── .gitignore ├── build.gradle.kts ├── proguard-rules.pro ├── room-schemas │ ├── io.github.zyrouge.symphony.services.database.CacheDatabase │ │ ├── 1.json │ │ └── 2.json │ └── io.github.zyrouge.symphony.services.database.PersistentDatabase │ │ └── 1.json └── src │ ├── canary │ └── res │ │ └── values │ │ └── strings.xml │ ├── debug │ └── res │ │ └── values │ │ └── strings.xml │ ├── main │ ├── AndroidManifest.xml │ ├── ic_launcher-playstore.png │ ├── java │ │ └── io │ │ │ └── github │ │ │ └── zyrouge │ │ │ └── symphony │ │ │ ├── ActivityIgnition.kt │ │ │ ├── ErrorActivity.kt │ │ │ ├── MainActivity.kt │ │ │ ├── Symphony.kt │ │ │ ├── services │ │ │ ├── AppMeta.kt │ │ │ ├── Permissions.kt │ │ │ ├── Settings.kt │ │ │ ├── database │ │ │ │ ├── CacheDatabase.kt │ │ │ │ ├── Database.kt │ │ │ │ ├── PersistentDatabase.kt │ │ │ │ ├── adapters │ │ │ │ │ ├── FileDatabaseAdapter.kt │ │ │ │ │ ├── FileTreeDatabaseAdapter.kt │ │ │ │ │ └── SQLiteKeyValueDatabaseAdapter.kt │ │ │ │ └── store │ │ │ │ │ ├── ArtworkCacheStore.kt │ │ │ │ │ ├── LyricsCacheStore.kt │ │ │ │ │ ├── PlaylistStore.kt │ │ │ │ │ └── SongCacheStore.kt │ │ │ ├── groove │ │ │ │ ├── Album.kt │ │ │ │ ├── AlbumArtist.kt │ │ │ │ ├── Artist.kt │ │ │ │ ├── Genre.kt │ │ │ │ ├── Groove.kt │ │ │ │ ├── MediaExposer.kt │ │ │ │ ├── Playlist.kt │ │ │ │ ├── Song.kt │ │ │ │ └── repositories │ │ │ │ │ ├── AlbumArtistRepository.kt │ │ │ │ │ ├── AlbumRepository.kt │ │ │ │ │ ├── ArtistRepository.kt │ │ │ │ │ ├── GenreRepository.kt │ │ │ │ │ ├── PlaylistRepository.kt │ │ │ │ │ └── SongRepository.kt │ │ │ ├── i18n │ │ │ │ ├── CommonTranslation.kt │ │ │ │ ├── Translation.kt │ │ │ │ ├── Translations.kt │ │ │ │ └── Translator.kt │ │ │ └── radio │ │ │ │ ├── Radio.kt │ │ │ │ ├── RadioArtworkCacher.kt │ │ │ │ ├── RadioEffects.kt │ │ │ │ ├── RadioFocus.kt │ │ │ │ ├── RadioNativeReceiver.kt │ │ │ │ ├── RadioNotification.kt │ │ │ │ ├── RadioNotificationManager.kt │ │ │ │ ├── RadioNotificationService.kt │ │ │ │ ├── RadioObservatory.kt │ │ │ │ ├── RadioPlayer.kt │ │ │ │ ├── RadioQueue.kt │ │ │ │ ├── RadioSession.kt │ │ │ │ └── RadioShorty.kt │ │ │ ├── ui │ │ │ ├── components │ │ │ │ ├── AddToPlaylistDialog.kt │ │ │ │ ├── AlbumArtistGrid.kt │ │ │ │ ├── AlbumArtistTile.kt │ │ │ │ ├── AlbumGrid.kt │ │ │ │ ├── AlbumRow.kt │ │ │ │ ├── AlbumTile.kt │ │ │ │ ├── ArtistGrid.kt │ │ │ │ ├── ArtistTile.kt │ │ │ │ ├── ConfirmationDialog.kt │ │ │ │ ├── ContentDrawScopeScrollBar.kt │ │ │ │ ├── ErrorComp.kt │ │ │ │ ├── GenericGrooveBanner.kt │ │ │ │ ├── GenericGrooveCard.kt │ │ │ │ ├── GenericSongListDropdown.kt │ │ │ │ ├── GenreGrid.kt │ │ │ │ ├── IconButtonPlaceholder.kt │ │ │ │ ├── IconTextBody.kt │ │ │ │ ├── InformationDialog.kt │ │ │ │ ├── IntroductoryDialog.kt │ │ │ │ ├── KeepScreenAwake.kt │ │ │ │ ├── LazyColumnScrollBar.kt │ │ │ │ ├── LazyGridScrollBar.kt │ │ │ │ ├── LoaderScaffold.kt │ │ │ │ ├── LongPressCopyableText.kt │ │ │ │ ├── LyricsText.kt │ │ │ │ ├── MediaSortBar.kt │ │ │ │ ├── MediaSortBarScaffold.kt │ │ │ │ ├── NewPlaylistDialog.kt │ │ │ │ ├── NowPlayingBottomBar.kt │ │ │ │ ├── PlaylistGrid.kt │ │ │ │ ├── PlaylistInformationDialog.kt │ │ │ │ ├── PlaylistManageSongsDialog.kt │ │ │ │ ├── PlaylistTile.kt │ │ │ │ ├── RenamePlaylistDialog.kt │ │ │ │ ├── ResponsiveGrid.kt │ │ │ │ ├── ScaffoldDialog.kt │ │ │ │ ├── Slider.kt │ │ │ │ ├── Snackbar.kt │ │ │ │ ├── SongCard.kt │ │ │ │ ├── SongExplorerList.kt │ │ │ │ ├── SongInformationDialog.kt │ │ │ │ ├── SongList.kt │ │ │ │ ├── SongTreeList.kt │ │ │ │ ├── SquareGrooveTile.kt │ │ │ │ ├── SubtleCaptionText.kt │ │ │ │ ├── Swipeable.kt │ │ │ │ ├── TimedContentText.kt │ │ │ │ ├── TopAppBarMinimalTitle.kt │ │ │ │ └── settings │ │ │ │ │ ├── ConsiderContributingTile.kt │ │ │ │ │ ├── FloatInputTile.kt │ │ │ │ │ ├── LinkTile.kt │ │ │ │ │ ├── MultiGrooveFolderTile.kt │ │ │ │ │ ├── MultiOptionTile.kt │ │ │ │ │ ├── MultiSystemFolderTile.kt │ │ │ │ │ ├── MultiTextOptionTile.kt │ │ │ │ │ ├── OptionTile.kt │ │ │ │ │ ├── SideHeading.kt │ │ │ │ │ ├── SimpleTile.kt │ │ │ │ │ ├── SliderTile.kt │ │ │ │ │ ├── SwitchTile.kt │ │ │ │ │ ├── TextInputTile.kt │ │ │ │ │ └── Tile.kt │ │ │ ├── helpers │ │ │ │ ├── Assets.kt │ │ │ │ ├── Context.kt │ │ │ │ ├── SimpleFileSystem.kt │ │ │ │ ├── Transitions.kt │ │ │ │ └── UserInterface.kt │ │ │ ├── theme │ │ │ │ ├── Color.kt │ │ │ │ ├── ColorScheme.kt │ │ │ │ ├── Theme.kt │ │ │ │ └── Typography.kt │ │ │ └── view │ │ │ │ ├── Album.kt │ │ │ │ ├── AlbumArtist.kt │ │ │ │ ├── Artist.kt │ │ │ │ ├── Base.kt │ │ │ │ ├── Genre.kt │ │ │ │ ├── Home.kt │ │ │ │ ├── Lyrics.kt │ │ │ │ ├── NowPlaying.kt │ │ │ │ ├── Playlist.kt │ │ │ │ ├── Queue.kt │ │ │ │ ├── Search.kt │ │ │ │ ├── Settings.kt │ │ │ │ ├── home │ │ │ │ ├── AlbumArtists.kt │ │ │ │ ├── Albums.kt │ │ │ │ ├── Artists.kt │ │ │ │ ├── Browser.kt │ │ │ │ ├── Folders.kt │ │ │ │ ├── ForYou.kt │ │ │ │ ├── Genres.kt │ │ │ │ ├── Playlists.kt │ │ │ │ ├── Songs.kt │ │ │ │ └── Tree.kt │ │ │ │ ├── nowPlaying │ │ │ │ ├── AppBar.kt │ │ │ │ ├── Body.kt │ │ │ │ ├── BodyContent.kt │ │ │ │ ├── BodyCover.kt │ │ │ │ ├── BottomBar.kt │ │ │ │ ├── NothingPlaying.kt │ │ │ │ ├── PitchDialog.kt │ │ │ │ ├── SleepTimerDialog.kt │ │ │ │ └── SpeedDialog.kt │ │ │ │ └── settings │ │ │ │ ├── AppearanceSettingsView.kt │ │ │ │ ├── GrooveSettingsView.kt │ │ │ │ ├── HomePageSettingsView.kt │ │ │ │ ├── MiniPlayerSettingsView.kt │ │ │ │ ├── NowPlayingSettingsView.kt │ │ │ │ ├── PlayerSettingsView.kt │ │ │ │ └── UpdateSettingsView.kt │ │ │ └── utils │ │ │ ├── ActivityUtils.kt │ │ │ ├── DocumentFileX.kt │ │ │ ├── DurationUtils.kt │ │ │ ├── Eventer.kt │ │ │ ├── Float.kt │ │ │ ├── Fuzzy.kt │ │ │ ├── Http.kt │ │ │ ├── ImagePreserver.kt │ │ │ ├── KeyGenerator.kt │ │ │ ├── List.kt │ │ │ ├── Logger.kt │ │ │ ├── RangeUtils.kt │ │ │ ├── RoomConvertors.kt │ │ │ ├── Run.kt │ │ │ ├── Set.kt │ │ │ ├── SimpleFileSystem.kt │ │ │ ├── SimplePath.kt │ │ │ ├── StringListUtils.kt │ │ │ ├── StringUtils.kt │ │ │ └── TimedContent.kt │ └── res │ │ ├── drawable │ │ ├── ic_launcher_foreground.xml │ │ ├── ic_launcher_monochrome.xml │ │ ├── material_icon_close.xml │ │ ├── material_icon_music_note.xml │ │ ├── material_icon_pause.xml │ │ ├── material_icon_play.xml │ │ ├── material_icon_skip_next.xml │ │ ├── material_icon_skip_previous.xml │ │ └── material_icon_stop.xml │ │ ├── font │ │ ├── dmsans_bold.ttf │ │ ├── dmsans_regular.ttf │ │ ├── inter_bold.ttf │ │ ├── inter_regular.ttf │ │ ├── poppins_bold.ttf │ │ ├── poppins_regular.ttf │ │ ├── productsans_bold.ttf │ │ ├── productsans_regular.ttf │ │ ├── roboto_bold.ttf │ │ └── roboto_regular.ttf │ │ ├── mipmap-anydpi │ │ ├── ic_launcher.xml │ │ └── ic_launcher_round.xml │ │ ├── mipmap-hdpi │ │ ├── ic_launcher.webp │ │ └── ic_launcher_round.webp │ │ ├── mipmap-mdpi │ │ ├── ic_launcher.webp │ │ └── ic_launcher_round.webp │ │ ├── mipmap-xhdpi │ │ ├── ic_launcher.webp │ │ └── ic_launcher_round.webp │ │ ├── mipmap-xxhdpi │ │ ├── ic_launcher.webp │ │ └── ic_launcher_round.webp │ │ ├── mipmap-xxxhdpi │ │ ├── ic_launcher.webp │ │ └── ic_launcher_round.webp │ │ ├── raw │ │ ├── placeholder_dark.png │ │ └── placeholder_light.png │ │ ├── values │ │ ├── ic_launcher_background.xml │ │ ├── splash.xml │ │ ├── strings.xml │ │ └── themes.xml │ │ └── xml │ │ ├── backup_rules.xml │ │ └── data_extraction_rules.xml │ └── test │ └── resources │ ├── audio-id3v2.3.mp3 │ ├── audio-id3v2.4.mp3 │ ├── audio.flac │ ├── audio.m4a │ ├── audio.mp3 │ └── audio.ogg ├── build.gradle.kts ├── cli ├── android │ └── move-outputs.ts ├── changelogs │ └── fastlane-character-limit.ts ├── git │ ├── diff-files-yn.ts │ ├── latest-tag.ts │ ├── tag-exists-yn.ts │ └── tag-exists.ts ├── helpers │ ├── git.ts │ ├── paths.ts │ ├── shortcuts.ts │ └── version.ts ├── i18n │ └── summary.ts └── version │ ├── bump.ts │ ├── print-canary.ts │ ├── print-nightly.ts │ └── print.ts ├── gradle.properties ├── gradle ├── libs.versions.toml └── wrapper │ ├── gradle-wrapper.jar │ └── gradle-wrapper.properties ├── gradlew ├── gradlew.bat ├── i18n ├── be.toml ├── de.toml ├── en.toml ├── es.toml ├── fa.toml ├── fi.toml ├── fr.toml ├── it.toml ├── ja.toml ├── pl.toml ├── pt.toml ├── ro.toml ├── ru.toml ├── ryu.toml ├── tr.toml ├── uk.toml ├── vi.toml └── zh-Hans.toml ├── media ├── banner-16-9-compact.png ├── banner-16-9.png ├── banner.png ├── icon-gray-light.png ├── icon-gray.png ├── icon-opaque-inverted.png ├── icon-opaque.png ├── icon.png ├── icon.svg ├── playstore-feature-graphic.png ├── screenshot-graphic-1.png ├── screenshot-graphic-2.png ├── screenshot-graphic-3.png ├── screenshot-graphic-4.png └── screenshots.png ├── metadata ├── en-US │ ├── changelogs │ │ ├── 104.txt │ │ ├── 105.txt │ │ ├── 106.txt │ │ ├── 107.txt │ │ ├── 108.txt │ │ ├── 109.txt │ │ ├── 110.txt │ │ ├── 112.txt │ │ ├── 113.txt │ │ ├── 115.txt │ │ ├── 71.txt │ │ ├── 79.txt │ │ ├── 89.txt │ │ ├── 92.txt │ │ └── 93.txt │ ├── full_description.txt │ ├── images │ │ ├── featureGraphic.png │ │ ├── icon.png │ │ └── phoneScreenshots │ │ │ ├── screenshot-album-artists.png │ │ │ ├── screenshot-folders.png │ │ │ ├── screenshot-for-you.png │ │ │ ├── screenshot-genres.png │ │ │ ├── screenshot-now-playing.png │ │ │ ├── screenshot-queue.png │ │ │ ├── screenshot-songs.png │ │ │ └── screenshot-tree.png │ ├── short_description.txt │ └── title.txt └── ja-JP │ ├── changelogs │ ├── 104.txt │ ├── 105.txt │ ├── 106.txt │ ├── 107.txt │ ├── 108.txt │ ├── 109.txt │ ├── 71.txt │ ├── 79.txt │ ├── 89.txt │ ├── 92.txt │ └── 93.txt │ ├── full_description.txt │ ├── images │ └── phoneScreenshots │ │ ├── a │ │ └── screenshot-for-you.png │ ├── short_description.txt │ └── title.txt ├── metaphony ├── .gitignore ├── build.gradle.kts ├── consumer-rules.pro ├── proguard-rules.pro └── src │ ├── androidTest │ └── java │ │ └── me │ │ └── zyrouge │ │ └── symphony │ │ └── metaphony │ │ └── AudioMetadataParserTest.kt │ └── main │ ├── AndroidManifest.xml │ ├── assets │ ├── audio-id3v2.3.mp3 │ ├── audio-id3v2.4.mp3 │ ├── audio.flac │ └── audio.mp3 │ ├── cpp │ ├── AudioMetadataParser.cpp │ ├── CMakeLists.txt │ ├── TagLibHelper.cpp │ └── TagLibHelper.h │ └── java │ └── me │ └── zyrouge │ └── symphony │ └── metaphony │ ├── AudioMetadata.kt │ └── AudioMetadataParser.kt ├── package-lock.json ├── package.json ├── secrets ├── secrets.txt └── signing_key.jks ├── settings.gradle.kts └── tsconfig.json /.github/ISSUE_TEMPLATE/bug-report.yml: -------------------------------------------------------------------------------- 1 | name: Bug Report 2 | title: "[Bug] " 3 | description: Use this to file a bug report. 4 | labels: ["type: bug"] 5 | 6 | body: 7 | - type: textarea 8 | attributes: 9 | label: Description 10 | description: A clear and concise description of what the bug is. 11 | validations: 12 | required: true 13 | 14 | - type: textarea 15 | attributes: 16 | label: Steps to Reproduce 17 | placeholder: | 18 | 1. Go to '...' 19 | 2. Click on '....' 20 | 3. Scroll down to '....' 21 | 4. See error 22 | validations: 23 | required: true 24 | 25 | - type: textarea 26 | attributes: 27 | label: Expected Behavior 28 | description: A clear and concise description of what you expected to happen. 29 | validations: 30 | required: true 31 | 32 | - type: textarea 33 | attributes: 34 | label: Screenshots 35 | description: If applicable, add screenshots to help explain your problem. 36 | validations: 37 | required: false 38 | 39 | - type: textarea 40 | attributes: 41 | label: Additional Context 42 | description: Add any other context about the problem here. 43 | validations: 44 | required: false 45 | 46 | - type: input 47 | attributes: 48 | label: Device 49 | placeholder: Google Pixel 6 50 | validations: 51 | required: false 52 | 53 | - type: input 54 | attributes: 55 | label: OS 56 | placeholder: Android 13 57 | validations: 58 | required: true 59 | 60 | - type: input 61 | attributes: 62 | label: Version 63 | placeholder: "2023.04.20" 64 | validations: 65 | required: true 66 | 67 | - type: checkboxes 68 | attributes: 69 | label: Contribution Guidelines 70 | options: 71 | - label: I agree to follow the [Contribution Guidelines](https://github.com/zyrouge/symphony/wiki/Contributions-Guidelines#issues). 72 | required: true 73 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/feature-request.yml: -------------------------------------------------------------------------------- 1 | name: Feature Request 2 | title: "[Feature] <title>" 3 | description: Use this to submit a feature request. 4 | labels: ["type: enhancement"] 5 | 6 | body: 7 | - type: textarea 8 | attributes: 9 | label: Description 10 | description: A clear and concise description of what the problem is. 11 | validations: 12 | required: true 13 | 14 | - type: textarea 15 | attributes: 16 | label: Solution 17 | description: A clear and concise description of what you want to happen. 18 | validations: 19 | required: true 20 | 21 | - type: textarea 22 | attributes: 23 | label: Alternatives 24 | description: A clear and concise description of any alternative solutions or features you've considered. 25 | validations: 26 | required: false 27 | 28 | - type: textarea 29 | attributes: 30 | label: Additional Context 31 | description: Add any other context or screenshots about the feature request here. 32 | validations: 33 | required: false 34 | 35 | - type: checkboxes 36 | attributes: 37 | label: Contribution Guidelines 38 | options: 39 | - label: I agree to follow the [Contribution Guidelines](https://github.com/zyrouge/symphony/wiki/Contributions-Guidelines#issues). 40 | required: true 41 | -------------------------------------------------------------------------------- /.github/PULL_REQUEST_TEMPLATE/pull_request_template.md: -------------------------------------------------------------------------------- 1 | ### Description 2 | 3 | A clear and concise description of what the pull request is for. 4 | 5 | Fixes #issue_number. 6 | 7 | ### Type of Change 8 | 9 | - [ ] Bug fix (non-breaking change which fixes an issue) 10 | - [ ] New feature (non-breaking change which adds functionality) 11 | - [ ] Breaking change (fix or feature that would cause existing functionality to not work as expected) 12 | 13 | ### Checklist 14 | 15 | <!-- change [ ] to [x] to mark the checkbox --> 16 | - [ ] I have read the [Contribution Guidelines](https://github.com/zyrouge/symphony/wiki/Contributions-Guidelines#pull-requests). 17 | -------------------------------------------------------------------------------- /.github/dependabot.yml: -------------------------------------------------------------------------------- 1 | version: 2 2 | updates: 3 | - package-ecosystem: "github-actions" 4 | directory: "/" 5 | schedule: 6 | interval: "daily" 7 | - package-ecosystem: "gradle" 8 | directory: "/" 9 | schedule: 10 | interval: "daily" 11 | - package-ecosystem: "npm" 12 | directory: "/" 13 | schedule: 14 | interval: "daily" 15 | -------------------------------------------------------------------------------- /.github/workflows/code-analysis.yml: -------------------------------------------------------------------------------- 1 | name: Code Analysis 2 | 3 | on: 4 | push: 5 | branches-ignore: 6 | - site 7 | - gh-pages 8 | - i18n-summary 9 | - dependabot/** 10 | paths: 11 | - app/** 12 | - i18n/** 13 | - gradle/** 14 | pull_request_target: 15 | types: 16 | - opened 17 | - edited 18 | paths: 19 | - app/** 20 | - i18n/** 21 | - gradle/** 22 | workflow_dispatch: 23 | 24 | permissions: {} 25 | 26 | jobs: 27 | build: 28 | runs-on: ubuntu-latest 29 | if: github.event_name != 'pull_request_target' || github.event.pull_request.draft == false 30 | 31 | steps: 32 | - uses: actions/checkout@v4 33 | with: 34 | submodules: recursive 35 | 36 | - uses: actions/setup-node@v4 37 | with: 38 | node-version: 20.x 39 | cache: npm 40 | 41 | - uses: actions/setup-java@v4 42 | with: 43 | distribution: zulu 44 | java-version: 17 45 | cache: gradle 46 | 47 | - name: 🚧 Do prerequisites 48 | run: npm ci 49 | 50 | - name: 🚨 Analyze code 51 | run: | 52 | npm run prebuild 53 | chmod +x ./gradlew 54 | ./gradlew lintRelease 55 | -------------------------------------------------------------------------------- /.github/workflows/fastlane-changelogs-analysis.yml: -------------------------------------------------------------------------------- 1 | name: Changelogs Analysis 2 | 3 | on: 4 | push: 5 | branches: 6 | - main 7 | paths: 8 | - metadata/**/changelogs 9 | pull_request_target: 10 | types: 11 | - opened 12 | - edited 13 | paths: 14 | - metadata/**/changelogs 15 | workflow_dispatch: 16 | 17 | jobs: 18 | i18n-summary: 19 | runs-on: ubuntu-latest 20 | if: github.event_name != 'pull_request_target' || github.event.pull_request.draft == false 21 | 22 | steps: 23 | - uses: actions/checkout@v4 24 | 25 | - uses: actions/setup-node@v4 26 | with: 27 | node-version: 20.x 28 | cache: npm 29 | 30 | - name: 🚧 Do prerequisites 31 | run: npm ci 32 | 33 | - name: 🚨 Analyze changelogs 34 | run: npm run changelogs:fastlane-character-limit 35 | -------------------------------------------------------------------------------- /.github/workflows/i18n-summary.yml: -------------------------------------------------------------------------------- 1 | name: i18n Summary 2 | 3 | on: 4 | push: 5 | branches: 6 | - main 7 | paths: 8 | - .phrasey/** 9 | - i18n/** 10 | pull_request_target: 11 | types: 12 | - opened 13 | - edited 14 | paths: 15 | - .phrasey/** 16 | - i18n/** 17 | workflow_dispatch: 18 | 19 | permissions: 20 | contents: write 21 | pull-requests: write 22 | 23 | jobs: 24 | i18n-summary: 25 | runs-on: ubuntu-latest 26 | if: github.event_name != 'pull_request_target' || github.event.pull_request.draft == false 27 | 28 | steps: 29 | - uses: actions/checkout@v4 30 | 31 | - uses: actions/setup-node@v4 32 | with: 33 | node-version: 20.x 34 | cache: npm 35 | 36 | - name: 🚧 Do prerequisites 37 | run: npm ci 38 | 39 | - name: 🚨 Analyze i18n 40 | run: npm run i18n:summary 41 | 42 | - name: 🚀 Push summary files 43 | if: github.event_name != 'pull_request_target' 44 | uses: zyrouge/gh-push-action@v1 45 | with: 46 | directory: phrasey-dist 47 | branch: i18n-summary 48 | checkout-orphan: true 49 | force: true 50 | 51 | - name: 💬 Comment summary 52 | if: github.event_name == 'pull_request_target' 53 | uses: actions/github-script@v7 54 | with: 55 | script: | 56 | const content = require("fs").readFileSync("phrasey-dist/README.md", "utf-8"); 57 | github.rest.issues.createComment({ 58 | issue_number: context.issue.number, 59 | owner: context.repo.owner, 60 | repo: context.repo.repo, 61 | body: content, 62 | }); 63 | -------------------------------------------------------------------------------- /.github/workflows/nightly.yml: -------------------------------------------------------------------------------- 1 | name: Nightly 2 | 3 | on: 4 | workflow_dispatch: 5 | 6 | env: 7 | SIGNING_KEYSTORE_FILE: ./secrets/signing_key.jks 8 | OUTPUT_DIR: ./dist 9 | 10 | permissions: 11 | contents: write 12 | 13 | jobs: 14 | nightly-build: 15 | runs-on: ubuntu-latest 16 | 17 | steps: 18 | - uses: actions/checkout@v4 19 | with: 20 | submodules: recursive 21 | 22 | - uses: actions/setup-node@v4 23 | with: 24 | node-version: 20.x 25 | cache: npm 26 | 27 | - uses: actions/setup-java@v4 28 | with: 29 | distribution: zulu 30 | java-version: 17 31 | cache: gradle 32 | 33 | - name: 🚧 Do prerequisites 34 | run: npm ci 35 | 36 | - name: 🔢 Get version 37 | id: app_version 38 | run: echo "version=$(npm run --silent version:print-nightly)" >> $GITHUB_OUTPUT 39 | 40 | - name: 🔎 Check for release 41 | run: npm run --silent git:tag-exists -- $TAG_NAME 42 | env: 43 | TAG_NAME: v${{ steps.app_version.outputs.version }} 44 | 45 | - name: 🔨 Generate certificate 46 | run: | 47 | mkdir -p $(dirname $SIGNING_KEYSTORE_FILE) 48 | echo $SIGNING_KEYSTORE_FILE_CONTENT | base64 -di > $SIGNING_KEYSTORE_FILE 49 | env: 50 | SIGNING_KEYSTORE_FILE_CONTENT: ${{ secrets.SIGNING_KEYSTORE_FILE }} 51 | 52 | - name: 🔨 Build assets 53 | run: | 54 | npm run prebuild 55 | chmod +x ./gradlew 56 | ./gradlew assembleNightly 57 | ./gradlew bundleNightly 58 | npm run postbuild 59 | env: 60 | APP_BUILD_TYPE: nightly 61 | APP_VERSION_NAME: ${{ steps.app_version.outputs.version }} 62 | SIGNING_KEYSTORE_PASSWORD: ${{ secrets.SIGNING_KEYSTORE_PASSWORD }} 63 | SIGNING_KEY_ALIAS: ${{ secrets.SIGNING_KEY_ALIAS }} 64 | SIGNING_KEY_PASSWORD: ${{ secrets.SIGNING_KEY_PASSWORD }} 65 | 66 | - name: 🚀 Upload assets 67 | uses: ncipollo/release-action@v1 68 | with: 69 | tag: v${{ steps.app_version.outputs.version }} 70 | artifacts: ${{ env.OUTPUT_DIR }}/* 71 | body: "Refer [commits of v${{ steps.app_version.outputs.version }}](https://github.com/zyrouge/symphony/commits/v${{ steps.app_version.outputs.version }}) for the changes." 72 | generateReleaseNotes: false 73 | draft: false 74 | prerelease: true 75 | artifactErrorsFailBuild: true 76 | -------------------------------------------------------------------------------- /.github/workflows/stable.yml: -------------------------------------------------------------------------------- 1 | name: Stable 2 | 3 | on: 4 | workflow_dispatch: 5 | 6 | env: 7 | SIGNING_KEYSTORE_FILE: ./secrets/signing_key.jks 8 | OUTPUT_DIR: ./dist 9 | 10 | permissions: 11 | contents: write 12 | 13 | jobs: 14 | release-build: 15 | runs-on: ubuntu-latest 16 | 17 | steps: 18 | - uses: actions/checkout@v4 19 | with: 20 | submodules: recursive 21 | 22 | - uses: actions/setup-node@v4 23 | with: 24 | node-version: 20.x 25 | cache: npm 26 | 27 | - uses: actions/setup-java@v4 28 | with: 29 | distribution: zulu 30 | java-version: 17 31 | cache: gradle 32 | 33 | - name: 🚧 Do prerequisites 34 | run: npm ci 35 | 36 | - name: 🔢 Get version 37 | id: app_version 38 | run: echo "version=$(npm run --silent version:print)" >> $GITHUB_OUTPUT 39 | 40 | - name: 🔎 Check for release 41 | run: npm run --silent git:tag-exists -- $TAG_NAME 42 | env: 43 | TAG_NAME: v${{ steps.app_version.outputs.version }} 44 | 45 | - name: 🔨 Generate certificate 46 | run: | 47 | mkdir -p $(dirname $SIGNING_KEYSTORE_FILE) 48 | echo $SIGNING_KEYSTORE_FILE_CONTENT | base64 -di > $SIGNING_KEYSTORE_FILE 49 | env: 50 | SIGNING_KEYSTORE_FILE_CONTENT: ${{ secrets.SIGNING_KEYSTORE_FILE }} 51 | 52 | - name: 🔨 Build assets 53 | run: | 54 | npm run prebuild 55 | chmod +x ./gradlew 56 | ./gradlew assembleRelease 57 | ./gradlew bundleRelease 58 | npm run postbuild 59 | env: 60 | APP_VERSION_NAME: ${{ steps.app_version.outputs.version }} 61 | SIGNING_KEYSTORE_PASSWORD: ${{ secrets.SIGNING_KEYSTORE_PASSWORD }} 62 | SIGNING_KEY_ALIAS: ${{ secrets.SIGNING_KEY_ALIAS }} 63 | SIGNING_KEY_PASSWORD: ${{ secrets.SIGNING_KEY_PASSWORD }} 64 | 65 | - name: 🚀 Upload assets 66 | uses: ncipollo/release-action@v1 67 | with: 68 | tag: v${{ steps.app_version.outputs.version }} 69 | artifacts: ${{ env.OUTPUT_DIR }}/* 70 | generateReleaseNotes: true 71 | draft: true 72 | artifactErrorsFailBuild: true 73 | -------------------------------------------------------------------------------- /.github/workflows/website.yml: -------------------------------------------------------------------------------- 1 | name: Website 2 | 3 | on: 4 | workflow_dispatch: 5 | 6 | permissions: {} 7 | 8 | jobs: 9 | website-deploy: 10 | runs-on: ubuntu-latest 11 | 12 | steps: 13 | - uses: actions/checkout@v4 14 | 15 | - run: | 16 | echo Please run it from the "site" branch. 17 | exit 1 18 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | *.iml 2 | .gradle 3 | local.properties 4 | .idea/* 5 | !/.idea/codeStyles 6 | /.idea/codeStyles/* 7 | !/.idea/codeStyles/Project.xml 8 | !/.idea/codeStyles/codeStyleConfig.xml 9 | !/.idea/dictionaries 10 | *.iml 11 | .DS_Store 12 | build 13 | captures 14 | .externalNativeBuild 15 | .cxx 16 | local.properties 17 | node_modules 18 | *.g.kt 19 | *.g.json 20 | phrasey-dist 21 | app/src/main/assets/i18n 22 | dist 23 | secrets 24 | .kotlin 25 | -------------------------------------------------------------------------------- /.gitmodules: -------------------------------------------------------------------------------- 1 | [submodule "metaphony/src/main/cpp/taglib"] 2 | path = metaphony/src/main/cpp/taglib 3 | url = https://github.com/taglib/taglib.git 4 | -------------------------------------------------------------------------------- /.idea/codeStyles/codeStyleConfig.xml: -------------------------------------------------------------------------------- 1 | <component name="ProjectCodeStyleConfiguration"> 2 | <state> 3 | <option name="USE_PER_PROJECT_SETTINGS" value="true" /> 4 | </state> 5 | </component> -------------------------------------------------------------------------------- /.idea/dictionaries/zyrouge.xml: -------------------------------------------------------------------------------- 1 | <component name="ProjectDictionaryState"> 2 | <dictionary name="zyrouge" /> 3 | </component> -------------------------------------------------------------------------------- /.phrasey/additional-locales.json: -------------------------------------------------------------------------------- 1 | [ 2 | { 3 | "display": "Okinawan", 4 | "native": "うちなーぐち", 5 | "code": "ryu", 6 | "details": { 7 | "language": { 8 | "display": "Okinawan", 9 | "native": "うちなーぐち", 10 | "code": "ryu" 11 | } 12 | }, 13 | "direction": "ltr" 14 | } 15 | ] 16 | -------------------------------------------------------------------------------- /.phrasey/config.toml: -------------------------------------------------------------------------------- 1 | [schema] 2 | file = "./schema.toml" 3 | format = "toml" 4 | 5 | [input] 6 | files = "../i18n/**.toml" 7 | format = "toml" 8 | fallback = "../i18n/en.toml" 9 | 10 | [output] 11 | dir = "../app/src/main/assets/i18n" 12 | format = "json" 13 | stringFormat = "java-format-string" 14 | 15 | [locales] 16 | file = "../i18n/locales.g.json" 17 | format = "json" 18 | 19 | [hooks] 20 | files = ["./hooks/kt-sync.js", "./hooks/locales.js"] 21 | -------------------------------------------------------------------------------- /.phrasey/hooks/locales.js: -------------------------------------------------------------------------------- 1 | const p = require("path"); 2 | const fs = require("fs-extra"); 3 | const { PhraseyLocaleBuilder } = require("@zyrouge/phrasey-locales-builder"); 4 | const { rootDir, rootI18nDir } = require("./utils"); 5 | 6 | /** 7 | * @type {import("phrasey").PhraseyHooksHandler} 8 | */ 9 | const hook = { 10 | beforeLocalesParsing: async ({ phrasey, log }) => { 11 | await createLocalesJson(phrasey, log); 12 | }, 13 | }; 14 | 15 | module.exports = hook; 16 | 17 | /** 18 | * 19 | * @param {import("phrasey").Phrasey} phrasey 20 | * @param {import("phrasey").PhraseyLogger} log 21 | */ 22 | async function createLocalesJson(phrasey, log) { 23 | const path = p.join(rootI18nDir, "locales.g.json"); 24 | if (await fs.exists(path)) { 25 | log.info(`Skipping generation of locales as it already exists.`); 26 | return; 27 | } 28 | const additionalLocalesPath = p.resolve( 29 | __dirname, 30 | `../additional-locales.json`, 31 | ); 32 | /** 33 | * @type {import("phrasey").PhraseyLocaleType[]} 34 | */ 35 | const defaultLocales = await PhraseyLocaleBuilder.build({ 36 | displayLocaleCode: "en", 37 | }); 38 | /** 39 | * @type {import("phrasey").PhraseyLocaleType[]} 40 | */ 41 | const additionalLocales = await fs.readJSON(additionalLocalesPath); 42 | const locales = [...defaultLocales, ...additionalLocales]; 43 | await fs.writeFile(path, JSON.stringify(locales)); 44 | log.success(`Generated "${p.relative(rootDir, path)}".`); 45 | } 46 | -------------------------------------------------------------------------------- /.phrasey/hooks/utils.js: -------------------------------------------------------------------------------- 1 | const p = require("path"); 2 | 3 | const rootDir = p.resolve(__dirname, "../.."); 4 | 5 | const appI18nDir = p.join( 6 | rootDir, 7 | "app/src/main/java/io/github/zyrouge/symphony/services/i18n" 8 | ); 9 | 10 | const rootI18nDir = p.join(rootDir, "i18n"); 11 | 12 | module.exports = { 13 | rootDir, 14 | appI18nDir, 15 | rootI18nDir, 16 | }; 17 | -------------------------------------------------------------------------------- /.prettierrc: -------------------------------------------------------------------------------- 1 | { 2 | "tabWidth": 4, 3 | "useTabs": false 4 | } 5 | -------------------------------------------------------------------------------- /.run/Test Metaphony.run.xml: -------------------------------------------------------------------------------- 1 | <component name="ProjectRunConfigurationManager"> 2 | <configuration default="false" name="Test Metaphony" type="GradleRunConfiguration" factoryName="Gradle"> 3 | <ExternalSystemSettings> 4 | <option name="executionName" /> 5 | <option name="externalProjectPath" value="$PROJECT_DIR$" /> 6 | <option name="externalSystemIdString" value="GRADLE" /> 7 | <option name="scriptParameters" value="" /> 8 | <option name="taskDescriptions"> 9 | <list /> 10 | </option> 11 | <option name="taskNames"> 12 | <list> 13 | <option value=":app:testDebugUnitTest" /> 14 | <option value="--tests" /> 15 | <option value=""io.github.zyrouge.metaphony.*"" /> 16 | </list> 17 | </option> 18 | <option name="vmOptions" /> 19 | </ExternalSystemSettings> 20 | <ExternalSystemDebugServerProcess>false</ExternalSystemDebugServerProcess> 21 | <ExternalSystemReattachDebugProcess>true</ExternalSystemReattachDebugProcess> 22 | <DebugAllEnabled>false</DebugAllEnabled> 23 | <RunAsTest>true</RunAsTest> 24 | <method v="2" /> 25 | </configuration> 26 | </component> -------------------------------------------------------------------------------- /.vscode/settings.json: -------------------------------------------------------------------------------- 1 | { 2 | "editor.formatOnSave": true, 3 | "editor.defaultFormatter": "esbenp.prettier-vscode", 4 | "[toml]": { 5 | "editor.defaultFormatter": "esbenp.prettier-vscode" 6 | }, 7 | "[plaintext]": { 8 | "editor.defaultFormatter": "esbenp.prettier-vscode" 9 | }, 10 | "js/ts.implicitProjectConfig.checkJs": true 11 | } 12 | -------------------------------------------------------------------------------- /app/.gitignore: -------------------------------------------------------------------------------- 1 | /build 2 | /release 3 | -------------------------------------------------------------------------------- /app/proguard-rules.pro: -------------------------------------------------------------------------------- 1 | -keepattributes LineNumberTable,SourceFile 2 | -renamesourcefileattribute SourceFile -------------------------------------------------------------------------------- /app/room-schemas/io.github.zyrouge.symphony.services.database.PersistentDatabase/1.json: -------------------------------------------------------------------------------- 1 | { 2 | "formatVersion": 1, 3 | "database": { 4 | "version": 1, 5 | "identityHash": "6091615e6ae35543e127d6744215fad8", 6 | "entities": [ 7 | { 8 | "tableName": "playlists", 9 | "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` TEXT NOT NULL, `title` TEXT NOT NULL, `songPaths` TEXT NOT NULL, `uri` TEXT, `path` TEXT, PRIMARY KEY(`id`))", 10 | "fields": [ 11 | { 12 | "fieldPath": "id", 13 | "columnName": "id", 14 | "affinity": "TEXT", 15 | "notNull": true 16 | }, 17 | { 18 | "fieldPath": "title", 19 | "columnName": "title", 20 | "affinity": "TEXT", 21 | "notNull": true 22 | }, 23 | { 24 | "fieldPath": "songPaths", 25 | "columnName": "songPaths", 26 | "affinity": "TEXT", 27 | "notNull": true 28 | }, 29 | { 30 | "fieldPath": "uri", 31 | "columnName": "uri", 32 | "affinity": "TEXT", 33 | "notNull": false 34 | }, 35 | { 36 | "fieldPath": "path", 37 | "columnName": "path", 38 | "affinity": "TEXT", 39 | "notNull": false 40 | } 41 | ], 42 | "primaryKey": { 43 | "autoGenerate": false, 44 | "columnNames": [ 45 | "id" 46 | ] 47 | }, 48 | "indices": [], 49 | "foreignKeys": [] 50 | } 51 | ], 52 | "views": [], 53 | "setupQueries": [ 54 | "CREATE TABLE IF NOT EXISTS room_master_table (id INTEGER PRIMARY KEY,identity_hash TEXT)", 55 | "INSERT OR REPLACE INTO room_master_table (id,identity_hash) VALUES(42, '6091615e6ae35543e127d6744215fad8')" 56 | ] 57 | } 58 | } -------------------------------------------------------------------------------- /app/src/canary/res/values/strings.xml: -------------------------------------------------------------------------------- 1 | <resources> 2 | <string name="app_name">Symphony (Canary)</string> 3 | </resources> -------------------------------------------------------------------------------- /app/src/debug/res/values/strings.xml: -------------------------------------------------------------------------------- 1 | <resources> 2 | <string name="app_name">Symphony (Debug)</string> 3 | </resources> -------------------------------------------------------------------------------- /app/src/main/AndroidManifest.xml: -------------------------------------------------------------------------------- 1 | <?xml version="1.0" encoding="utf-8"?> 2 | <manifest xmlns:android="http://schemas.android.com/apk/res/android" 3 | xmlns:tools="http://schemas.android.com/tools"> 4 | 5 | <uses-permission android:name="android.permission.POST_NOTIFICATIONS" /> 6 | <uses-permission android:name="android.permission.INTERNET" /> 7 | <uses-permission android:name="android.permission.FOREGROUND_SERVICE" /> 8 | <uses-permission android:name="android.permission.FOREGROUND_SERVICE_MEDIA_PLAYBACK" /> 9 | 10 | <application 11 | android:allowBackup="true" 12 | android:dataExtractionRules="@xml/data_extraction_rules" 13 | android:enableOnBackInvokedCallback="true" 14 | android:fullBackupContent="@xml/backup_rules" 15 | android:icon="@mipmap/ic_launcher" 16 | android:label="@string/app_name" 17 | android:supportsRtl="true" 18 | android:theme="@style/Theme.Symphony" 19 | tools:targetApi="tiramisu"> 20 | <activity 21 | android:name=".MainActivity" 22 | android:theme="@style/Theme.Symphony.SplashScreen" 23 | android:configChanges="orientation|screenSize|screenLayout|keyboardHidden|uiMode" 24 | android:windowSoftInputMode="adjustResize" 25 | android:launchMode="singleTask" 26 | android:exported="true"> 27 | <intent-filter> 28 | <action android:name="android.intent.action.MAIN" /> 29 | <category android:name="android.intent.category.LAUNCHER" /> 30 | </intent-filter> 31 | 32 | <meta-data 33 | android:name="android.app.lib_name" 34 | android:value="" /> 35 | </activity> 36 | <activity android:name=".ErrorActivity" /> 37 | 38 | <service 39 | android:name=".services.radio.RadioNotificationService" 40 | android:foregroundServiceType="mediaPlayback" 41 | android:stopWithTask="true" /> 42 | </application> 43 | </manifest> 44 | -------------------------------------------------------------------------------- /app/src/main/ic_launcher-playstore.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/zyrouge/symphony/d6ff56b54422c5db6151107be48140d6b954e860/app/src/main/ic_launcher-playstore.png -------------------------------------------------------------------------------- /app/src/main/java/io/github/zyrouge/symphony/ActivityIgnition.kt: -------------------------------------------------------------------------------- 1 | package io.github.zyrouge.symphony 2 | 3 | import androidx.lifecycle.ViewModel 4 | import kotlinx.coroutines.flow.MutableStateFlow 5 | import kotlinx.coroutines.flow.asStateFlow 6 | import kotlinx.coroutines.flow.update 7 | 8 | class ActivityIgnition : ViewModel() { 9 | private val readyFlow = MutableStateFlow(false) 10 | val ready = readyFlow.asStateFlow() 11 | 12 | internal fun emitReady() { 13 | if (readyFlow.value) { 14 | return 15 | } 16 | readyFlow.update { 17 | true 18 | } 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /app/src/main/java/io/github/zyrouge/symphony/ErrorActivity.kt: -------------------------------------------------------------------------------- 1 | package io.github.zyrouge.symphony 2 | 3 | import android.content.Context 4 | import android.content.Intent 5 | import android.os.Bundle 6 | import androidx.activity.ComponentActivity 7 | import androidx.activity.compose.setContent 8 | import io.github.zyrouge.symphony.ui.components.ErrorComp 9 | 10 | class ErrorActivity : ComponentActivity() { 11 | override fun onCreate(savedInstanceState: Bundle?) { 12 | super.onCreate(savedInstanceState) 13 | 14 | val errorMessage = intent?.extras?.getString(KEY_ERROR_MESSAGE) ?: "Unknown" 15 | val errorStackTrace = intent?.extras?.getString(KEY_ERROR_STACK_TRACE) ?: "-" 16 | 17 | setContent { 18 | ErrorComp(errorMessage, errorStackTrace) 19 | } 20 | } 21 | 22 | companion object { 23 | const val KEY_ERROR_MESSAGE = "error_message" 24 | const val KEY_ERROR_STACK_TRACE = "error_stack_trace" 25 | 26 | fun start(context: Context, error: Throwable) { 27 | val intent = Intent(context, ErrorActivity::class.java) 28 | intent.flags = Intent.FLAG_ACTIVITY_NEW_TASK or Intent.FLAG_ACTIVITY_TASK_ON_HOME 29 | intent.putExtra(KEY_ERROR_MESSAGE, error.toString()) 30 | intent.putExtra(KEY_ERROR_STACK_TRACE, error.stackTraceToString()) 31 | context.startActivity(intent) 32 | } 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /app/src/main/java/io/github/zyrouge/symphony/MainActivity.kt: -------------------------------------------------------------------------------- 1 | package io.github.zyrouge.symphony 2 | 3 | import android.os.Bundle 4 | import androidx.activity.ComponentActivity 5 | import androidx.activity.compose.setContent 6 | import androidx.activity.enableEdgeToEdge 7 | import androidx.activity.viewModels 8 | import androidx.compose.runtime.LaunchedEffect 9 | import androidx.compose.ui.platform.LocalContext 10 | import androidx.core.splashscreen.SplashScreen.Companion.installSplashScreen 11 | import io.github.zyrouge.symphony.ui.view.BaseView 12 | import io.github.zyrouge.symphony.utils.Logger 13 | 14 | class MainActivity : ComponentActivity() { 15 | private var gSymphony: Symphony? = null 16 | 17 | override fun onCreate(savedInstanceState: Bundle?) { 18 | super.onCreate(savedInstanceState) 19 | 20 | val ignition: ActivityIgnition by viewModels() 21 | if (savedInstanceState == null) { 22 | installSplashScreen().apply { 23 | setKeepOnScreenCondition { !ignition.ready.value } 24 | } 25 | } 26 | 27 | Thread.setDefaultUncaughtExceptionHandler { _, err -> 28 | Logger.error("MainActivity", "uncaught exception", err) 29 | ErrorActivity.start(this, err) 30 | finish() 31 | } 32 | 33 | val symphony: Symphony by viewModels() 34 | symphony.permission.handle(this) 35 | gSymphony = symphony 36 | symphony.emitActivityReady() 37 | attachHandlers() 38 | 39 | enableEdgeToEdge() 40 | setContent { 41 | LaunchedEffect(LocalContext.current) { 42 | ignition.emitReady() 43 | } 44 | BaseView(symphony = symphony, activity = this) 45 | } 46 | } 47 | 48 | override fun onPause() { 49 | super.onPause() 50 | gSymphony?.emitActivityPause() 51 | } 52 | 53 | override fun onDestroy() { 54 | super.onDestroy() 55 | gSymphony?.emitActivityDestroy() 56 | } 57 | 58 | private fun attachHandlers() { 59 | gSymphony?.closeApp = { 60 | finish() 61 | } 62 | } 63 | } 64 | -------------------------------------------------------------------------------- /app/src/main/java/io/github/zyrouge/symphony/services/Permissions.kt: -------------------------------------------------------------------------------- 1 | package io.github.zyrouge.symphony.services 2 | 3 | import android.Manifest 4 | import android.content.pm.PackageManager 5 | import android.os.Build 6 | import androidx.activity.result.contract.ActivityResultContracts 7 | import io.github.zyrouge.symphony.MainActivity 8 | import io.github.zyrouge.symphony.Symphony 9 | 10 | class Permissions(private val symphony: Symphony) { 11 | data class State( 12 | val required: List<String>, 13 | val granted: List<String>, 14 | val denied: List<String>, 15 | ) { 16 | fun hasAll() = denied.isEmpty() 17 | } 18 | 19 | fun handle(activity: MainActivity) { 20 | val state = getState(activity) 21 | if (state.hasAll()) { 22 | return 23 | } 24 | val contract = ActivityResultContracts.RequestMultiplePermissions() 25 | activity.registerForActivityResult(contract) {}.launch(state.denied.toTypedArray()) 26 | } 27 | 28 | private fun getRequiredPermissions(): List<String> { 29 | val required = mutableListOf<String>() 30 | if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) { 31 | required.add(Manifest.permission.POST_NOTIFICATIONS) 32 | } 33 | return required 34 | } 35 | 36 | private fun getState(activity: MainActivity): State { 37 | val required = getRequiredPermissions() 38 | val granted = mutableListOf<String>() 39 | val denied = mutableListOf<String>() 40 | required.forEach { 41 | if (activity.checkSelfPermission(it) == PackageManager.PERMISSION_GRANTED) { 42 | granted.add(it) 43 | } else { 44 | denied.add(it) 45 | } 46 | } 47 | return State(required = required, granted = granted, denied = denied) 48 | } 49 | } 50 | -------------------------------------------------------------------------------- /app/src/main/java/io/github/zyrouge/symphony/services/database/CacheDatabase.kt: -------------------------------------------------------------------------------- 1 | package io.github.zyrouge.symphony.services.database 2 | 3 | import androidx.room.AutoMigration 4 | import androidx.room.Database 5 | import androidx.room.DeleteColumn 6 | import androidx.room.Room 7 | import androidx.room.RoomDatabase 8 | import androidx.room.TypeConverters 9 | import androidx.room.migration.AutoMigrationSpec 10 | import io.github.zyrouge.symphony.Symphony 11 | import io.github.zyrouge.symphony.services.database.store.SongCacheStore 12 | import io.github.zyrouge.symphony.services.groove.Song 13 | import io.github.zyrouge.symphony.utils.RoomConvertors 14 | 15 | @Database( 16 | entities = [Song::class], 17 | version = 2, 18 | autoMigrations = [AutoMigration(1, 2, CacheDatabase.Migration1To2::class)] 19 | ) 20 | @TypeConverters(RoomConvertors::class) 21 | abstract class CacheDatabase : RoomDatabase() { 22 | abstract fun songs(): SongCacheStore 23 | 24 | companion object { 25 | fun create(symphony: Symphony) = Room 26 | .databaseBuilder( 27 | symphony.applicationContext, 28 | CacheDatabase::class.java, 29 | "cache" 30 | ) 31 | .build() 32 | } 33 | 34 | @DeleteColumn("songs", "minBitrate") 35 | @DeleteColumn("songs", "maxBitrate") 36 | @DeleteColumn("songs", "bitsPerSample") 37 | @DeleteColumn("songs", "samples") 38 | @DeleteColumn("songs", "codec") 39 | class Migration1To2 : AutoMigrationSpec 40 | } 41 | -------------------------------------------------------------------------------- /app/src/main/java/io/github/zyrouge/symphony/services/database/Database.kt: -------------------------------------------------------------------------------- 1 | package io.github.zyrouge.symphony.services.database 2 | 3 | import io.github.zyrouge.symphony.Symphony 4 | import io.github.zyrouge.symphony.services.database.store.ArtworkCacheStore 5 | import io.github.zyrouge.symphony.services.database.store.LyricsCacheStore 6 | 7 | class Database(symphony: Symphony) { 8 | private val cache = CacheDatabase.create(symphony) 9 | private val persistent = PersistentDatabase.create(symphony) 10 | 11 | val artworkCache = ArtworkCacheStore(symphony) 12 | val lyricsCache = LyricsCacheStore(symphony) 13 | val songCache get() = cache.songs() 14 | val playlists get() = persistent.playlists() 15 | } 16 | -------------------------------------------------------------------------------- /app/src/main/java/io/github/zyrouge/symphony/services/database/PersistentDatabase.kt: -------------------------------------------------------------------------------- 1 | package io.github.zyrouge.symphony.services.database 2 | 3 | import androidx.room.Database 4 | import androidx.room.Room 5 | import androidx.room.RoomDatabase 6 | import androidx.room.TypeConverters 7 | import io.github.zyrouge.symphony.Symphony 8 | import io.github.zyrouge.symphony.services.database.store.PlaylistStore 9 | import io.github.zyrouge.symphony.services.groove.Playlist 10 | import io.github.zyrouge.symphony.utils.RoomConvertors 11 | 12 | @Database(entities = [Playlist::class], version = 1) 13 | @TypeConverters(RoomConvertors::class) 14 | abstract class PersistentDatabase : RoomDatabase() { 15 | abstract fun playlists(): PlaylistStore 16 | 17 | companion object { 18 | fun create(symphony: Symphony) = Room 19 | .databaseBuilder( 20 | symphony.applicationContext, 21 | PersistentDatabase::class.java, 22 | "persistent" 23 | ) 24 | .build() 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /app/src/main/java/io/github/zyrouge/symphony/services/database/adapters/FileDatabaseAdapter.kt: -------------------------------------------------------------------------------- 1 | package io.github.zyrouge.symphony.services.database.adapters 2 | 3 | import java.io.File 4 | 5 | class FileDatabaseAdapter(val file: File) { 6 | fun overwrite(content: String) = overwrite(content.toByteArray()) 7 | fun overwrite(bytes: ByteArray) { 8 | file.outputStream().use { it.write(bytes) } 9 | } 10 | 11 | fun read() = file.inputStream().use { String(it.readBytes()) } 12 | } 13 | -------------------------------------------------------------------------------- /app/src/main/java/io/github/zyrouge/symphony/services/database/adapters/FileTreeDatabaseAdapter.kt: -------------------------------------------------------------------------------- 1 | package io.github.zyrouge.symphony.services.database.adapters 2 | 3 | import java.io.File 4 | 5 | class FileTreeDatabaseAdapter(val tree: File) { 6 | init { 7 | tree.mkdirs() 8 | } 9 | 10 | fun get(name: String): File = File(tree, name) 11 | 12 | fun list(): List<String> = tree.list()?.toList() ?: emptyList() 13 | 14 | fun clear() { 15 | tree.deleteRecursively() 16 | tree.mkdirs() 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /app/src/main/java/io/github/zyrouge/symphony/services/database/store/ArtworkCacheStore.kt: -------------------------------------------------------------------------------- 1 | package io.github.zyrouge.symphony.services.database.store 2 | 3 | import io.github.zyrouge.symphony.Symphony 4 | import io.github.zyrouge.symphony.services.database.adapters.FileTreeDatabaseAdapter 5 | import java.nio.file.Paths 6 | 7 | class ArtworkCacheStore(val symphony: Symphony) { 8 | private val adapter = FileTreeDatabaseAdapter( 9 | Paths 10 | .get(symphony.applicationContext.dataDir.absolutePath, "covers") 11 | .toFile() 12 | ) 13 | 14 | fun get(key: String) = adapter.get(key) 15 | fun all() = adapter.list() 16 | fun clear() = adapter.clear() 17 | } 18 | -------------------------------------------------------------------------------- /app/src/main/java/io/github/zyrouge/symphony/services/database/store/LyricsCacheStore.kt: -------------------------------------------------------------------------------- 1 | package io.github.zyrouge.symphony.services.database.store 2 | 3 | import io.github.zyrouge.symphony.Symphony 4 | import io.github.zyrouge.symphony.services.database.adapters.SQLiteKeyValueDatabaseAdapter 5 | 6 | class LyricsCacheStore(val symphony: Symphony) { 7 | private val adapter = SQLiteKeyValueDatabaseAdapter( 8 | SQLiteKeyValueDatabaseAdapter.Transformer.AsString(), 9 | SQLiteKeyValueDatabaseAdapter.CacheOpenHelper(symphony.applicationContext, "lyrics", 1) 10 | ) 11 | 12 | fun get(key: String) = adapter.get(key) 13 | fun put(key: String, value: String) = adapter.put(key, value) 14 | fun delete(key: String) = adapter.delete(key) 15 | fun delete(keys: Collection<String>) = adapter.delete(keys) 16 | fun keys() = adapter.keys() 17 | fun all() = adapter.all() 18 | fun clear() = adapter.clear() 19 | } 20 | -------------------------------------------------------------------------------- /app/src/main/java/io/github/zyrouge/symphony/services/database/store/PlaylistStore.kt: -------------------------------------------------------------------------------- 1 | package io.github.zyrouge.symphony.services.database.store 2 | 3 | import androidx.room.Dao 4 | import androidx.room.Insert 5 | import androidx.room.MapColumn 6 | import androidx.room.Query 7 | import androidx.room.Update 8 | import io.github.zyrouge.symphony.services.groove.Playlist 9 | 10 | @Dao 11 | interface PlaylistStore { 12 | @Insert 13 | suspend fun insert(vararg playlist: Playlist): List<Long> 14 | 15 | @Update 16 | suspend fun update(vararg playlist: Playlist): Int 17 | 18 | @Query("DELETE FROM playlists WHERE id = :playlistId") 19 | suspend fun delete(playlistId: String): Int 20 | 21 | @Query("SELECT * FROM playlists") 22 | suspend fun entries(): Map<@MapColumn("id") String, Playlist> 23 | } 24 | -------------------------------------------------------------------------------- /app/src/main/java/io/github/zyrouge/symphony/services/database/store/SongCacheStore.kt: -------------------------------------------------------------------------------- 1 | package io.github.zyrouge.symphony.services.database.store 2 | 3 | import androidx.room.Dao 4 | import androidx.room.Insert 5 | import androidx.room.MapColumn 6 | import androidx.room.Query 7 | import androidx.room.Update 8 | import io.github.zyrouge.symphony.services.groove.Song 9 | 10 | @Dao 11 | interface SongCacheStore { 12 | @Insert() 13 | suspend fun insert(vararg song: Song): List<Long> 14 | 15 | @Update 16 | suspend fun update(vararg song: Song): Int 17 | 18 | @Query("DELETE FROM songs WHERE id = :songId") 19 | suspend fun delete(songId: String): Int 20 | 21 | @Query("DELETE FROM songs WHERE id IN (:songIds)") 22 | suspend fun delete(songIds: Collection<String>): Int 23 | 24 | @Query("DELETE FROM songs") 25 | suspend fun clear(): Int 26 | 27 | @Query("SELECT * FROM songs") 28 | suspend fun entriesPathMapped(): Map<@MapColumn("path") String, Song> 29 | } 30 | -------------------------------------------------------------------------------- /app/src/main/java/io/github/zyrouge/symphony/services/groove/Album.kt: -------------------------------------------------------------------------------- 1 | package io.github.zyrouge.symphony.services.groove 2 | 3 | import androidx.compose.runtime.Immutable 4 | import io.github.zyrouge.symphony.Symphony 5 | import kotlin.time.Duration 6 | 7 | @Immutable 8 | data class Album( 9 | val id: String, 10 | val name: String, 11 | val artists: MutableSet<String>, 12 | var startYear: Int?, 13 | var endYear: Int?, 14 | var numberOfTracks: Int, 15 | var duration: Duration, 16 | ) { 17 | fun createArtworkImageRequest(symphony: Symphony) = 18 | symphony.groove.album.createArtworkImageRequest(id) 19 | 20 | fun getSongIds(symphony: Symphony) = symphony.groove.album.getSongIds(id) 21 | fun getSortedSongIds(symphony: Symphony) = symphony.groove.song.sort( 22 | getSongIds(symphony), 23 | symphony.settings.lastUsedAlbumSongsSortBy.value, 24 | symphony.settings.lastUsedAlbumSongsSortReverse.value, 25 | ) 26 | } 27 | -------------------------------------------------------------------------------- /app/src/main/java/io/github/zyrouge/symphony/services/groove/AlbumArtist.kt: -------------------------------------------------------------------------------- 1 | package io.github.zyrouge.symphony.services.groove 2 | 3 | import androidx.compose.runtime.Immutable 4 | import io.github.zyrouge.symphony.Symphony 5 | 6 | @Immutable 7 | data class AlbumArtist( 8 | val name: String, 9 | var numberOfAlbums: Int, 10 | var numberOfTracks: Int, 11 | ) { 12 | fun createArtworkImageRequest(symphony: Symphony) = 13 | symphony.groove.albumArtist.createArtworkImageRequest(name) 14 | 15 | fun getSongIds(symphony: Symphony) = symphony.groove.albumArtist.getSongIds(name) 16 | fun getSortedSongIds(symphony: Symphony) = symphony.groove.song.sort( 17 | getSongIds(symphony), 18 | symphony.settings.lastUsedSongsSortBy.value, 19 | symphony.settings.lastUsedSongsSortReverse.value, 20 | ) 21 | 22 | fun getAlbumIds(symphony: Symphony) = symphony.groove.albumArtist.getAlbumIds(name) 23 | } 24 | -------------------------------------------------------------------------------- /app/src/main/java/io/github/zyrouge/symphony/services/groove/Artist.kt: -------------------------------------------------------------------------------- 1 | package io.github.zyrouge.symphony.services.groove 2 | 3 | import androidx.compose.runtime.Immutable 4 | import io.github.zyrouge.symphony.Symphony 5 | 6 | @Immutable 7 | data class Artist( 8 | val name: String, 9 | var numberOfAlbums: Int, 10 | var numberOfTracks: Int, 11 | ) { 12 | fun createArtworkImageRequest(symphony: Symphony) = 13 | symphony.groove.artist.createArtworkImageRequest(name) 14 | 15 | fun getSongIds(symphony: Symphony) = symphony.groove.artist.getSongIds(name) 16 | fun getSortedSongIds(symphony: Symphony) = symphony.groove.song.sort( 17 | getSongIds(symphony), 18 | symphony.settings.lastUsedSongsSortBy.value, 19 | symphony.settings.lastUsedSongsSortReverse.value, 20 | ) 21 | 22 | fun getAlbumIds(symphony: Symphony) = symphony.groove.artist.getAlbumIds(name) 23 | } 24 | -------------------------------------------------------------------------------- /app/src/main/java/io/github/zyrouge/symphony/services/groove/Genre.kt: -------------------------------------------------------------------------------- 1 | package io.github.zyrouge.symphony.services.groove 2 | 3 | import androidx.compose.runtime.Immutable 4 | import io.github.zyrouge.symphony.Symphony 5 | 6 | @Immutable 7 | data class Genre( 8 | val name: String, 9 | var numberOfTracks: Int, 10 | ) { 11 | fun getSongIds(symphony: Symphony) = symphony.groove.genre.getSongIds(name) 12 | fun getSortedSongIds(symphony: Symphony) = symphony.groove.song.sort( 13 | getSongIds(symphony), 14 | symphony.settings.lastUsedSongsSortBy.value, 15 | symphony.settings.lastUsedSongsSortReverse.value, 16 | ) 17 | } 18 | -------------------------------------------------------------------------------- /app/src/main/java/io/github/zyrouge/symphony/services/groove/Groove.kt: -------------------------------------------------------------------------------- 1 | package io.github.zyrouge.symphony.services.groove 2 | 3 | import io.github.zyrouge.symphony.Symphony 4 | import io.github.zyrouge.symphony.services.groove.repositories.AlbumArtistRepository 5 | import io.github.zyrouge.symphony.services.groove.repositories.AlbumRepository 6 | import io.github.zyrouge.symphony.services.groove.repositories.ArtistRepository 7 | import io.github.zyrouge.symphony.services.groove.repositories.GenreRepository 8 | import io.github.zyrouge.symphony.services.groove.repositories.PlaylistRepository 9 | import io.github.zyrouge.symphony.services.groove.repositories.SongRepository 10 | import kotlinx.coroutines.CompletableDeferred 11 | import kotlinx.coroutines.CoroutineScope 12 | import kotlinx.coroutines.Dispatchers 13 | import kotlinx.coroutines.async 14 | import kotlinx.coroutines.awaitAll 15 | import kotlinx.coroutines.launch 16 | 17 | class Groove(private val symphony: Symphony) : Symphony.Hooks { 18 | enum class Kind { 19 | SONG, 20 | ALBUM, 21 | ARTIST, 22 | ALBUM_ARTIST, 23 | GENRE, 24 | PLAYLIST, 25 | } 26 | 27 | val coroutineScope = CoroutineScope(Dispatchers.Default) 28 | var readyDeferred = CompletableDeferred<Boolean>() 29 | 30 | val exposer = MediaExposer(symphony) 31 | val song = SongRepository(symphony) 32 | val album = AlbumRepository(symphony) 33 | val artist = ArtistRepository(symphony) 34 | val albumArtist = AlbumArtistRepository(symphony) 35 | val genre = GenreRepository(symphony) 36 | val playlist = PlaylistRepository(symphony) 37 | 38 | private suspend fun fetch() { 39 | coroutineScope.launch { 40 | awaitAll( 41 | async { exposer.fetch() }, 42 | async { playlist.fetch() }, 43 | ) 44 | }.join() 45 | } 46 | 47 | private suspend fun reset() { 48 | coroutineScope.launch { 49 | awaitAll( 50 | async { exposer.reset() }, 51 | async { albumArtist.reset() }, 52 | async { album.reset() }, 53 | async { artist.reset() }, 54 | async { genre.reset() }, 55 | async { playlist.reset() }, 56 | async { song.reset() }, 57 | ) 58 | }.join() 59 | } 60 | 61 | private suspend fun clearCache() { 62 | symphony.database.songCache.clear() 63 | symphony.database.artworkCache.clear() 64 | symphony.database.lyricsCache.clear() 65 | } 66 | 67 | data class FetchOptions( 68 | val resetInMemoryCache: Boolean = false, 69 | val resetPersistentCache: Boolean = false, 70 | ) 71 | 72 | fun fetch(options: FetchOptions) { 73 | coroutineScope.launch { 74 | if (options.resetInMemoryCache) { 75 | reset() 76 | } 77 | if (options.resetPersistentCache) { 78 | clearCache() 79 | } 80 | fetch() 81 | } 82 | } 83 | 84 | override fun onSymphonyReady() { 85 | coroutineScope.launch { 86 | fetch() 87 | readyDeferred.complete(true) 88 | } 89 | } 90 | } 91 | -------------------------------------------------------------------------------- /app/src/main/java/io/github/zyrouge/symphony/services/i18n/CommonTranslation.kt: -------------------------------------------------------------------------------- 1 | package io.github.zyrouge.symphony.services.i18n 2 | 3 | @Suppress("Unused", "ConstPropertyName", "FunctionName") 4 | object CommonTranslation { 5 | const val SomethingWentHorriblyWrong = "Something went horribly wrong!" 6 | const val System = "System" 7 | 8 | fun ErrorX(x: String) = "Error: $x" 9 | fun SomethingWentHorriblyWrongErrorX(x: String) = 10 | "$SomethingWentHorriblyWrong (${ErrorX(x)})" 11 | } 12 | -------------------------------------------------------------------------------- /app/src/main/java/io/github/zyrouge/symphony/services/i18n/Translation.kt: -------------------------------------------------------------------------------- 1 | package io.github.zyrouge.symphony.services.i18n 2 | 3 | import kotlinx.serialization.ExperimentalSerializationApi 4 | import kotlinx.serialization.json.Json 5 | import kotlinx.serialization.json.decodeFromStream 6 | import java.io.InputStream 7 | 8 | class Translation(container: _Container) : _Translation(container) { 9 | companion object { 10 | private val json = Json { ignoreUnknownKeys = true } 11 | 12 | @OptIn(ExperimentalSerializationApi::class) 13 | fun fromInputStream(input: InputStream): Translation { 14 | val container = json.decodeFromStream<_Container>(input) 15 | return Translation(container) 16 | } 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /app/src/main/java/io/github/zyrouge/symphony/services/i18n/Translations.kt: -------------------------------------------------------------------------------- 1 | package io.github.zyrouge.symphony.services.i18n 2 | 3 | import io.github.zyrouge.symphony.Symphony 4 | 5 | class Translations(private val symphony: Symphony) : _Translations() { 6 | val defaultLocaleCode = "en" 7 | 8 | fun supports(locale: String) = localeCodes.contains(locale) 9 | 10 | fun parse(locale: String) = symphony.applicationContext.assets.open("i18n/${locale}.json").use { 11 | Translation.fromInputStream(it) 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /app/src/main/java/io/github/zyrouge/symphony/services/i18n/Translator.kt: -------------------------------------------------------------------------------- 1 | package io.github.zyrouge.symphony.services.i18n 2 | 3 | import androidx.core.os.LocaleListCompat 4 | import io.github.zyrouge.symphony.Symphony 5 | 6 | class Translator(private val symphony: Symphony) { 7 | val translations = Translations(symphony) 8 | 9 | suspend fun onChange(fn: (Translation) -> Unit) { 10 | symphony.settings.language.flow.collect { 11 | fn(getCurrentTranslation()) 12 | } 13 | } 14 | 15 | fun getCurrentTranslation() = symphony.settings.language.value 16 | ?.let { translations.parse(it) } 17 | ?: getDefaultTranslation() 18 | 19 | fun getDefaultTranslation(): Translation { 20 | val localeCode = getDefaultLocaleCode() 21 | return translations.parse(localeCode) 22 | } 23 | 24 | fun getLocaleDisplayName(localeCode: String) = 25 | translations.localeDisplayNames[localeCode] 26 | 27 | fun getLocaleNativeName(localeCode: String) = 28 | translations.localeNativeNames[localeCode] 29 | 30 | fun getDefaultLocaleDisplayName() = getLocaleDisplayName(getDefaultLocaleCode())!! 31 | fun getDefaultLocaleNativeName() = getLocaleNativeName(getDefaultLocaleCode())!! 32 | fun getDefaultLocaleCode() = LocaleListCompat.getDefault()[0]?.language 33 | ?.takeIf { translations.supports(it) } 34 | ?: translations.defaultLocaleCode 35 | } 36 | -------------------------------------------------------------------------------- /app/src/main/java/io/github/zyrouge/symphony/services/radio/RadioArtworkCacher.kt: -------------------------------------------------------------------------------- 1 | package io.github.zyrouge.symphony.services.radio 2 | 3 | import android.graphics.Bitmap 4 | import android.graphics.BitmapFactory 5 | import androidx.core.graphics.drawable.toBitmap 6 | import coil.imageLoader 7 | import io.github.zyrouge.symphony.Symphony 8 | import io.github.zyrouge.symphony.services.groove.Song 9 | import io.github.zyrouge.symphony.ui.helpers.Assets 10 | 11 | class RadioArtworkCacher(val symphony: Symphony) { 12 | private var default: Bitmap? = null 13 | private var cached = mutableMapOf<String, Bitmap>() 14 | private val cacheLimit = 3 15 | 16 | suspend fun getArtwork(song: Song): Bitmap { 17 | return cached[song.id] ?: kotlin.run { 18 | val result = symphony.applicationContext.imageLoader 19 | .execute(song.createArtworkImageRequest(symphony).build()) 20 | val bitmap = result.drawable?.toBitmap() ?: getDefaultArtwork() 21 | updateCache(song.id, bitmap) 22 | bitmap 23 | } 24 | } 25 | 26 | private fun getDefaultArtwork(): Bitmap { 27 | return default ?: run { 28 | val bitmap = BitmapFactory.decodeResource( 29 | symphony.applicationContext.resources, 30 | Assets.placeholderDarkId, 31 | ) 32 | default = bitmap 33 | bitmap 34 | } 35 | } 36 | 37 | private fun updateCache(key: String, value: Bitmap) { 38 | if (!cached.containsKey(key) && cached.size >= cacheLimit) { 39 | cached.remove(cached.keys.first()) 40 | } 41 | cached[key] = value 42 | } 43 | } 44 | -------------------------------------------------------------------------------- /app/src/main/java/io/github/zyrouge/symphony/services/radio/RadioEffects.kt: -------------------------------------------------------------------------------- 1 | package io.github.zyrouge.symphony.services.radio 2 | 3 | import java.util.Timer 4 | import kotlin.math.max 5 | import kotlin.math.min 6 | 7 | object RadioEffects { 8 | class Fader( 9 | val options: Options, 10 | val onUpdate: (Float) -> Unit, 11 | val onFinish: (Boolean) -> Unit, 12 | ) { 13 | data class Options( 14 | val from: Float, 15 | val to: Float, 16 | val duration: Int, 17 | val interval: Int = DEFAULT_INTERVAL, 18 | ) { 19 | companion object { 20 | private const val DEFAULT_INTERVAL = 50 21 | } 22 | } 23 | 24 | private var timer: Timer? = null 25 | private var ended = false 26 | 27 | fun start() { 28 | val increments = 29 | (options.to - options.from) * (options.interval.toFloat() / options.duration) 30 | var volume = options.from 31 | val isReverse = options.to < options.from 32 | timer = kotlin.concurrent.timer(period = options.interval.toLong()) { 33 | if (volume != options.to) { 34 | onUpdate(volume) 35 | volume = when { 36 | isReverse -> max(options.to, volume + increments) 37 | else -> min(options.to, volume + increments) 38 | } 39 | } else { 40 | ended = true 41 | onFinish(true) 42 | destroy() 43 | } 44 | } 45 | } 46 | 47 | fun stop() { 48 | if (!ended) onFinish(false) 49 | destroy() 50 | } 51 | 52 | private fun destroy() { 53 | timer?.cancel() 54 | timer = null 55 | } 56 | } 57 | 58 | // fun fadeIn(player: RadioPlayer, onEnd: () -> Unit) { 59 | // val options = Fader.Options( 60 | // when { 61 | // player.isPlaying -> player.volume 62 | // else -> RadioPlayer.MIN_VOLUME 63 | // }, 64 | // RadioPlayer.MAX_VOLUME, 65 | // ) 66 | // val fader = Fader( 67 | // options, 68 | // onUpdate = { player.setVolume(it) }, 69 | // onFinish = { onEnd() } 70 | // ) 71 | // player.setVolume(options.from) 72 | // player.start() 73 | // fader.start() 74 | // } 75 | // 76 | // fun fadeOut(player: RadioPlayer, onEnd: () -> Unit) { 77 | // val options = Fader.Options(player.volume, RadioPlayer.MIN_VOLUME) 78 | // val fader = Fader( 79 | // options, 80 | // onUpdate = { player.setVolume(it) }, 81 | // onFinish = { 82 | // player.pause() 83 | // onEnd() 84 | // } 85 | // ) 86 | // player.setVolume(options.from) 87 | // fader.start() 88 | // } 89 | } 90 | -------------------------------------------------------------------------------- /app/src/main/java/io/github/zyrouge/symphony/services/radio/RadioFocus.kt: -------------------------------------------------------------------------------- 1 | package io.github.zyrouge.symphony.services.radio 2 | 3 | import android.media.AudioManager 4 | import androidx.media.AudioAttributesCompat 5 | import androidx.media.AudioFocusRequestCompat 6 | import androidx.media.AudioManagerCompat 7 | import io.github.zyrouge.symphony.Symphony 8 | 9 | // Credits: https://github.com/RetroMusicPlayer/RetroMusicPlayer/blob/7b1593009319c8d8e04660470ba37f814e8203eb/app/src/main/java/code/name/monkey/retromusic/service/LocalPlayback.kt 10 | class RadioFocus(val symphony: Symphony) { 11 | var hasFocus = false 12 | private set 13 | private var restoreVolumeOnFocusGain = false 14 | 15 | private val audioManager: AudioManager = 16 | symphony.applicationContext.getSystemService(AudioManager::class.java) 17 | 18 | private val audioFocusRequest: AudioFocusRequestCompat = 19 | AudioFocusRequestCompat.Builder(AudioManagerCompat.AUDIOFOCUS_GAIN) 20 | .setAudioAttributes( 21 | AudioAttributesCompat.Builder() 22 | .setContentType(AudioAttributesCompat.CONTENT_TYPE_MUSIC) 23 | .build() 24 | ) 25 | .setOnAudioFocusChangeListener { event -> 26 | when (event) { 27 | AudioManager.AUDIOFOCUS_GAIN -> { 28 | hasFocus = true 29 | if (restoreVolumeOnFocusGain) { 30 | restoreVolumeOnFocusGain = false 31 | when { 32 | symphony.radio.isPlaying -> symphony.radio.restoreVolume() 33 | else -> symphony.radio.resume() 34 | } 35 | } 36 | } 37 | 38 | AudioManager.AUDIOFOCUS_LOSS, AudioManager.AUDIOFOCUS_LOSS_TRANSIENT -> { 39 | hasFocus = false 40 | restoreVolumeOnFocusGain = symphony.radio.isPlaying 41 | if (!symphony.settings.ignoreAudioFocusLoss.value) { 42 | symphony.radio.pause() 43 | } 44 | } 45 | 46 | AudioManager.AUDIOFOCUS_LOSS_TRANSIENT_CAN_DUCK -> { 47 | restoreVolumeOnFocusGain = symphony.radio.isPlaying 48 | if (symphony.radio.isPlaying) { 49 | symphony.radio.duck() 50 | } 51 | } 52 | } 53 | } 54 | .build() 55 | 56 | fun requestFocus() = AudioManagerCompat.requestAudioFocus( 57 | audioManager, 58 | audioFocusRequest 59 | ) == AudioManager.AUDIOFOCUS_REQUEST_GRANTED 60 | 61 | fun abandonFocus() = AudioManagerCompat.abandonAudioFocusRequest( 62 | audioManager, 63 | audioFocusRequest 64 | ) == AudioManager.AUDIOFOCUS_REQUEST_GRANTED 65 | } 66 | -------------------------------------------------------------------------------- /app/src/main/java/io/github/zyrouge/symphony/services/radio/RadioNativeReceiver.kt: -------------------------------------------------------------------------------- 1 | package io.github.zyrouge.symphony.services.radio 2 | 3 | import android.content.BroadcastReceiver 4 | import android.content.Context 5 | import android.content.Intent 6 | import android.content.IntentFilter 7 | import android.media.AudioManager 8 | import io.github.zyrouge.symphony.Symphony 9 | 10 | class RadioNativeReceiver(private val symphony: Symphony) : BroadcastReceiver() { 11 | fun start() { 12 | symphony.applicationContext.registerReceiver( 13 | this, 14 | IntentFilter().apply { 15 | addAction(AudioManager.ACTION_AUDIO_BECOMING_NOISY) 16 | addAction(Intent.ACTION_HEADSET_PLUG) 17 | } 18 | ) 19 | } 20 | 21 | fun destroy() { 22 | symphony.applicationContext.unregisterReceiver(this) 23 | } 24 | 25 | override fun onReceive(context: Context?, intent: Intent?) { 26 | intent?.action?.let { action -> 27 | when (action) { 28 | Intent.ACTION_HEADSET_PLUG -> { 29 | intent.extras?.getInt("state", -1)?.let { 30 | when (it) { 31 | 0 -> onHeadphonesDisconnect() 32 | 1 -> onHeadphonesConnect() 33 | else -> {} 34 | } 35 | } 36 | } 37 | 38 | AudioManager.ACTION_AUDIO_BECOMING_NOISY -> onHeadphonesDisconnect() 39 | else -> {} 40 | } 41 | } 42 | } 43 | 44 | private fun onHeadphonesConnect() { 45 | if (!symphony.radio.hasPlayer) { 46 | return 47 | } 48 | if (!symphony.radio.isPlaying && symphony.settings.playOnHeadphonesConnect.value) { 49 | symphony.radio.resume() 50 | } 51 | } 52 | 53 | private fun onHeadphonesDisconnect() { 54 | if (!symphony.radio.hasPlayer) { 55 | return 56 | } 57 | if (symphony.radio.isPlaying && symphony.settings.pauseOnHeadphonesDisconnect.value) { 58 | symphony.radio.pauseInstant() 59 | } 60 | } 61 | } 62 | -------------------------------------------------------------------------------- /app/src/main/java/io/github/zyrouge/symphony/services/radio/RadioNotificationService.kt: -------------------------------------------------------------------------------- 1 | package io.github.zyrouge.symphony.services.radio 2 | 3 | import android.app.Service 4 | import android.app.Service.START_NOT_STICKY 5 | import android.app.Service.STOP_FOREGROUND_REMOVE 6 | import android.content.Intent 7 | import android.os.IBinder 8 | import io.github.zyrouge.symphony.utils.Eventer 9 | 10 | class RadioNotificationService : Service() { 11 | enum class Event { 12 | START, 13 | STOP, 14 | } 15 | 16 | override fun onBind(p0: Intent?): IBinder? = null 17 | 18 | override fun onStartCommand(intent: Intent?, flags: Int, startId: Int): Int { 19 | instance = this 20 | events.dispatch(Event.START) 21 | return START_NOT_STICKY 22 | } 23 | 24 | override fun onDestroy() { 25 | super.onDestroy() 26 | destroy(false) 27 | } 28 | 29 | companion object { 30 | val events = Eventer<Event>() 31 | var instance: RadioNotificationService? = null 32 | 33 | fun destroy(stop: Boolean = true) { 34 | instance?.let { 35 | instance = null 36 | if (stop) { 37 | it.stopForeground(STOP_FOREGROUND_REMOVE) 38 | it.stopSelf() 39 | } 40 | events.dispatch(Event.STOP) 41 | } 42 | } 43 | } 44 | } 45 | -------------------------------------------------------------------------------- /app/src/main/java/io/github/zyrouge/symphony/services/radio/RadioShorty.kt: -------------------------------------------------------------------------------- 1 | package io.github.zyrouge.symphony.services.radio 2 | 3 | import io.github.zyrouge.symphony.Symphony 4 | import kotlin.random.Random 5 | 6 | class RadioShorty(private val symphony: Symphony) { 7 | fun playPause() { 8 | if (!symphony.radio.hasPlayer) { 9 | return 10 | } 11 | when { 12 | symphony.radio.isPlaying -> symphony.radio.pause() 13 | else -> symphony.radio.resume() 14 | } 15 | } 16 | 17 | fun seekFromCurrent(offsetSecs: Int) { 18 | if (!symphony.radio.hasPlayer) { 19 | return 20 | } 21 | symphony.radio.currentPlaybackPosition?.run { 22 | val to = (played + (offsetSecs * 1000)).coerceIn(0..total) 23 | symphony.radio.seek(to) 24 | } 25 | } 26 | 27 | fun previous(): Boolean { 28 | return when { 29 | !symphony.radio.hasPlayer -> false 30 | symphony.radio.currentPlaybackPosition!!.played <= 3000 && symphony.radio.canJumpToPrevious() -> { 31 | symphony.radio.jumpToPrevious() 32 | true 33 | } 34 | 35 | else -> { 36 | symphony.radio.seek(0) 37 | false 38 | } 39 | } 40 | } 41 | 42 | fun skip(): Boolean { 43 | return when { 44 | !symphony.radio.hasPlayer -> false 45 | symphony.radio.canJumpToNext() -> { 46 | symphony.radio.jumpToNext() 47 | true 48 | } 49 | 50 | else -> { 51 | symphony.radio.play(Radio.PlayOptions(index = 0, autostart = false)) 52 | false 53 | } 54 | } 55 | } 56 | 57 | fun playQueue( 58 | songIds: List<String>, 59 | options: Radio.PlayOptions = Radio.PlayOptions(), 60 | shuffle: Boolean = false, 61 | ) { 62 | symphony.radio.stop(ended = false) 63 | if (songIds.isEmpty()) { 64 | return 65 | } 66 | symphony.radio.queue.add( 67 | songIds, 68 | options = options.run { 69 | copy(index = if (shuffle) Random.nextInt(songIds.size) else options.index) 70 | } 71 | ) 72 | symphony.radio.queue.setShuffleMode(shuffle) 73 | } 74 | 75 | fun playQueue( 76 | songId: String, 77 | options: Radio.PlayOptions = Radio.PlayOptions(), 78 | shuffle: Boolean = false, 79 | ) = playQueue(listOf(songId), options = options, shuffle = shuffle) 80 | } 81 | -------------------------------------------------------------------------------- /app/src/main/java/io/github/zyrouge/symphony/ui/components/AlbumRow.kt: -------------------------------------------------------------------------------- 1 | package io.github.zyrouge.symphony.ui.components 2 | 3 | import androidx.compose.foundation.layout.Box 4 | import androidx.compose.foundation.layout.BoxWithConstraints 5 | import androidx.compose.foundation.layout.width 6 | import androidx.compose.foundation.lazy.LazyRow 7 | import androidx.compose.foundation.lazy.itemsIndexed 8 | import androidx.compose.runtime.Composable 9 | import androidx.compose.ui.Modifier 10 | import androidx.compose.ui.unit.dp 11 | import androidx.compose.ui.unit.min 12 | import io.github.zyrouge.symphony.services.groove.Groove 13 | import io.github.zyrouge.symphony.ui.helpers.ViewContext 14 | 15 | @Composable 16 | fun AlbumRow(context: ViewContext, albumIds: List<String>) { 17 | BoxWithConstraints { 18 | val maxSize = min( 19 | this@BoxWithConstraints.maxHeight, 20 | this@BoxWithConstraints.maxWidth, 21 | ).div(2f) 22 | val width = min(maxSize, 200.dp) 23 | 24 | LazyRow { 25 | itemsIndexed( 26 | albumIds, 27 | key = { i, x -> "$i-$x" }, 28 | contentType = { _, _ -> Groove.Kind.ALBUM } 29 | ) { _, albumId -> 30 | context.symphony.groove.album.get(albumId)?.let { album -> 31 | Box(modifier = Modifier.width(width)) { 32 | AlbumTile(context, album) 33 | } 34 | } 35 | } 36 | } 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /app/src/main/java/io/github/zyrouge/symphony/ui/components/ConfirmationDialog.kt: -------------------------------------------------------------------------------- 1 | package io.github.zyrouge.symphony.ui.components 2 | 3 | import androidx.compose.foundation.layout.Box 4 | import androidx.compose.foundation.layout.padding 5 | import androidx.compose.material3.Text 6 | import androidx.compose.material3.TextButton 7 | import androidx.compose.runtime.Composable 8 | import androidx.compose.ui.Modifier 9 | import androidx.compose.ui.unit.dp 10 | import io.github.zyrouge.symphony.ui.helpers.ViewContext 11 | 12 | @Composable 13 | fun ConfirmationDialog( 14 | context: ViewContext, 15 | title: @Composable () -> Unit, 16 | description: @Composable () -> Unit, 17 | onResult: (Boolean) -> Unit, 18 | ) { 19 | ScaffoldDialog( 20 | onDismissRequest = { onResult(false) }, 21 | title = title, 22 | content = { 23 | Box( 24 | modifier = Modifier.padding( 25 | start = 16.dp, 26 | end = 16.dp, 27 | top = 12.dp, 28 | ) 29 | ) { 30 | description() 31 | } 32 | }, 33 | actions = { 34 | TextButton(onClick = { onResult(false) }) { 35 | Text(context.symphony.t.No) 36 | } 37 | TextButton(onClick = { onResult(true) }) { 38 | Text(context.symphony.t.Yes) 39 | } 40 | } 41 | ) 42 | } 43 | -------------------------------------------------------------------------------- /app/src/main/java/io/github/zyrouge/symphony/ui/components/ContentDrawScopeScrollBar.kt: -------------------------------------------------------------------------------- 1 | package io.github.zyrouge.symphony.ui.components 2 | 3 | import androidx.compose.ui.geometry.CornerRadius 4 | import androidx.compose.ui.geometry.Offset 5 | import androidx.compose.ui.geometry.Rect 6 | import androidx.compose.ui.geometry.RoundRect 7 | import androidx.compose.ui.geometry.Size 8 | import androidx.compose.ui.graphics.Color 9 | import androidx.compose.ui.graphics.Path 10 | import androidx.compose.ui.graphics.drawscope.ContentDrawScope 11 | import androidx.compose.ui.unit.dp 12 | 13 | data object ContentDrawScopeScrollBarDefaults { 14 | val scrollPointerWidth = 4.dp 15 | val scrollPointerHeight = 16.dp 16 | } 17 | 18 | fun ContentDrawScope.drawScrollBar( 19 | scrollPointerColor: Color, 20 | scrollPointerOffsetY: Float, 21 | ) { 22 | val scrollPointerWidth = ContentDrawScopeScrollBarDefaults.scrollPointerWidth.toPx() 23 | val scrollPointerHeight = ContentDrawScopeScrollBarDefaults.scrollPointerHeight.toPx() 24 | val scrollPointerCorner = CornerRadius(scrollPointerWidth, scrollPointerWidth) 25 | 26 | drawPath( 27 | color = scrollPointerColor, 28 | path = Path().apply { 29 | addRoundRect( 30 | RoundRect( 31 | rect = Rect( 32 | offset = Offset( 33 | size.width - scrollPointerWidth, 34 | scrollPointerOffsetY, 35 | ), 36 | size = Size(scrollPointerWidth, scrollPointerHeight), 37 | ), 38 | topLeft = scrollPointerCorner, 39 | bottomLeft = scrollPointerCorner, 40 | ) 41 | ) 42 | }, 43 | ) 44 | } 45 | -------------------------------------------------------------------------------- /app/src/main/java/io/github/zyrouge/symphony/ui/components/ErrorComp.kt: -------------------------------------------------------------------------------- 1 | package io.github.zyrouge.symphony.ui.components 2 | 3 | import androidx.compose.foundation.background 4 | import androidx.compose.foundation.layout.Arrangement 5 | import androidx.compose.foundation.layout.Column 6 | import androidx.compose.foundation.layout.Spacer 7 | import androidx.compose.foundation.layout.fillMaxSize 8 | import androidx.compose.foundation.layout.fillMaxWidth 9 | import androidx.compose.foundation.layout.height 10 | import androidx.compose.foundation.layout.padding 11 | import androidx.compose.foundation.rememberScrollState 12 | import androidx.compose.foundation.verticalScroll 13 | import androidx.compose.material3.ProvideTextStyle 14 | import androidx.compose.material3.Text 15 | import androidx.compose.runtime.Composable 16 | import androidx.compose.ui.Modifier 17 | import androidx.compose.ui.graphics.Color 18 | import androidx.compose.ui.text.TextStyle 19 | import androidx.compose.ui.text.font.FontWeight 20 | import androidx.compose.ui.text.style.TextAlign 21 | import androidx.compose.ui.unit.dp 22 | import androidx.compose.ui.unit.sp 23 | import io.github.zyrouge.symphony.services.i18n.CommonTranslation 24 | 25 | @Composable 26 | fun ErrorComp(message: String, stackTrace: String) { 27 | Column( 28 | modifier = Modifier 29 | .background(Color.Red) 30 | .fillMaxSize() 31 | .verticalScroll(rememberScrollState()) 32 | .padding(20.dp), 33 | verticalArrangement = Arrangement.Center, 34 | ) { 35 | val normalTextStyle = TextStyle(color = Color.White) 36 | val boldTextStyle = normalTextStyle.copy(fontWeight = FontWeight.Bold) 37 | ProvideTextStyle(value = normalTextStyle) { 38 | Text( 39 | ":(", 40 | modifier = Modifier.fillMaxWidth(), 41 | textAlign = TextAlign.Center, 42 | style = boldTextStyle.copy(fontSize = 40.sp), 43 | ) 44 | Spacer(modifier = Modifier.height(20.dp)) 45 | Text( 46 | CommonTranslation.SomethingWentHorriblyWrong, 47 | modifier = Modifier.fillMaxWidth(), 48 | textAlign = TextAlign.Center, 49 | style = boldTextStyle, 50 | ) 51 | Spacer(modifier = Modifier.height(20.dp)) 52 | Text( 53 | CommonTranslation.ErrorX(message), 54 | style = boldTextStyle, 55 | ) 56 | Text(stackTrace) 57 | } 58 | } 59 | } 60 | -------------------------------------------------------------------------------- /app/src/main/java/io/github/zyrouge/symphony/ui/components/GenericSongListDropdown.kt: -------------------------------------------------------------------------------- 1 | package io.github.zyrouge.symphony.ui.components 2 | 3 | import androidx.compose.material.icons.Icons 4 | import androidx.compose.material.icons.automirrored.filled.PlaylistAdd 5 | import androidx.compose.material.icons.automirrored.filled.PlaylistPlay 6 | import androidx.compose.material3.DropdownMenu 7 | import androidx.compose.material3.DropdownMenuItem 8 | import androidx.compose.material3.Icon 9 | import androidx.compose.material3.Text 10 | import androidx.compose.runtime.Composable 11 | import androidx.compose.runtime.getValue 12 | import androidx.compose.runtime.mutableStateOf 13 | import androidx.compose.runtime.remember 14 | import androidx.compose.runtime.setValue 15 | import io.github.zyrouge.symphony.ui.helpers.ViewContext 16 | 17 | @Composable 18 | fun GenericSongListDropdown( 19 | context: ViewContext, 20 | songIds: List<String>, 21 | expanded: Boolean, 22 | onDismissRequest: () -> Unit, 23 | ) { 24 | var showAddToPlaylistDialog by remember { mutableStateOf(false) } 25 | 26 | DropdownMenu( 27 | expanded = expanded, 28 | onDismissRequest = onDismissRequest 29 | ) { 30 | DropdownMenuItem( 31 | leadingIcon = { 32 | Icon(Icons.AutoMirrored.Filled.PlaylistPlay, null) 33 | }, 34 | text = { 35 | Text(context.symphony.t.ShufflePlay) 36 | }, 37 | onClick = { 38 | onDismissRequest() 39 | context.symphony.radio.shorty.playQueue(songIds, shuffle = true) 40 | } 41 | ) 42 | DropdownMenuItem( 43 | leadingIcon = { 44 | Icon(Icons.AutoMirrored.Filled.PlaylistPlay, null) 45 | }, 46 | text = { 47 | Text(context.symphony.t.PlayNext) 48 | }, 49 | onClick = { 50 | onDismissRequest() 51 | context.symphony.radio.queue.add( 52 | songIds, 53 | context.symphony.radio.queue.currentSongIndex + 1 54 | ) 55 | } 56 | ) 57 | DropdownMenuItem( 58 | leadingIcon = { 59 | Icon(Icons.AutoMirrored.Filled.PlaylistPlay, null) 60 | }, 61 | text = { 62 | Text(context.symphony.t.AddToQueue) 63 | }, 64 | onClick = { 65 | onDismissRequest() 66 | context.symphony.radio.queue.add(songIds) 67 | } 68 | ) 69 | DropdownMenuItem( 70 | leadingIcon = { 71 | Icon(Icons.AutoMirrored.Filled.PlaylistAdd, null) 72 | }, 73 | text = { 74 | Text(context.symphony.t.AddToPlaylist) 75 | }, 76 | onClick = { 77 | onDismissRequest() 78 | showAddToPlaylistDialog = true 79 | } 80 | ) 81 | } 82 | 83 | if (showAddToPlaylistDialog) { 84 | AddToPlaylistDialog( 85 | context, 86 | songIds = songIds, 87 | onDismissRequest = { 88 | showAddToPlaylistDialog = false 89 | } 90 | ) 91 | } 92 | } 93 | -------------------------------------------------------------------------------- /app/src/main/java/io/github/zyrouge/symphony/ui/components/IconButtonPlaceholder.kt: -------------------------------------------------------------------------------- 1 | package io.github.zyrouge.symphony.ui.components 2 | 3 | import androidx.compose.foundation.layout.Box 4 | import androidx.compose.foundation.layout.size 5 | import androidx.compose.runtime.Composable 6 | import androidx.compose.ui.Modifier 7 | import androidx.compose.ui.unit.dp 8 | 9 | val IconButtonPlaceholderSize = 48.dp 10 | 11 | @Composable 12 | fun IconButtonPlaceholder() { 13 | Box(modifier = Modifier.size(IconButtonPlaceholderSize)) 14 | } 15 | -------------------------------------------------------------------------------- /app/src/main/java/io/github/zyrouge/symphony/ui/components/IconTextBody.kt: -------------------------------------------------------------------------------- 1 | package io.github.zyrouge.symphony.ui.components 2 | 3 | import androidx.compose.foundation.layout.* 4 | import androidx.compose.material3.MaterialTheme 5 | import androidx.compose.material3.ProvideTextStyle 6 | import androidx.compose.runtime.Composable 7 | import androidx.compose.ui.Alignment 8 | import androidx.compose.ui.Modifier 9 | import androidx.compose.ui.text.style.TextAlign 10 | import androidx.compose.ui.unit.dp 11 | 12 | @Composable 13 | fun IconTextBody(icon: @Composable (Modifier) -> Unit, content: @Composable () -> Unit) { 14 | Column( 15 | modifier = Modifier 16 | .padding(20.dp) 17 | .fillMaxSize(), 18 | horizontalAlignment = Alignment.CenterHorizontally, 19 | verticalArrangement = Arrangement.Center 20 | ) { 21 | IconTextBodyCompact(icon, content) 22 | } 23 | } 24 | 25 | @Composable 26 | fun IconTextBodyCompact(icon: @Composable (Modifier) -> Unit, content: @Composable () -> Unit) { 27 | Column(horizontalAlignment = Alignment.CenterHorizontally) { 28 | icon(Modifier.size(48.dp)) 29 | Spacer(modifier = Modifier.height(8.dp)) 30 | ProvideTextStyle(MaterialTheme.typography.bodyMedium.copy(textAlign = TextAlign.Center)) { 31 | content() 32 | } 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /app/src/main/java/io/github/zyrouge/symphony/ui/components/InformationDialog.kt: -------------------------------------------------------------------------------- 1 | package io.github.zyrouge.symphony.ui.components 2 | 3 | import androidx.compose.foundation.layout.Arrangement 4 | import androidx.compose.foundation.layout.Column 5 | import androidx.compose.foundation.layout.ColumnScope 6 | import androidx.compose.foundation.layout.padding 7 | import androidx.compose.foundation.rememberScrollState 8 | import androidx.compose.foundation.verticalScroll 9 | import androidx.compose.material3.MaterialTheme 10 | import androidx.compose.material3.ProvideTextStyle 11 | import androidx.compose.material3.Text 12 | import androidx.compose.runtime.Composable 13 | import androidx.compose.ui.Modifier 14 | import androidx.compose.ui.unit.dp 15 | import io.github.zyrouge.symphony.ui.helpers.ViewContext 16 | 17 | @Composable 18 | fun InformationDialog( 19 | context: ViewContext, 20 | content: @Composable (ColumnScope.() -> Unit), 21 | onDismissRequest: () -> Unit, 22 | ) { 23 | ScaffoldDialog( 24 | onDismissRequest = onDismissRequest, 25 | title = { 26 | Text(context.symphony.t.Details) 27 | }, 28 | content = { 29 | Column( 30 | verticalArrangement = Arrangement.spacedBy(12.dp), 31 | modifier = Modifier 32 | .padding(16.dp, 12.dp) 33 | .verticalScroll(rememberScrollState()), 34 | ) { 35 | content() 36 | } 37 | } 38 | ) 39 | } 40 | 41 | @Composable 42 | fun InformationKeyValue(key: String, value: @Composable () -> Unit) { 43 | Column { 44 | Text( 45 | key, 46 | style = MaterialTheme.typography.bodySmall.copy( 47 | color = MaterialTheme.colorScheme.onSurface.copy(alpha = 0.7f) 48 | ) 49 | ) 50 | ProvideTextStyle(value = MaterialTheme.typography.bodyMedium) { 51 | value() 52 | } 53 | } 54 | } 55 | -------------------------------------------------------------------------------- /app/src/main/java/io/github/zyrouge/symphony/ui/components/KeepScreenAwake.kt: -------------------------------------------------------------------------------- 1 | package io.github.zyrouge.symphony.ui.components 2 | 3 | import androidx.compose.runtime.Composable 4 | import androidx.compose.runtime.DisposableEffect 5 | import androidx.compose.ui.platform.LocalView 6 | 7 | @Composable 8 | fun KeepScreenAwake() { 9 | val view = LocalView.current 10 | 11 | DisposableEffect(Unit) { 12 | view.keepScreenOn = true 13 | onDispose { 14 | view.keepScreenOn = false 15 | } 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /app/src/main/java/io/github/zyrouge/symphony/ui/components/LazyColumnScrollBar.kt: -------------------------------------------------------------------------------- 1 | package io.github.zyrouge.symphony.ui.components 2 | 3 | import androidx.compose.animation.animateColorAsState 4 | import androidx.compose.animation.core.EaseInOut 5 | import androidx.compose.animation.core.animateFloatAsState 6 | import androidx.compose.animation.core.tween 7 | import androidx.compose.foundation.lazy.LazyListState 8 | import androidx.compose.material3.MaterialTheme 9 | import androidx.compose.runtime.derivedStateOf 10 | import androidx.compose.runtime.getValue 11 | import androidx.compose.runtime.mutableFloatStateOf 12 | import androidx.compose.runtime.remember 13 | import androidx.compose.runtime.setValue 14 | import androidx.compose.ui.Modifier 15 | import androidx.compose.ui.composed 16 | import androidx.compose.ui.draw.drawWithContent 17 | import io.github.zyrouge.symphony.utils.toSafeFinite 18 | 19 | fun Modifier.drawScrollBar(state: LazyListState): Modifier = composed { 20 | val scrollPointerColor = MaterialTheme.colorScheme.surfaceTint 21 | val isLastItemVisible by remember { 22 | derivedStateOf { 23 | state.layoutInfo.visibleItemsInfo.lastOrNull()?.index == state.layoutInfo.totalItemsCount - 1 24 | } 25 | } 26 | val showScrollPointer by remember { 27 | derivedStateOf { 28 | !(state.firstVisibleItemIndex == 0 && isLastItemVisible) 29 | } 30 | } 31 | val showScrollPointerColorAnimated by animateColorAsState( 32 | scrollPointerColor.copy(alpha = if (showScrollPointer) 1f else 0f), 33 | animationSpec = tween(durationMillis = 500), 34 | label = "c-lazy-column-scroll-pointer-color", 35 | ) 36 | var scrollPointerOffsetY by remember { mutableFloatStateOf(0f) } 37 | val scrollPointerOffsetYAnimated by animateFloatAsState( 38 | scrollPointerOffsetY, 39 | animationSpec = tween(durationMillis = 50, easing = EaseInOut), 40 | label = "c-lazy-column-scroll-pointer-offset-y", 41 | ) 42 | 43 | drawWithContent { 44 | drawContent() 45 | scrollPointerOffsetY = when { 46 | isLastItemVisible -> size.height - ContentDrawScopeScrollBarDefaults.scrollPointerHeight.toPx() 47 | else -> (size.height / state.layoutInfo.totalItemsCount) * state.firstVisibleItemIndex 48 | }.toSafeFinite() 49 | drawScrollBar( 50 | scrollPointerColor = showScrollPointerColorAnimated, 51 | scrollPointerOffsetY = scrollPointerOffsetYAnimated, 52 | ) 53 | } 54 | } 55 | -------------------------------------------------------------------------------- /app/src/main/java/io/github/zyrouge/symphony/ui/components/LazyGridScrollBar.kt: -------------------------------------------------------------------------------- 1 | package io.github.zyrouge.symphony.ui.components 2 | 3 | import androidx.compose.animation.animateColorAsState 4 | import androidx.compose.animation.core.animateFloatAsState 5 | import androidx.compose.animation.core.tween 6 | import androidx.compose.foundation.lazy.grid.LazyGridState 7 | import androidx.compose.material3.MaterialTheme 8 | import androidx.compose.runtime.derivedStateOf 9 | import androidx.compose.runtime.getValue 10 | import androidx.compose.runtime.mutableFloatStateOf 11 | import androidx.compose.runtime.remember 12 | import androidx.compose.runtime.setValue 13 | import androidx.compose.ui.Modifier 14 | import androidx.compose.ui.composed 15 | import androidx.compose.ui.draw.drawWithContent 16 | import io.github.zyrouge.symphony.utils.toSafeFinite 17 | import kotlin.math.floor 18 | 19 | fun Modifier.drawScrollBar(state: LazyGridState, columns: Int): Modifier = composed { 20 | val scrollPointerColor = MaterialTheme.colorScheme.surfaceTint 21 | val isLastItemVisible by remember { 22 | derivedStateOf { 23 | state.layoutInfo.visibleItemsInfo.lastOrNull()?.index == state.layoutInfo.totalItemsCount - 1 24 | } 25 | } 26 | val rows by remember { 27 | derivedStateOf { 28 | floor(state.layoutInfo.totalItemsCount.toFloat() / columns).toSafeFinite() 29 | } 30 | } 31 | val showScrollPointer by remember { 32 | derivedStateOf { 33 | !(state.firstVisibleItemIndex == 0 && isLastItemVisible) 34 | } 35 | } 36 | val showScrollPointerColorAnimated by animateColorAsState( 37 | scrollPointerColor.copy(alpha = if (showScrollPointer) 1f else 0f), 38 | animationSpec = tween(durationMillis = 500), 39 | label = "c-lazy-grid-scroll-pointer-color", 40 | ) 41 | var scrollPointerOffsetY by remember { mutableFloatStateOf(0f) } 42 | val scrollPointerOffsetYAnimated by animateFloatAsState( 43 | scrollPointerOffsetY, 44 | animationSpec = tween(durationMillis = 150), 45 | label = "c-lazy-grid-scroll-pointer-offset-y", 46 | ) 47 | 48 | drawWithContent { 49 | drawContent() 50 | val scrollBarHeight = 51 | size.height - ContentDrawScopeScrollBarDefaults.scrollPointerHeight.toPx() 52 | scrollPointerOffsetY = when { 53 | isLastItemVisible -> scrollBarHeight 54 | else -> (scrollBarHeight / rows) * (state.firstVisibleItemIndex / columns) 55 | }.toSafeFinite() 56 | drawScrollBar( 57 | scrollPointerColor = showScrollPointerColorAnimated, 58 | scrollPointerOffsetY = scrollPointerOffsetYAnimated, 59 | ) 60 | } 61 | } 62 | -------------------------------------------------------------------------------- /app/src/main/java/io/github/zyrouge/symphony/ui/components/LongPressCopyableText.kt: -------------------------------------------------------------------------------- 1 | package io.github.zyrouge.symphony.ui.components 2 | 3 | import androidx.compose.foundation.gestures.detectTapGestures 4 | import androidx.compose.material3.Text 5 | import androidx.compose.runtime.Composable 6 | import androidx.compose.ui.Modifier 7 | import androidx.compose.ui.input.pointer.pointerInput 8 | import io.github.zyrouge.symphony.ui.helpers.ViewContext 9 | import io.github.zyrouge.symphony.utils.ActivityUtils 10 | 11 | @Composable 12 | fun LongPressCopyableText(context: ViewContext, text: String) { 13 | Text( 14 | text, 15 | modifier = Modifier.pointerInput(Unit) { 16 | detectTapGestures(onLongPress = { 17 | ActivityUtils.copyToClipboardAndNotify(context.symphony, text) 18 | }) 19 | } 20 | ) 21 | } 22 | -------------------------------------------------------------------------------- /app/src/main/java/io/github/zyrouge/symphony/ui/components/MediaSortBarScaffold.kt: -------------------------------------------------------------------------------- 1 | package io.github.zyrouge.symphony.ui.components 2 | 3 | import androidx.compose.foundation.layout.Box 4 | import androidx.compose.foundation.layout.fillMaxSize 5 | import androidx.compose.foundation.layout.padding 6 | import androidx.compose.runtime.Composable 7 | import androidx.compose.runtime.getValue 8 | import androidx.compose.runtime.mutableIntStateOf 9 | import androidx.compose.runtime.remember 10 | import androidx.compose.runtime.setValue 11 | import androidx.compose.ui.Modifier 12 | import androidx.compose.ui.layout.onGloballyPositioned 13 | import androidx.compose.ui.platform.LocalDensity 14 | 15 | @Composable 16 | fun MediaSortBarScaffold( 17 | mediaSortBar: @Composable () -> Unit, 18 | content: @Composable () -> Unit, 19 | ) { 20 | val density = LocalDensity.current 21 | var height by remember { mutableIntStateOf(0) } 22 | 23 | Box(modifier = Modifier.fillMaxSize()) { 24 | Box( 25 | modifier = Modifier.onGloballyPositioned { 26 | height = it.size.height 27 | } 28 | ) { 29 | mediaSortBar() 30 | } 31 | Box( 32 | modifier = Modifier 33 | .padding(top = with(density) { height.toDp() }) 34 | ) { 35 | content() 36 | } 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /app/src/main/java/io/github/zyrouge/symphony/ui/components/PlaylistInformationDialog.kt: -------------------------------------------------------------------------------- 1 | package io.github.zyrouge.symphony.ui.components 2 | 3 | import androidx.compose.runtime.Composable 4 | import io.github.zyrouge.symphony.services.groove.Playlist 5 | import io.github.zyrouge.symphony.ui.helpers.ViewContext 6 | 7 | @Composable 8 | fun PlaylistInformationDialog( 9 | context: ViewContext, 10 | playlist: Playlist, 11 | onDismissRequest: () -> Unit, 12 | ) { 13 | InformationDialog( 14 | context, 15 | content = { 16 | InformationKeyValue(context.symphony.t.Id) { 17 | LongPressCopyableText(context, playlist.id) 18 | } 19 | InformationKeyValue(context.symphony.t.Title) { 20 | LongPressCopyableText(context, playlist.title) 21 | } 22 | InformationKeyValue(context.symphony.t.TrackCount) { 23 | LongPressCopyableText(context, playlist.numberOfTracks.toString()) 24 | } 25 | InformationKeyValue(context.symphony.t.PlaylistStoreLocation) { 26 | LongPressCopyableText( 27 | context, 28 | when { 29 | playlist.isLocal -> context.symphony.t.LocalStorage 30 | else -> context.symphony.t.AppBuiltIn 31 | } 32 | ) 33 | } 34 | playlist.path?.let { 35 | InformationKeyValue(context.symphony.t.Path) { 36 | LongPressCopyableText(context, it) 37 | } 38 | } 39 | }, 40 | onDismissRequest = onDismissRequest, 41 | ) 42 | } 43 | -------------------------------------------------------------------------------- /app/src/main/java/io/github/zyrouge/symphony/ui/components/RenamePlaylistDialog.kt: -------------------------------------------------------------------------------- 1 | package io.github.zyrouge.symphony.ui.components 2 | 3 | import androidx.compose.foundation.layout.Box 4 | import androidx.compose.foundation.layout.fillMaxWidth 5 | import androidx.compose.foundation.layout.padding 6 | import androidx.compose.material3.DividerDefaults 7 | import androidx.compose.material3.OutlinedTextField 8 | import androidx.compose.material3.Text 9 | import androidx.compose.material3.TextButton 10 | import androidx.compose.material3.TextFieldDefaults 11 | import androidx.compose.runtime.Composable 12 | import androidx.compose.runtime.getValue 13 | import androidx.compose.runtime.mutableStateOf 14 | import androidx.compose.runtime.remember 15 | import androidx.compose.runtime.setValue 16 | import androidx.compose.ui.Modifier 17 | import androidx.compose.ui.focus.FocusRequester 18 | import androidx.compose.ui.focus.focusRequester 19 | import androidx.compose.ui.graphics.Color 20 | import androidx.compose.ui.unit.dp 21 | import io.github.zyrouge.symphony.services.groove.Playlist 22 | import io.github.zyrouge.symphony.ui.helpers.ViewContext 23 | 24 | @Composable 25 | fun RenamePlaylistDialog( 26 | context: ViewContext, 27 | playlist: Playlist, 28 | onRename: () -> Unit = {}, 29 | onDismissRequest: () -> Unit, 30 | ) { 31 | var input by remember { mutableStateOf(playlist.title) } 32 | val focusRequester = remember { FocusRequester() } 33 | 34 | ScaffoldDialog( 35 | onDismissRequest = onDismissRequest, 36 | title = { 37 | Text(context.symphony.t.RenamePlaylist) 38 | }, 39 | content = { 40 | Box( 41 | modifier = Modifier 42 | .padding(start = 20.dp, end = 20.dp, top = 12.dp) 43 | ) { 44 | OutlinedTextField( 45 | modifier = Modifier 46 | .fillMaxWidth() 47 | .focusRequester(focusRequester), 48 | singleLine = true, 49 | colors = TextFieldDefaults.colors( 50 | focusedContainerColor = Color.Transparent, 51 | unfocusedContainerColor = Color.Transparent, 52 | unfocusedIndicatorColor = DividerDefaults.color, 53 | ), 54 | value = input, 55 | onValueChange = { 56 | input = it 57 | } 58 | ) 59 | } 60 | }, 61 | actions = { 62 | TextButton(onClick = onDismissRequest) { 63 | Text(context.symphony.t.Cancel) 64 | } 65 | TextButton( 66 | enabled = input.isNotBlank() && input != playlist.title, 67 | onClick = { 68 | onRename() 69 | onDismissRequest() 70 | context.symphony.groove.playlist.renamePlaylist(playlist, input) 71 | } 72 | ) { 73 | Text(context.symphony.t.Done) 74 | } 75 | }, 76 | ) 77 | } 78 | -------------------------------------------------------------------------------- /app/src/main/java/io/github/zyrouge/symphony/ui/components/Snackbar.kt: -------------------------------------------------------------------------------- 1 | package io.github.zyrouge.symphony.ui.components 2 | 3 | import androidx.compose.material3.MaterialTheme 4 | import androidx.compose.material3.Snackbar 5 | import androidx.compose.material3.SnackbarData 6 | import androidx.compose.runtime.Composable 7 | 8 | @Composable 9 | fun AdaptiveSnackbar(snackbarData: SnackbarData) = Snackbar( 10 | snackbarData = snackbarData, 11 | containerColor = MaterialTheme.colorScheme.surface, 12 | contentColor = MaterialTheme.colorScheme.onSurface, 13 | actionColor = MaterialTheme.colorScheme.primary, 14 | actionContentColor = MaterialTheme.colorScheme.primary, 15 | dismissActionContentColor = MaterialTheme.colorScheme.onSurface, 16 | ) 17 | -------------------------------------------------------------------------------- /app/src/main/java/io/github/zyrouge/symphony/ui/components/SubtleCaptionText.kt: -------------------------------------------------------------------------------- 1 | package io.github.zyrouge.symphony.ui.components 2 | 3 | import androidx.compose.foundation.layout.fillMaxWidth 4 | import androidx.compose.foundation.layout.padding 5 | import androidx.compose.material3.MaterialTheme 6 | import androidx.compose.material3.Text 7 | import androidx.compose.runtime.Composable 8 | import androidx.compose.ui.Modifier 9 | import androidx.compose.ui.text.style.TextAlign 10 | import androidx.compose.ui.unit.dp 11 | 12 | @Composable 13 | fun SubtleCaptionText(text: String) { 14 | Text( 15 | text, 16 | textAlign = TextAlign.Center, 17 | style = MaterialTheme.typography.bodyMedium.copy( 18 | color = MaterialTheme.colorScheme.onSurface.copy(alpha = 0.7f) 19 | ), 20 | modifier = Modifier 21 | .fillMaxWidth() 22 | .padding(0.dp, 20.dp), 23 | ) 24 | } 25 | -------------------------------------------------------------------------------- /app/src/main/java/io/github/zyrouge/symphony/ui/components/Swipeable.kt: -------------------------------------------------------------------------------- 1 | package io.github.zyrouge.symphony.ui.components 2 | 3 | import androidx.compose.foundation.gestures.detectDragGestures 4 | import androidx.compose.ui.Modifier 5 | import androidx.compose.ui.geometry.Offset 6 | import androidx.compose.ui.input.pointer.pointerInput 7 | import kotlin.math.absoluteValue 8 | 9 | fun Modifier.swipeable( 10 | minimumDragAmount: Float = 50f, 11 | onSwipeLeft: (() -> Unit)? = null, 12 | onSwipeRight: (() -> Unit)? = null, 13 | onSwipeUp: (() -> Unit)? = null, 14 | onSwipeDown: (() -> Unit)? = null, 15 | ) = pointerInput(Unit) { 16 | var offset = Offset.Zero 17 | detectDragGestures( 18 | onDrag = { pointer, dragAmount -> 19 | pointer.consume() 20 | offset += dragAmount 21 | }, 22 | onDragEnd = { 23 | val xAbs = offset.x.absoluteValue 24 | val yAbs = offset.y.absoluteValue 25 | when { 26 | xAbs > minimumDragAmount && xAbs > yAbs -> when { 27 | offset.x > 0 -> onSwipeRight?.invoke() 28 | else -> onSwipeLeft?.invoke() 29 | } 30 | 31 | yAbs > minimumDragAmount -> when { 32 | offset.y > 0 -> onSwipeDown?.invoke() 33 | else -> onSwipeUp?.invoke() 34 | } 35 | } 36 | offset = Offset.Zero 37 | }, 38 | onDragCancel = { 39 | offset = Offset.Zero 40 | } 41 | ) 42 | } 43 | -------------------------------------------------------------------------------- /app/src/main/java/io/github/zyrouge/symphony/ui/components/TopAppBarMinimalTitle.kt: -------------------------------------------------------------------------------- 1 | package io.github.zyrouge.symphony.ui.components 2 | 3 | import androidx.compose.foundation.layout.Box 4 | import androidx.compose.foundation.layout.fillMaxWidth 5 | import androidx.compose.foundation.layout.padding 6 | import androidx.compose.material3.MaterialTheme 7 | import androidx.compose.material3.ProvideTextStyle 8 | import androidx.compose.runtime.Composable 9 | import androidx.compose.ui.Alignment 10 | import androidx.compose.ui.Modifier 11 | import androidx.compose.ui.text.font.FontWeight 12 | import androidx.compose.ui.text.style.TextAlign 13 | import androidx.compose.ui.unit.dp 14 | import androidx.compose.ui.unit.sp 15 | import io.github.zyrouge.symphony.utils.runIfOrThis 16 | 17 | @Composable 18 | fun TopAppBarMinimalTitle( 19 | modifier: Modifier = Modifier, 20 | fillMaxWidth: Boolean = true, 21 | content: @Composable () -> Unit, 22 | ) { 23 | Box( 24 | modifier = modifier 25 | .runIfOrThis(fillMaxWidth) { fillMaxWidth() } 26 | .padding(8.dp), 27 | contentAlignment = Alignment.Center, 28 | ) { 29 | ProvideTextStyle( 30 | MaterialTheme.typography.bodyLarge.copy( 31 | fontWeight = FontWeight.Bold, 32 | letterSpacing = 0.sp, 33 | textAlign = TextAlign.Center, 34 | ) 35 | ) { 36 | content() 37 | } 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /app/src/main/java/io/github/zyrouge/symphony/ui/components/settings/ConsiderContributingTile.kt: -------------------------------------------------------------------------------- 1 | package io.github.zyrouge.symphony.ui.components.settings 2 | 3 | import android.net.Uri 4 | import androidx.compose.foundation.background 5 | import androidx.compose.foundation.clickable 6 | import androidx.compose.foundation.layout.Arrangement 7 | import androidx.compose.foundation.layout.Box 8 | import androidx.compose.foundation.layout.Row 9 | import androidx.compose.foundation.layout.fillMaxWidth 10 | import androidx.compose.foundation.layout.padding 11 | import androidx.compose.foundation.layout.size 12 | import androidx.compose.foundation.layout.width 13 | import androidx.compose.material.icons.Icons 14 | import androidx.compose.material.icons.filled.East 15 | import androidx.compose.material.icons.filled.Favorite 16 | import androidx.compose.material3.Icon 17 | import androidx.compose.material3.MaterialTheme 18 | import androidx.compose.material3.Text 19 | import androidx.compose.runtime.Composable 20 | import androidx.compose.ui.Alignment 21 | import androidx.compose.ui.Modifier 22 | import androidx.compose.ui.text.font.FontWeight 23 | import androidx.compose.ui.unit.dp 24 | import io.github.zyrouge.symphony.services.AppMeta 25 | import io.github.zyrouge.symphony.ui.helpers.ViewContext 26 | import io.github.zyrouge.symphony.utils.ActivityUtils 27 | 28 | @Composable 29 | fun ConsiderContributingTile(context: ViewContext) { 30 | val contentColor = MaterialTheme.colorScheme.onPrimary 31 | 32 | Box( 33 | modifier = Modifier 34 | .fillMaxWidth() 35 | .background(MaterialTheme.colorScheme.primary) 36 | .clickable { 37 | ActivityUtils.startBrowserActivity( 38 | context.activity, 39 | Uri.parse(AppMeta.contributingUrl) 40 | ) 41 | } 42 | ) { 43 | Row( 44 | modifier = Modifier 45 | .fillMaxWidth() 46 | .padding(24.dp, 8.dp), 47 | horizontalArrangement = Arrangement.Center, 48 | verticalAlignment = Alignment.CenterVertically, 49 | ) { 50 | Icon( 51 | Icons.Filled.Favorite, 52 | null, 53 | tint = contentColor, 54 | modifier = Modifier.size(12.dp), 55 | ) 56 | Box(modifier = Modifier.width(4.dp)) 57 | Text( 58 | context.symphony.t.ConsiderContributing, 59 | style = MaterialTheme.typography.labelLarge.copy( 60 | fontWeight = FontWeight.Bold, 61 | color = contentColor, 62 | ), 63 | ) 64 | } 65 | Box( 66 | modifier = Modifier 67 | .align(Alignment.CenterEnd) 68 | .padding(8.dp, 0.dp) 69 | ) { 70 | Icon( 71 | Icons.Filled.East, 72 | null, 73 | tint = contentColor, 74 | modifier = Modifier.size(20.dp), 75 | ) 76 | } 77 | } 78 | } -------------------------------------------------------------------------------- /app/src/main/java/io/github/zyrouge/symphony/ui/components/settings/LinkTile.kt: -------------------------------------------------------------------------------- 1 | package io.github.zyrouge.symphony.ui.components.settings 2 | 3 | import android.net.Uri 4 | import androidx.compose.material3.Card 5 | import androidx.compose.material3.ExperimentalMaterial3Api 6 | import androidx.compose.material3.ListItem 7 | import androidx.compose.material3.Text 8 | import androidx.compose.runtime.Composable 9 | import io.github.zyrouge.symphony.ui.helpers.ViewContext 10 | import io.github.zyrouge.symphony.utils.ActivityUtils 11 | 12 | @OptIn(ExperimentalMaterial3Api::class) 13 | @Composable 14 | fun SettingsLinkTile( 15 | context: ViewContext, 16 | icon: @Composable () -> Unit, 17 | title: @Composable () -> Unit, 18 | url: String, 19 | ) { 20 | Card( 21 | colors = SettingsTileDefaults.cardColors(), 22 | onClick = { 23 | ActivityUtils.startBrowserActivity(context.activity, Uri.parse(url)) 24 | } 25 | ) { 26 | ListItem( 27 | colors = SettingsTileDefaults.listItemColors(), 28 | leadingContent = { icon() }, 29 | headlineContent = { title() }, 30 | supportingContent = { Text(url) } 31 | ) 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /app/src/main/java/io/github/zyrouge/symphony/ui/components/settings/SideHeading.kt: -------------------------------------------------------------------------------- 1 | package io.github.zyrouge.symphony.ui.components.settings 2 | 3 | import androidx.compose.foundation.layout.Box 4 | import androidx.compose.foundation.layout.padding 5 | import androidx.compose.material3.MaterialTheme 6 | import androidx.compose.material3.Text 7 | import androidx.compose.runtime.Composable 8 | import androidx.compose.ui.Modifier 9 | import androidx.compose.ui.unit.dp 10 | 11 | @Composable 12 | fun SettingsSideHeading(text: String) { 13 | Box(modifier = Modifier.padding(start = 16.dp, end = 16.dp, top = 16.dp, bottom = 8.dp)) { 14 | Text( 15 | text, 16 | style = MaterialTheme.typography.labelMedium.copy( 17 | color = MaterialTheme.colorScheme.primary 18 | ) 19 | ) 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /app/src/main/java/io/github/zyrouge/symphony/ui/components/settings/SimpleTile.kt: -------------------------------------------------------------------------------- 1 | package io.github.zyrouge.symphony.ui.components.settings 2 | 3 | import androidx.compose.material3.* 4 | import androidx.compose.runtime.* 5 | 6 | @OptIn(ExperimentalMaterial3Api::class) 7 | @Composable 8 | fun SettingsSimpleTile( 9 | icon: @Composable () -> Unit, 10 | title: @Composable () -> Unit, 11 | subtitle: (@Composable () -> Unit)? = null, 12 | onClick: () -> Unit = {}, 13 | ) { 14 | Card( 15 | colors = SettingsTileDefaults.cardColors(), 16 | onClick = onClick 17 | ) { 18 | ListItem( 19 | colors = SettingsTileDefaults.listItemColors(), 20 | leadingContent = { icon() }, 21 | headlineContent = { title() }, 22 | supportingContent = { subtitle?.let { it() } } 23 | ) 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /app/src/main/java/io/github/zyrouge/symphony/ui/components/settings/SwitchTile.kt: -------------------------------------------------------------------------------- 1 | package io.github.zyrouge.symphony.ui.components.settings 2 | 3 | import androidx.compose.material3.Card 4 | import androidx.compose.material3.ExperimentalMaterial3Api 5 | import androidx.compose.material3.ListItem 6 | import androidx.compose.material3.Switch 7 | import androidx.compose.runtime.Composable 8 | 9 | @OptIn(ExperimentalMaterial3Api::class) 10 | @Composable 11 | fun SettingsSwitchTile( 12 | icon: @Composable () -> Unit, 13 | title: @Composable () -> Unit, 14 | value: Boolean, 15 | onChange: (Boolean) -> Unit, 16 | ) { 17 | Card( 18 | colors = SettingsTileDefaults.cardColors(), 19 | onClick = { 20 | onChange(!value) 21 | } 22 | ) { 23 | ListItem( 24 | colors = SettingsTileDefaults.listItemColors(), 25 | leadingContent = { icon() }, 26 | headlineContent = { title() }, 27 | trailingContent = { 28 | Switch( 29 | checked = value, 30 | onCheckedChange = { 31 | onChange(!value) 32 | } 33 | ) 34 | } 35 | ) 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /app/src/main/java/io/github/zyrouge/symphony/ui/components/settings/Tile.kt: -------------------------------------------------------------------------------- 1 | package io.github.zyrouge.symphony.ui.components.settings 2 | 3 | import androidx.compose.material3.CardDefaults 4 | import androidx.compose.material3.ListItemDefaults 5 | import androidx.compose.material3.MaterialTheme 6 | import androidx.compose.runtime.Composable 7 | import androidx.compose.ui.graphics.Color 8 | 9 | object SettingsTileDefaults { 10 | @Composable 11 | fun cardColors() = CardDefaults.cardColors( 12 | containerColor = Color.Transparent, 13 | disabledContainerColor = Color.Transparent, 14 | ) 15 | 16 | @Composable 17 | fun listItemColors(enabled: Boolean = true) = when { 18 | enabled -> ListItemDefaults.colors(containerColor = Color.Transparent) 19 | else -> ListItemDefaults.colors( 20 | containerColor = Color.Transparent, 21 | leadingIconColor = MaterialTheme.colorScheme.onSurface.copy(alpha = 0.7f), 22 | trailingIconColor = MaterialTheme.colorScheme.onSurface.copy(alpha = 0.7f), 23 | headlineColor = MaterialTheme.colorScheme.onSurface.copy(alpha = 0.7f), 24 | supportingColor = MaterialTheme.colorScheme.onSurface.copy(alpha = 0.7f), 25 | ) 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /app/src/main/java/io/github/zyrouge/symphony/ui/helpers/Assets.kt: -------------------------------------------------------------------------------- 1 | package io.github.zyrouge.symphony.ui.helpers 2 | 3 | import android.content.ContentResolver 4 | import android.content.res.Resources 5 | import android.net.Uri 6 | import coil.request.ImageRequest 7 | import io.github.zyrouge.symphony.R 8 | import io.github.zyrouge.symphony.Symphony 9 | import io.github.zyrouge.symphony.ui.theme.isLight 10 | import io.github.zyrouge.symphony.ui.theme.toColorSchemeMode 11 | 12 | object Assets { 13 | val placeholderDarkId = R.raw.placeholder_dark 14 | val placeholderLightId = R.raw.placeholder_light 15 | 16 | private fun getPlaceholderId(isLight: Boolean = false) = when { 17 | isLight -> placeholderLightId 18 | else -> placeholderDarkId 19 | } 20 | 21 | fun getPlaceholderId(symphony: Symphony) = getPlaceholderId( 22 | isLight = symphony.settings.themeMode.value.toColorSchemeMode(symphony) 23 | .isLight(), 24 | ) 25 | 26 | fun getPlaceholderUri(symphony: Symphony) = buildUriOfResource( 27 | symphony.applicationContext.resources, 28 | getPlaceholderId(symphony), 29 | ) 30 | 31 | fun createPlaceholderImageRequest(symphony: Symphony) = 32 | ImageRequest.Builder(symphony.applicationContext) 33 | .data(getPlaceholderUri(symphony)) 34 | 35 | private fun buildUriOfResource(resources: Resources, resourceId: Int): Uri { 36 | return Uri.Builder() 37 | .scheme(ContentResolver.SCHEME_ANDROID_RESOURCE) 38 | .authority(resources.getResourcePackageName(resourceId)) 39 | .appendPath(resources.getResourceTypeName(resourceId)) 40 | .appendPath(resources.getResourceEntryName(resourceId)) 41 | .build() 42 | } 43 | } 44 | -------------------------------------------------------------------------------- /app/src/main/java/io/github/zyrouge/symphony/ui/helpers/Context.kt: -------------------------------------------------------------------------------- 1 | package io.github.zyrouge.symphony.ui.helpers 2 | 3 | import androidx.navigation.NavHostController 4 | import io.github.zyrouge.symphony.MainActivity 5 | import io.github.zyrouge.symphony.Symphony 6 | 7 | data class ViewContext( 8 | val symphony: Symphony, 9 | val activity: MainActivity, 10 | val navController: NavHostController, 11 | ) { 12 | companion object { 13 | fun <T> parameterizedFn(fn: (ViewContext) -> T) = fn 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /app/src/main/java/io/github/zyrouge/symphony/ui/helpers/SimpleFileSystem.kt: -------------------------------------------------------------------------------- 1 | package io.github.zyrouge.symphony.ui.helpers 2 | 3 | import io.github.zyrouge.symphony.utils.SimpleFileSystem 4 | import io.github.zyrouge.symphony.utils.SimplePath 5 | 6 | fun SimpleFileSystem.Folder.navigateToFolder(path: SimplePath): SimpleFileSystem.Folder? { 7 | var folder: SimpleFileSystem.Folder? = this 8 | path.parts.forEach { x -> 9 | folder = folder?.let { 10 | val child = it.children[x] 11 | child as? SimpleFileSystem.Folder 12 | } 13 | } 14 | return folder 15 | } 16 | -------------------------------------------------------------------------------- /app/src/main/java/io/github/zyrouge/symphony/ui/helpers/UserInterface.kt: -------------------------------------------------------------------------------- 1 | package io.github.zyrouge.symphony.ui.helpers 2 | 3 | import android.content.Context 4 | import android.content.res.Configuration 5 | import androidx.compose.foundation.layout.BoxWithConstraintsScope 6 | import androidx.compose.ui.unit.Dp 7 | import coil.request.ImageRequest 8 | 9 | enum class ScreenOrientation { 10 | PORTRAIT, 11 | LANDSCAPE; 12 | 13 | val isPortrait: Boolean get() = this == PORTRAIT 14 | val isLandscape: Boolean get() = this == LANDSCAPE 15 | 16 | companion object { 17 | fun fromConfiguration(configuration: Configuration) = when (configuration.orientation) { 18 | Configuration.ORIENTATION_LANDSCAPE -> LANDSCAPE 19 | else -> PORTRAIT 20 | } 21 | 22 | fun fromConstraints(constraints: BoxWithConstraintsScope) = 23 | fromDimension(constraints.maxHeight, constraints.maxWidth) 24 | 25 | fun fromDimension(height: Dp, width: Dp) = when { 26 | width.value > height.value -> LANDSCAPE 27 | else -> PORTRAIT 28 | } 29 | } 30 | } 31 | 32 | fun createHandyImageRequest(context: Context, image: Any, fallback: Int) = 33 | createHandyImageRequest(context, image, fallbackResId = fallback) 34 | 35 | private fun createHandyImageRequest( 36 | context: Context, 37 | image: Any, 38 | fallbackResId: Int? = null, 39 | ) = ImageRequest.Builder(context).apply { 40 | data(image) 41 | fallbackResId?.let { 42 | placeholder(it) 43 | fallback(it) 44 | error(it) 45 | } 46 | crossfade(true) 47 | } 48 | -------------------------------------------------------------------------------- /app/src/main/java/io/github/zyrouge/symphony/ui/theme/Color.kt: -------------------------------------------------------------------------------- 1 | package io.github.zyrouge.symphony.ui.theme 2 | 3 | import androidx.compose.ui.graphics.Color 4 | 5 | enum class PrimaryThemeColor { 6 | Red, 7 | Orange, 8 | Amber, 9 | Yellow, 10 | Lime, 11 | Green, 12 | Emerald, 13 | Teal, 14 | Cyan, 15 | Sky, 16 | Blue, 17 | Indigo, 18 | Violet, 19 | Purple, 20 | Fuchsia, 21 | Pink, 22 | Rose; 23 | } 24 | 25 | object ThemeColors { 26 | val Red = Color(0xFFEF4444) 27 | val Orange = Color(0xFFF97316) 28 | val Amber = Color(0xFFF59E0B) 29 | val Yellow = Color(0xFFEAB308) 30 | val Lime = Color(0xFF84CC16) 31 | val Green = Color(0xFF22C55E) 32 | val Emerald = Color(0xFF10B981) 33 | val Teal = Color(0xFF14B8A6) 34 | val Cyan = Color(0xFF06B6D4) 35 | val Sky = Color(0xFF0EA5E9) 36 | val Blue = Color(0xFF3B82F6) 37 | val Indigo = Color(0xFF6366f1) 38 | val Violet = Color(0xFF8B5CF6) 39 | val Purple = Color(0xFFA855F7) 40 | val Fuchsia = Color(0xFFD946EF) 41 | val Pink = Color(0xFFEC4899) 42 | val Rose = Color(0xFFF43f5E) 43 | 44 | val Neutral50 = Color(0xFFFAFAFA) 45 | val Neutral100 = Color(0xFFF5F5F5) 46 | val Neutral200 = Color(0xFFE5E5E5) 47 | val Neutral800 = Color(0xFF262626) 48 | val Neutral900 = Color(0xFF171717) 49 | 50 | val DefaultPrimaryColor = PrimaryThemeColor.Purple 51 | val PrimaryColorsMap = mapOf( 52 | PrimaryThemeColor.Red to Red, 53 | PrimaryThemeColor.Orange to Orange, 54 | PrimaryThemeColor.Amber to Amber, 55 | PrimaryThemeColor.Yellow to Yellow, 56 | PrimaryThemeColor.Lime to Lime, 57 | PrimaryThemeColor.Green to Green, 58 | PrimaryThemeColor.Emerald to Emerald, 59 | PrimaryThemeColor.Teal to Teal, 60 | PrimaryThemeColor.Cyan to Cyan, 61 | PrimaryThemeColor.Sky to Sky, 62 | PrimaryThemeColor.Blue to Blue, 63 | PrimaryThemeColor.Indigo to Indigo, 64 | PrimaryThemeColor.Violet to Violet, 65 | PrimaryThemeColor.Purple to Purple, 66 | PrimaryThemeColor.Fuchsia to Fuchsia, 67 | PrimaryThemeColor.Pink to Pink, 68 | PrimaryThemeColor.Rose to Rose, 69 | ) 70 | 71 | fun resolvePrimaryColorKey(value: String?) = 72 | PrimaryThemeColor.entries.find { it.name == value } ?: DefaultPrimaryColor 73 | 74 | fun resolvePrimaryColor(value: PrimaryThemeColor) = PrimaryColorsMap[value]!! 75 | fun resolvePrimaryColor(value: String?) = resolvePrimaryColor(resolvePrimaryColorKey(value)) 76 | } 77 | -------------------------------------------------------------------------------- /app/src/main/java/io/github/zyrouge/symphony/ui/view/home/AlbumArtists.kt: -------------------------------------------------------------------------------- 1 | package io.github.zyrouge.symphony.ui.view.home 2 | 3 | import androidx.compose.runtime.Composable 4 | import androidx.compose.runtime.collectAsState 5 | import androidx.compose.runtime.getValue 6 | import io.github.zyrouge.symphony.ui.components.AlbumArtistGrid 7 | import io.github.zyrouge.symphony.ui.components.LoaderScaffold 8 | import io.github.zyrouge.symphony.ui.helpers.ViewContext 9 | 10 | @Composable 11 | fun AlbumArtistsView(context: ViewContext) { 12 | val isUpdating by context.symphony.groove.albumArtist.isUpdating.collectAsState() 13 | val albumArtistNames by context.symphony.groove.albumArtist.all.collectAsState() 14 | val albumArtistsCount by context.symphony.groove.albumArtist.count.collectAsState() 15 | 16 | LoaderScaffold(context, isLoading = isUpdating) { 17 | AlbumArtistGrid( 18 | context, 19 | albumArtistNames = albumArtistNames, 20 | albumArtistsCount = albumArtistsCount, 21 | ) 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /app/src/main/java/io/github/zyrouge/symphony/ui/view/home/Albums.kt: -------------------------------------------------------------------------------- 1 | package io.github.zyrouge.symphony.ui.view.home 2 | 3 | import androidx.compose.runtime.Composable 4 | import androidx.compose.runtime.collectAsState 5 | import androidx.compose.runtime.getValue 6 | import io.github.zyrouge.symphony.ui.components.AlbumGrid 7 | import io.github.zyrouge.symphony.ui.components.LoaderScaffold 8 | import io.github.zyrouge.symphony.ui.helpers.ViewContext 9 | 10 | @Composable 11 | fun AlbumsView(context: ViewContext) { 12 | val isUpdating by context.symphony.groove.album.isUpdating.collectAsState() 13 | val albumIds by context.symphony.groove.album.all.collectAsState() 14 | val albumsCount by context.symphony.groove.album.count.collectAsState() 15 | 16 | LoaderScaffold(context, isLoading = isUpdating) { 17 | AlbumGrid( 18 | context, 19 | albumIds = albumIds, 20 | albumsCount = albumsCount, 21 | ) 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /app/src/main/java/io/github/zyrouge/symphony/ui/view/home/Artists.kt: -------------------------------------------------------------------------------- 1 | package io.github.zyrouge.symphony.ui.view.home 2 | 3 | import androidx.compose.runtime.Composable 4 | import androidx.compose.runtime.collectAsState 5 | import androidx.compose.runtime.getValue 6 | import io.github.zyrouge.symphony.ui.components.ArtistGrid 7 | import io.github.zyrouge.symphony.ui.components.LoaderScaffold 8 | import io.github.zyrouge.symphony.ui.helpers.ViewContext 9 | 10 | @Composable 11 | fun ArtistsView(context: ViewContext) { 12 | val isUpdating by context.symphony.groove.artist.isUpdating.collectAsState() 13 | val artistNames by context.symphony.groove.artist.all.collectAsState() 14 | val artistsCount by context.symphony.groove.artist.count.collectAsState() 15 | 16 | LoaderScaffold(context, isLoading = isUpdating) { 17 | ArtistGrid( 18 | context, 19 | artistName = artistNames, 20 | artistsCount = artistsCount, 21 | ) 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /app/src/main/java/io/github/zyrouge/symphony/ui/view/home/Browser.kt: -------------------------------------------------------------------------------- 1 | package io.github.zyrouge.symphony.ui.view.home 2 | 3 | import androidx.compose.runtime.Composable 4 | import androidx.compose.runtime.collectAsState 5 | import androidx.compose.runtime.getValue 6 | import io.github.zyrouge.symphony.ui.components.LoaderScaffold 7 | import io.github.zyrouge.symphony.ui.components.SongExplorerList 8 | import io.github.zyrouge.symphony.ui.helpers.ViewContext 9 | import io.github.zyrouge.symphony.utils.SimplePath 10 | 11 | @Composable 12 | fun BrowserView(context: ViewContext) { 13 | val isUpdating by context.symphony.groove.song.isUpdating.collectAsState() 14 | val id by context.symphony.groove.song.id.collectAsState() 15 | val explorer = context.symphony.groove.song.explorer 16 | val lastUsedFolderPath by context.symphony.settings.lastUsedBrowserPath.flow.collectAsState() 17 | 18 | LoaderScaffold(context, isLoading = isUpdating) { 19 | SongExplorerList( 20 | context, 21 | initialPath = lastUsedFolderPath?.let { SimplePath(it) }, 22 | key = id, 23 | explorer = explorer, 24 | onPathChange = { path -> 25 | context.symphony.settings.lastUsedBrowserPath.setValue(path.pathString) 26 | } 27 | ) 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /app/src/main/java/io/github/zyrouge/symphony/ui/view/home/Genres.kt: -------------------------------------------------------------------------------- 1 | package io.github.zyrouge.symphony.ui.view.home 2 | 3 | import androidx.compose.runtime.Composable 4 | import androidx.compose.runtime.collectAsState 5 | import androidx.compose.runtime.getValue 6 | import io.github.zyrouge.symphony.ui.components.GenreGrid 7 | import io.github.zyrouge.symphony.ui.components.LoaderScaffold 8 | import io.github.zyrouge.symphony.ui.helpers.ViewContext 9 | 10 | @Composable 11 | fun GenresView(context: ViewContext) { 12 | val isUpdating by context.symphony.groove.genre.isUpdating.collectAsState() 13 | val genreNames by context.symphony.groove.genre.all.collectAsState() 14 | val genresCount by context.symphony.groove.genre.count.collectAsState() 15 | 16 | LoaderScaffold(context, isLoading = isUpdating) { 17 | GenreGrid( 18 | context, 19 | genreNames = genreNames, 20 | genresCount = genresCount, 21 | ) 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /app/src/main/java/io/github/zyrouge/symphony/ui/view/home/Songs.kt: -------------------------------------------------------------------------------- 1 | package io.github.zyrouge.symphony.ui.view.home 2 | 3 | import androidx.compose.runtime.Composable 4 | import androidx.compose.runtime.collectAsState 5 | import androidx.compose.runtime.getValue 6 | import io.github.zyrouge.symphony.ui.components.LoaderScaffold 7 | import io.github.zyrouge.symphony.ui.components.SongList 8 | import io.github.zyrouge.symphony.ui.helpers.ViewContext 9 | 10 | @Composable 11 | fun SongsView(context: ViewContext) { 12 | val isUpdating by context.symphony.groove.song.isUpdating.collectAsState() 13 | val songIds by context.symphony.groove.song.all.collectAsState() 14 | val songsCount by context.symphony.groove.song.count.collectAsState() 15 | 16 | LoaderScaffold(context, isLoading = isUpdating) { 17 | SongList( 18 | context, 19 | songIds = songIds, 20 | songsCount = songsCount, 21 | enableAddMediaFoldersHint = true, 22 | ) 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /app/src/main/java/io/github/zyrouge/symphony/ui/view/home/Tree.kt: -------------------------------------------------------------------------------- 1 | package io.github.zyrouge.symphony.ui.view.home 2 | 3 | import androidx.compose.runtime.Composable 4 | import androidx.compose.runtime.collectAsState 5 | import androidx.compose.runtime.getValue 6 | import io.github.zyrouge.symphony.ui.components.LoaderScaffold 7 | import io.github.zyrouge.symphony.ui.components.SongTreeList 8 | import io.github.zyrouge.symphony.ui.helpers.ViewContext 9 | 10 | @Composable 11 | fun TreeView(context: ViewContext) { 12 | val isUpdating by context.symphony.groove.song.isUpdating.collectAsState() 13 | val songIds by context.symphony.groove.song.all.collectAsState() 14 | val songsCount by context.symphony.groove.song.count.collectAsState() 15 | val disabledTreePaths by context.symphony.settings.lastDisabledTreePaths.flow.collectAsState() 16 | 17 | LoaderScaffold(context, isLoading = isUpdating) { 18 | SongTreeList( 19 | context, 20 | songIds = songIds, 21 | songsCount = songsCount, 22 | initialDisabled = disabledTreePaths.toList(), 23 | onDisable = { paths -> 24 | context.symphony.settings.lastDisabledTreePaths.setValue(paths.toSet()) 25 | }, 26 | ) 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /app/src/main/java/io/github/zyrouge/symphony/ui/view/nowPlaying/AppBar.kt: -------------------------------------------------------------------------------- 1 | package io.github.zyrouge.symphony.ui.view.nowPlaying 2 | 3 | import androidx.compose.foundation.layout.Row 4 | import androidx.compose.foundation.layout.padding 5 | import androidx.compose.foundation.layout.size 6 | import androidx.compose.material.icons.Icons 7 | import androidx.compose.material.icons.filled.ExpandMore 8 | import androidx.compose.material3.CenterAlignedTopAppBar 9 | import androidx.compose.material3.ExperimentalMaterial3Api 10 | import androidx.compose.material3.Icon 11 | import androidx.compose.material3.IconButton 12 | import androidx.compose.material3.Text 13 | import androidx.compose.material3.TopAppBarDefaults 14 | import androidx.compose.runtime.Composable 15 | import androidx.compose.ui.Alignment 16 | import androidx.compose.ui.Modifier 17 | import androidx.compose.ui.graphics.Color 18 | import androidx.compose.ui.unit.dp 19 | import io.github.zyrouge.symphony.ui.components.IconButtonPlaceholder 20 | import io.github.zyrouge.symphony.ui.components.TopAppBarMinimalTitle 21 | import io.github.zyrouge.symphony.ui.helpers.ViewContext 22 | 23 | @OptIn(ExperimentalMaterial3Api::class) 24 | @Composable 25 | fun NowPlayingAppBar(context: ViewContext) { 26 | CenterAlignedTopAppBar( 27 | title = { 28 | TopAppBarMinimalTitle { 29 | Text(context.symphony.t.NowPlaying) 30 | } 31 | }, 32 | colors = TopAppBarDefaults.centerAlignedTopAppBarColors( 33 | containerColor = Color.Transparent 34 | ), 35 | navigationIcon = { 36 | IconButton( 37 | onClick = { 38 | context.navController.popBackStack() 39 | } 40 | ) { 41 | Icon( 42 | Icons.Filled.ExpandMore, 43 | null, 44 | modifier = Modifier.size(32.dp) 45 | ) 46 | } 47 | }, 48 | actions = { 49 | IconButtonPlaceholder() 50 | }, 51 | ) 52 | } 53 | 54 | @Composable 55 | fun NowPlayingLandscapeAppBar(context: ViewContext) { 56 | Row( 57 | modifier = Modifier.padding(defaultHorizontalPadding, 4.dp, 12.dp, 12.dp), 58 | verticalAlignment = Alignment.CenterVertically, 59 | ) { 60 | TopAppBarMinimalTitle( 61 | modifier = Modifier.weight(1f), 62 | fillMaxWidth = false, 63 | ) { 64 | Text(context.symphony.t.NowPlaying) 65 | } 66 | IconButton( 67 | onClick = { 68 | context.navController.popBackStack() 69 | } 70 | ) { 71 | Icon( 72 | Icons.Filled.ExpandMore, 73 | null, 74 | modifier = Modifier.size(32.dp) 75 | ) 76 | } 77 | } 78 | } 79 | -------------------------------------------------------------------------------- /app/src/main/java/io/github/zyrouge/symphony/ui/view/nowPlaying/NothingPlaying.kt: -------------------------------------------------------------------------------- 1 | package io.github.zyrouge.symphony.ui.view.nowPlaying 2 | 3 | import androidx.compose.foundation.layout.Box 4 | import androidx.compose.foundation.layout.fillMaxSize 5 | import androidx.compose.foundation.layout.padding 6 | import androidx.compose.material.icons.Icons 7 | import androidx.compose.material.icons.filled.Headphones 8 | import androidx.compose.material3.Icon 9 | import androidx.compose.material3.Scaffold 10 | import androidx.compose.material3.Text 11 | import androidx.compose.runtime.Composable 12 | import androidx.compose.ui.Modifier 13 | import io.github.zyrouge.symphony.ui.components.IconTextBody 14 | import io.github.zyrouge.symphony.ui.helpers.ViewContext 15 | 16 | @Composable 17 | fun NothingPlaying(context: ViewContext) { 18 | Scaffold( 19 | modifier = Modifier.fillMaxSize(), 20 | topBar = { 21 | NowPlayingAppBar(context) 22 | }, 23 | content = { contentPadding -> 24 | Box( 25 | modifier = Modifier 26 | .padding(contentPadding) 27 | .fillMaxSize() 28 | ) { 29 | NothingPlayingBody(context) 30 | } 31 | } 32 | ) 33 | } 34 | 35 | @Composable 36 | fun NothingPlayingBody(context: ViewContext) { 37 | IconTextBody( 38 | icon = { modifier -> 39 | Icon( 40 | Icons.Filled.Headphones, 41 | null, 42 | modifier = modifier 43 | ) 44 | }, 45 | content = { 46 | Text(context.symphony.t.NothingIsBeingPlayedRightNow) 47 | } 48 | ) 49 | } 50 | -------------------------------------------------------------------------------- /app/src/main/java/io/github/zyrouge/symphony/utils/ActivityUtils.kt: -------------------------------------------------------------------------------- 1 | package io.github.zyrouge.symphony.utils 2 | 3 | import android.content.ClipData 4 | import android.content.ClipboardManager 5 | import android.content.Context 6 | import android.content.Intent 7 | import android.net.Uri 8 | import android.widget.Toast 9 | import io.github.zyrouge.symphony.Symphony 10 | 11 | object ActivityUtils { 12 | fun startBrowserActivity(activity: Context, uri: Uri) { 13 | activity.startActivity(Intent(Intent.ACTION_VIEW).setData(uri)) 14 | } 15 | 16 | fun copyToClipboardAndNotify(symphony: Symphony, text: String) { 17 | val context = symphony.applicationContext 18 | val clipboardManager = context.getSystemService(ClipboardManager::class.java) 19 | clipboardManager.setPrimaryClip(ClipData.newPlainText(null, text)) 20 | Toast.makeText(context, symphony.t.CopiedXToClipboard(text), Toast.LENGTH_SHORT).show() 21 | } 22 | 23 | fun makePersistableReadableUri(context: Context, uri: Uri) { 24 | context.contentResolver.takePersistableUriPermission( 25 | uri, 26 | Intent.FLAG_GRANT_READ_URI_PERMISSION 27 | ) 28 | } 29 | 30 | fun releasePersistableReadableUri(context: Context, uri: Uri) { 31 | context.contentResolver.releasePersistableUriPermission( 32 | uri, 33 | Intent.FLAG_GRANT_READ_URI_PERMISSION 34 | ) 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /app/src/main/java/io/github/zyrouge/symphony/utils/DurationUtils.kt: -------------------------------------------------------------------------------- 1 | package io.github.zyrouge.symphony.utils 2 | 3 | import java.util.concurrent.TimeUnit 4 | 5 | object DurationUtils { 6 | fun formatMs(ms: Long) = formatMinSec( 7 | TimeUnit.MILLISECONDS.toDays(ms).floorDiv(TimeUnit.DAYS.toDays(1)), 8 | TimeUnit.MILLISECONDS.toHours(ms) % TimeUnit.DAYS.toHours(1), 9 | TimeUnit.MILLISECONDS.toMinutes(ms) % TimeUnit.HOURS.toMinutes(1), 10 | TimeUnit.MILLISECONDS.toSeconds(ms) % TimeUnit.MINUTES.toSeconds(1) 11 | ) 12 | 13 | fun formatMinSec(d: Long, h: Long, m: Long, s: Long) = when { 14 | d == 0L && h == 0L -> String.format(null, "%02d:%02d", m, s) 15 | d == 0L -> String.format(null, "%02d:%02d:%02d", h, m, s) 16 | else -> String.format(null, "%02d:%02d:%02d:%02d", d, h, m, s) 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /app/src/main/java/io/github/zyrouge/symphony/utils/Eventer.kt: -------------------------------------------------------------------------------- 1 | package io.github.zyrouge.symphony.utils 2 | 3 | typealias EventSubscriber<T> = (T) -> Unit 4 | typealias EventUnsubscribeFn = () -> Unit 5 | 6 | class Eventer<T> { 7 | private val subscribers = mutableListOf<EventSubscriber<T>>() 8 | 9 | fun subscribe(subscriber: EventSubscriber<T>): EventUnsubscribeFn { 10 | subscribers.add(subscriber) 11 | return { unsubscribe(subscriber) } 12 | } 13 | 14 | fun unsubscribe(subscriber: EventSubscriber<T>) { 15 | subscribers.remove(subscriber) 16 | } 17 | 18 | fun dispatch(event: T) { 19 | subscribers.forEach { it(event) } 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /app/src/main/java/io/github/zyrouge/symphony/utils/Float.kt: -------------------------------------------------------------------------------- 1 | package io.github.zyrouge.symphony.utils 2 | 3 | fun Float.toSafeFinite() = if (!isFinite()) 0f else this 4 | -------------------------------------------------------------------------------- /app/src/main/java/io/github/zyrouge/symphony/utils/Fuzzy.kt: -------------------------------------------------------------------------------- 1 | package io.github.zyrouge.symphony.utils 2 | 3 | import me.xdrop.fuzzywuzzy.FuzzySearch 4 | import kotlin.math.max 5 | 6 | class FuzzySearchComparator(val input: String) { 7 | fun compareString(value: String) = Fuzzy.compare(input, value) 8 | 9 | fun compareCollection(values: Collection<String>): Int? { 10 | if (values.isEmpty()) { 11 | return null 12 | } 13 | var score = 0 14 | values.forEach { 15 | score = max(score, compareString(it)) 16 | } 17 | return score 18 | } 19 | } 20 | 21 | data class FuzzySearchOption<T>( 22 | val match: FuzzySearchComparator.(T) -> Int?, 23 | val weight: Int = 1, 24 | ) 25 | 26 | data class FuzzyResultEntity<T>( 27 | val score: Int, 28 | val entity: T, 29 | ) 30 | 31 | class FuzzySearcher<T>(val options: List<FuzzySearchOption<T>>) { 32 | fun search( 33 | terms: String, 34 | entities: List<T>, 35 | maxLength: Int = -1, 36 | ): List<FuzzyResultEntity<T>> { 37 | val results = entities 38 | .map { compare(terms, it) } 39 | .sortedByDescending { it.score } 40 | return when { 41 | maxLength > -1 -> results.subListNonStrict(maxLength) 42 | else -> results 43 | } 44 | } 45 | 46 | private fun compare(terms: String, entity: T): FuzzyResultEntity<T> { 47 | var score = 0 48 | val comparator = FuzzySearchComparator(terms) 49 | options.forEach { option -> 50 | option.match.invoke(comparator, entity)?.let { 51 | score = max(score, it * option.weight) 52 | } 53 | } 54 | return FuzzyResultEntity(score, entity) 55 | } 56 | } 57 | 58 | object Fuzzy { 59 | fun compare(input: String, against: String) = FuzzySearch.tokenSetPartialRatio( 60 | normalizeTerms(input), 61 | normalizeTerms(against), 62 | ) 63 | 64 | private val symbolsRegex = Regex("""[~${'$'}&+,:;=?@#|'"<>.^*()\[\]%!\-_/\\]+""") 65 | private val whitespaceRegex = Regex("""\s+""") 66 | private fun normalizeTerms(terms: String) = terms.lowercase() 67 | .replace(symbolsRegex, "") 68 | .replace(whitespaceRegex, " ") 69 | } 70 | -------------------------------------------------------------------------------- /app/src/main/java/io/github/zyrouge/symphony/utils/Http.kt: -------------------------------------------------------------------------------- 1 | package io.github.zyrouge.symphony.utils 2 | 3 | import okhttp3.OkHttpClient 4 | 5 | val HttpClient = OkHttpClient.Builder().cache(null).build() 6 | -------------------------------------------------------------------------------- /app/src/main/java/io/github/zyrouge/symphony/utils/ImagePreserver.kt: -------------------------------------------------------------------------------- 1 | package io.github.zyrouge.symphony.utils 2 | 3 | import android.graphics.Bitmap 4 | import kotlin.math.max 5 | 6 | object ImagePreserver { 7 | enum class Quality(val maxSide: Int?) { 8 | Low(256), 9 | Medium(512), 10 | High(1024), 11 | Loseless(null), 12 | } 13 | 14 | fun resize(bitmap: Bitmap, quality: Quality): Bitmap { 15 | if (quality.maxSide == null || max(bitmap.width, bitmap.height) < quality.maxSide) { 16 | return bitmap 17 | } 18 | val (width, height) = calculateDimensions(bitmap.width, bitmap.height, quality.maxSide) 19 | return Bitmap.createScaledBitmap(bitmap, width, height, true) 20 | } 21 | 22 | private fun calculateDimensions(width: Int, height: Int, maxSide: Int) = when { 23 | width > height -> maxSide to (height * (maxSide.toFloat() / width)).toInt() 24 | width < height -> (width * (maxSide.toFloat() / height)).toInt() to maxSide 25 | else -> maxSide to maxSide 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /app/src/main/java/io/github/zyrouge/symphony/utils/KeyGenerator.kt: -------------------------------------------------------------------------------- 1 | package io.github.zyrouge.symphony.utils 2 | 3 | interface KeyGenerator { 4 | fun next(): String 5 | 6 | class TimeIncremental(private var i: Int = 0, private var time: Long = 0) : KeyGenerator { 7 | @Synchronized 8 | override fun next(): String { 9 | val now = System.currentTimeMillis() 10 | if (now != time) { 11 | time = now 12 | i = 0 13 | } else { 14 | i++ 15 | } 16 | return "$now.$i" 17 | } 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /app/src/main/java/io/github/zyrouge/symphony/utils/List.kt: -------------------------------------------------------------------------------- 1 | package io.github.zyrouge.symphony.utils 2 | 3 | import java.util.concurrent.CopyOnWriteArrayList 4 | import kotlin.math.max 5 | import kotlin.math.min 6 | import kotlin.random.Random 7 | 8 | fun <T> List<T>.subListNonStrict(length: Int, start: Int = 0) = 9 | subList(start, min(start + length, size)) 10 | 11 | fun <T> List<T>.randomSubList(length: Int): List<T> { 12 | val mut = toMutableList() 13 | val out = mutableListOf<T>() 14 | val possibleLength = max(0, min(length, mut.size)) 15 | for (i in 0 until possibleLength) { 16 | val index = Random.nextInt(mut.size) 17 | out.add(mut.removeAt(index)) 18 | } 19 | return out 20 | } 21 | 22 | fun <T> List<T>.mutate(fn: MutableList<T>.() -> Unit): List<T> { 23 | val out = toMutableList() 24 | fn.invoke(out) 25 | return out 26 | } 27 | 28 | fun <T> concurrentListOf(): MutableList<T> = CopyOnWriteArrayList(mutableListOf<T>()) 29 | -------------------------------------------------------------------------------- /app/src/main/java/io/github/zyrouge/symphony/utils/Logger.kt: -------------------------------------------------------------------------------- 1 | package io.github.zyrouge.symphony.utils 2 | 3 | import android.util.Log 4 | import io.github.zyrouge.symphony.services.AppMeta 5 | 6 | object Logger { 7 | private const val TAG = "${AppMeta.appName}Logger" 8 | 9 | fun warn(mod: String, text: String) = Log.w(TAG, "$mod: $text") 10 | fun warn(mod: String, text: String, throwable: Throwable) = 11 | warn(mod, joinTextThrowable(text, throwable)) 12 | 13 | fun error(mod: String, text: String) = Log.e(TAG, "$mod: $text") 14 | fun error(mod: String, text: String, throwable: Throwable) = 15 | error(mod, joinTextThrowable(text, throwable)) 16 | 17 | fun joinTextThrowable(text: String, throwable: Throwable) = StringBuilder().apply { 18 | append(text) 19 | append("\nError: ${throwable.message}") 20 | append("\nStack trace: ${throwable.stackTraceToString()}") 21 | }.toString() 22 | } 23 | -------------------------------------------------------------------------------- /app/src/main/java/io/github/zyrouge/symphony/utils/RangeUtils.kt: -------------------------------------------------------------------------------- 1 | package io.github.zyrouge.symphony.utils 2 | 3 | object RangeUtils { 4 | fun calculateGap(range: ClosedFloatingPointRange<Float>) = range.endInclusive - range.start 5 | 6 | fun calculateRatioFromValue(value: Float, range: ClosedFloatingPointRange<Float>) = 7 | (value - range.start) / calculateGap(range) 8 | 9 | fun calculateValueFromRatio(ratio: Float, range: ClosedFloatingPointRange<Float>) = 10 | (ratio * calculateGap(range)) + range.start 11 | } 12 | -------------------------------------------------------------------------------- /app/src/main/java/io/github/zyrouge/symphony/utils/RoomConvertors.kt: -------------------------------------------------------------------------------- 1 | package io.github.zyrouge.symphony.utils 2 | 3 | import android.net.Uri 4 | import androidx.room.TypeConverter 5 | import kotlinx.serialization.encodeToString 6 | import kotlinx.serialization.json.Json 7 | import java.time.LocalDate 8 | 9 | class RoomConvertors { 10 | @TypeConverter 11 | fun serializeUri(value: Uri) = value.toString() 12 | 13 | @TypeConverter 14 | fun deserializeUri(value: String) = Uri.parse(value) 15 | 16 | @TypeConverter 17 | fun serializeStringSet(value: Set<String>) = Json.encodeToString(value) 18 | 19 | @TypeConverter 20 | fun deserializeStringSet(value: String) = Json.decodeFromString<Set<String>>(value) 21 | 22 | @TypeConverter 23 | fun serializeStringList(value: List<String>) = Json.encodeToString(value) 24 | 25 | @TypeConverter 26 | fun deserializeStringList(value: String) = Json.decodeFromString<List<String>>(value) 27 | 28 | @TypeConverter 29 | fun serializeLocalDate(value: LocalDate) = value.toString() 30 | 31 | @TypeConverter 32 | fun deserializeLocalDate(value: String): LocalDate = LocalDate.parse(value) 33 | } 34 | -------------------------------------------------------------------------------- /app/src/main/java/io/github/zyrouge/symphony/utils/Run.kt: -------------------------------------------------------------------------------- 1 | package io.github.zyrouge.symphony.utils 2 | 3 | fun <T> runIfOrDefault(value: Boolean, defaultValue: T, fn: () -> T) = when { 4 | value -> fn() 5 | else -> defaultValue 6 | } 7 | 8 | fun <T> T.runIfOrThis(value: Boolean, fn: T.() -> T) = when { 9 | value -> fn.invoke(this) 10 | else -> this 11 | } 12 | -------------------------------------------------------------------------------- /app/src/main/java/io/github/zyrouge/symphony/utils/Set.kt: -------------------------------------------------------------------------------- 1 | package io.github.zyrouge.symphony.utils 2 | 3 | import java.util.concurrent.ConcurrentHashMap 4 | 5 | fun Set<String>.joinToStringIfNotEmpty() = if (isNotEmpty()) joinToString() else null 6 | fun Set<String>.joinToStringIfNotEmpty(sensitive: Boolean) = 7 | joinToStringIfNotEmpty()?.withCase(sensitive) 8 | 9 | typealias ConcurrentSet<T> = ConcurrentHashMap.KeySetView<T, Boolean> 10 | 11 | fun <T> concurrentSetOf(vararg elements: T): ConcurrentSet<T> = 12 | ConcurrentHashMap.newKeySet<T>().apply { addAll(elements) } 13 | 14 | fun <T> concurrentSetOf(elements: Collection<T>): ConcurrentSet<T> = 15 | ConcurrentHashMap.newKeySet<T>().apply { addAll(elements) } 16 | -------------------------------------------------------------------------------- /app/src/main/java/io/github/zyrouge/symphony/utils/SimpleFileSystem.kt: -------------------------------------------------------------------------------- 1 | package io.github.zyrouge.symphony.utils 2 | 3 | import java.util.concurrent.ConcurrentHashMap 4 | 5 | sealed class SimpleFileSystem(val parent: Folder?, val name: String) { 6 | val fullPath 7 | get(): SimplePath { 8 | val parts = mutableListOf(name) 9 | var currentParent = parent 10 | while (currentParent != null) { 11 | parts.add(0, currentParent.name) 12 | currentParent = currentParent.parent 13 | } 14 | return SimplePath(parts) 15 | } 16 | 17 | class File( 18 | parent: Folder? = null, 19 | name: String, 20 | var data: Any? = null, 21 | ) : SimpleFileSystem(parent, name) 22 | 23 | class Folder( 24 | parent: Folder? = null, 25 | name: String = "root", 26 | var children: ConcurrentHashMap<String, SimpleFileSystem> = ConcurrentHashMap(), 27 | ) : SimpleFileSystem(parent, name) { 28 | val isEmpty get() = children.isEmpty() 29 | val childFoldersCount get() = children.values.count { it is Folder } 30 | 31 | fun addChildFolder(name: String): Folder { 32 | if (children.containsKey(name)) { 33 | throw Exception("Child '$name' already exists") 34 | } 35 | val child = Folder(this, name) 36 | children[name] = child 37 | return child 38 | } 39 | 40 | fun addChildFile(name: String): File { 41 | if (children.containsKey(name)) { 42 | throw Exception("Child '$name' already exists") 43 | } 44 | val child = File(this, name) 45 | children[name] = child 46 | return child 47 | } 48 | 49 | fun addChildFile(path: SimplePath): File { 50 | val parts = path.parts.toMutableList() 51 | var parent = this 52 | while (parts.size > 1) { 53 | val x = parts.removeAt(0) 54 | val found = parent.children[x] 55 | parent = when (found) { 56 | is Folder -> found 57 | null -> parent.addChildFolder(x) 58 | else -> throw Exception("Child '$x' is not a folder") 59 | } 60 | } 61 | return parent.addChildFile(parts[0]) 62 | } 63 | } 64 | } 65 | -------------------------------------------------------------------------------- /app/src/main/java/io/github/zyrouge/symphony/utils/SimplePath.kt: -------------------------------------------------------------------------------- 1 | package io.github.zyrouge.symphony.utils 2 | 3 | import androidx.compose.runtime.Immutable 4 | import kotlin.text.split 5 | 6 | @Immutable 7 | class SimplePath(val parts: List<String>) { 8 | constructor(path: String) : this(n(p(path))) 9 | constructor(path: String, vararg subParts: String) : this(n(p(path) + p(*subParts))) 10 | constructor(path: SimplePath, vararg subParts: String) : this(n(path.parts + p(*subParts))) 11 | 12 | val name get() = parts.last() 13 | val nameWithoutExtension get() = name.substringBeforeLast(".") 14 | val extension get() = name.substringAfterLast(".", "") 15 | val parent get() = if (parts.size > 1) SimplePath(parts.subList(0, parts.lastIndex)) else null 16 | val size get() = parts.size 17 | val pathString get() = parts.joinToString("/") 18 | 19 | fun join(vararg nParts: String) = SimplePath(this, *nParts) 20 | 21 | override fun toString() = pathString 22 | 23 | companion object { 24 | private fun p(vararg path: String) = path.fold(listOf<String>()) { prev, curr -> 25 | prev + curr.split("/", "\\") 26 | } 27 | 28 | private fun n(parts: List<String>): List<String> { 29 | val normalized = mutableListOf<String>() 30 | for (x in parts) { 31 | when { 32 | x.isEmpty() -> {} 33 | x == "." -> {} 34 | x == ".." -> normalized.removeAt(normalized.lastIndex) 35 | else -> normalized.add(x) 36 | } 37 | } 38 | return normalized 39 | } 40 | } 41 | } 42 | -------------------------------------------------------------------------------- /app/src/main/java/io/github/zyrouge/symphony/utils/StringListUtils.kt: -------------------------------------------------------------------------------- 1 | package io.github.zyrouge.symphony.utils 2 | 3 | object StringListUtils { 4 | enum class SortBy { 5 | CUSTOM, 6 | NAME, 7 | } 8 | 9 | fun sort(values: List<String>, by: SortBy, reverse: Boolean): List<String> { 10 | val sorted = when (by) { 11 | SortBy.CUSTOM -> values 12 | SortBy.NAME -> values.sorted() 13 | } 14 | return if (reverse) sorted.reversed() else sorted 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /app/src/main/java/io/github/zyrouge/symphony/utils/StringUtils.kt: -------------------------------------------------------------------------------- 1 | package io.github.zyrouge.symphony.utils 2 | 3 | fun String.withCase(sensitive: Boolean) = if (!sensitive) lowercase() else this 4 | -------------------------------------------------------------------------------- /app/src/main/java/io/github/zyrouge/symphony/utils/TimedContent.kt: -------------------------------------------------------------------------------- 1 | package io.github.zyrouge.symphony.utils 2 | 3 | import java.time.Duration 4 | 5 | data class TimedContent(val pairs: List<Pair<Long, String>>) { 6 | val isSynced: Boolean get() = pairs.firstOrNull()?.first != pairs.lastOrNull()?.first 7 | 8 | companion object { 9 | val lrcLineSeparatorRegex = Regex("""\n|\r|\r\n""") 10 | val lrcLineFilterRegex = Regex("""^\[\s*(\d+):(\d+)\.(\d+)?\s*](.*)""") 11 | 12 | fun fromLyrics(content: String): TimedContent { 13 | var lastTime = 0L 14 | val pairs = content.split(lrcLineSeparatorRegex).map { x -> 15 | val match = lrcLineFilterRegex.matchEntire(x) 16 | val pair = when { 17 | match != null -> Duration 18 | .ofMinutes(match.groupValues[1].toLong()) 19 | .plusSeconds(match.groupValues[2].toLong()) 20 | .plusMillis(match.groupValues[3].toLong()) 21 | .toMillis() to match.groupValues[4].trim() 22 | 23 | else -> lastTime to x.trim() 24 | } 25 | lastTime = pair.first 26 | pair 27 | } 28 | return TimedContent(pairs) 29 | } 30 | } 31 | } -------------------------------------------------------------------------------- /app/src/main/res/drawable/ic_launcher_foreground.xml: -------------------------------------------------------------------------------- 1 | <vector xmlns:android="http://schemas.android.com/apk/res/android" 2 | xmlns:aapt="http://schemas.android.com/aapt" 3 | android:width="108dp" 4 | android:height="108dp" 5 | android:viewportWidth="1000" 6 | android:viewportHeight="1000"> 7 | <group android:scaleX="0.4" 8 | android:scaleY="0.4" 9 | android:translateX="300" 10 | android:translateY="300"> 11 | <group> 12 | <clip-path 13 | android:pathData="M0,0h1000v1000h-1000z"/> 14 | <group> 15 | <clip-path 16 | android:pathData="M937,425L937,425A50,50 0,0 1,987 475L987,525A50,50 0,0 1,937 575L937,575A50,50 0,0 1,887 525L887,475A50,50 0,0 1,937 425zM762,225L762,225A50,50 0,0 1,812 275L812,725A50,50 0,0 1,762 775L762,775A50,50 0,0 1,712 725L712,275A50,50 0,0 1,762 225zM587,45L587,45A50,50 0,0 1,637 95L637,795A50,50 0,0 1,587 845L587,845A50,50 0,0 1,537 795L537,95A50,50 0,0 1,587 45zM412,954.1L412,954.1A50,50 0,0 1,362 904.1L362,204.1A50,50 0,0 1,412 154.1L412,154.1A50,50 0,0 1,462 204.1L462,904.1A50,50 0,0 1,412 954.1zM237,750L237,750A50,50 0,0 1,187 700L187,300A50,50 0,0 1,237 250L237,250A50,50 0,0 1,287 300L287,700A50,50 0,0 1,237 750zM62,625L62,625A50,50 0,0 1,12 575L12,425A50,50 0,0 1,62 375L62,375A50,50 0,0 1,112 425L112,575A50,50 0,0 1,62 625z"/> 17 | <path 18 | android:pathData="M0,45h1000v909h-1000z" 19 | android:fillColor="#D9D9D9"/> 20 | <path 21 | android:pathData="M0,45h1000v909h-1000z"> 22 | <aapt:attr name="android:fillColor"> 23 | <gradient 24 | android:startX="500" 25 | android:startY="45" 26 | android:endX="500" 27 | android:endY="954" 28 | android:type="linear"> 29 | <item android:offset="0" android:color="#FFC084FC"/> 30 | <item android:offset="1" android:color="#FFA855F7"/> 31 | </gradient> 32 | </aapt:attr> 33 | </path> 34 | </group> 35 | </group> 36 | </group> 37 | </vector> 38 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/ic_launcher_monochrome.xml: -------------------------------------------------------------------------------- 1 | <vector xmlns:android="http://schemas.android.com/apk/res/android" 2 | android:width="108dp" 3 | android:height="108dp" 4 | android:viewportWidth="1000" 5 | android:viewportHeight="1000"> 6 | <group android:scaleX="0.4" 7 | android:scaleY="0.4" 8 | android:translateX="300" 9 | android:translateY="300"> 10 | <path 11 | android:pathData="M937,425L937,425A50,50 0,0 1,987 475L987,525A50,50 0,0 1,937 575L937,575A50,50 0,0 1,887 525L887,475A50,50 0,0 1,937 425z" 12 | android:fillColor="#C084FC"/> 13 | <path 14 | android:pathData="M762,225L762,225A50,50 0,0 1,812 275L812,725A50,50 0,0 1,762 775L762,775A50,50 0,0 1,712 725L712,275A50,50 0,0 1,762 225z" 15 | android:fillColor="#C084FC"/> 16 | <path 17 | android:pathData="M587,45L587,45A50,50 0,0 1,637 95L637,795A50,50 0,0 1,587 845L587,845A50,50 0,0 1,537 795L537,95A50,50 0,0 1,587 45z" 18 | android:fillColor="#C084FC"/> 19 | <path 20 | android:pathData="M412,954.1L412,954.1A50,50 0,0 1,362 904.1L362,204.1A50,50 0,0 1,412 154.1L412,154.1A50,50 0,0 1,462 204.1L462,904.1A50,50 0,0 1,412 954.1z" 21 | android:fillColor="#C084FC"/> 22 | <path 23 | android:pathData="M237,750L237,750A50,50 0,0 1,187 700L187,300A50,50 0,0 1,237 250L237,250A50,50 0,0 1,287 300L287,700A50,50 0,0 1,237 750z" 24 | android:fillColor="#C084FC"/> 25 | <path 26 | android:pathData="M62,625L62,625A50,50 0,0 1,12 575L12,425A50,50 0,0 1,62 375L62,375A50,50 0,0 1,112 425L112,575A50,50 0,0 1,62 625z" 27 | android:fillColor="#C084FC"/> 28 | </group> 29 | </vector> 30 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/material_icon_close.xml: -------------------------------------------------------------------------------- 1 | <vector xmlns:android="http://schemas.android.com/apk/res/android" 2 | android:width="48dp" 3 | android:height="48dp" 4 | android:viewportWidth="48" 5 | android:viewportHeight="48"> 6 | <group 7 | android:scaleX="0.5" 8 | android:scaleY="0.5" 9 | android:pivotX="24" 10 | android:pivotY="24"> 11 | <path 12 | android:fillColor="#FF000000" 13 | android:pathData="M24.05,25.25 L13.3,36q-0.3,0.3 -0.65,0.325 -0.35,0.025 -0.65,-0.325 -0.35,-0.3 -0.35,-0.625t0.35,-0.625l10.8,-10.8 -10.85,-10.8q-0.25,-0.25 -0.275,-0.6 -0.025,-0.35 0.275,-0.65 0.3,-0.3 0.625,-0.3t0.675,0.3L24,22.65 34.75,11.9q0.25,-0.25 0.6,-0.275 0.35,-0.025 0.7,0.275 0.3,0.35 0.3,0.7t-0.3,0.65L25.3,24l10.75,10.8q0.25,0.25 0.275,0.6 0.025,0.35 -0.275,0.65 -0.3,0.3 -0.625,0.3t-0.575,-0.3Z" /> 14 | </group> 15 | </vector> 16 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/material_icon_music_note.xml: -------------------------------------------------------------------------------- 1 | <vector xmlns:android="http://schemas.android.com/apk/res/android" 2 | android:width="48dp" 3 | android:height="48dp" 4 | android:viewportWidth="48" 5 | android:viewportHeight="48"> 6 | <path 7 | android:fillColor="#FF000000" 8 | android:pathData="M19.65,42q-3.15,0 -5.325,-2.175Q12.15,37.65 12.15,34.5q0,-3.15 2.175,-5.325Q16.5,27 19.65,27q1.4,0 2.525,0.4t1.975,1.1V6h11.7v6.75h-8.7V34.5q0,3.15 -2.175,5.325Q22.8,42 19.65,42Z" /> 9 | </vector> 10 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/material_icon_pause.xml: -------------------------------------------------------------------------------- 1 | <vector xmlns:android="http://schemas.android.com/apk/res/android" 2 | android:width="48dp" 3 | android:height="48dp" 4 | android:viewportWidth="48" 5 | android:viewportHeight="48"> 6 | <group 7 | android:scaleX="0.75" 8 | android:scaleY="0.75" 9 | android:pivotX="24" 10 | android:pivotY="24"> 11 | <path 12 | android:fillColor="#FF000000" 13 | android:pathData="M29.15,36.1q-0.75,0 -1.25,-0.5t-0.5,-1.25v-20.8q0,-0.7 0.5,-1.225 0.5,-0.525 1.25,-0.525h5.25q0.75,0 1.25,0.525t0.5,1.225v20.8q0,0.75 -0.5,1.25t-1.25,0.5ZM13.6,36.1q-0.75,0 -1.25,-0.5t-0.5,-1.25v-20.8q0,-0.7 0.5,-1.225 0.5,-0.525 1.25,-0.525h5.3q0.7,0 1.225,0.525 0.525,0.525 0.525,1.225v20.8q0,0.75 -0.525,1.25t-1.225,0.5Z" /> 14 | </group> 15 | </vector> 16 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/material_icon_play.xml: -------------------------------------------------------------------------------- 1 | <vector xmlns:android="http://schemas.android.com/apk/res/android" 2 | android:width="48dp" 3 | android:height="48dp" 4 | android:viewportWidth="48" 5 | android:viewportHeight="48"> 6 | <path 7 | android:fillColor="#FF000000" 8 | android:pathData="M20.5,32.6q-0.9,0.55 -1.75,0.075 -0.85,-0.475 -0.85,-1.525V16.7q0,-1 0.85,-1.475 0.85,-0.475 1.75,0.075l11.35,7.3q0.8,0.5 0.8,1.35 0,0.85 -0.8,1.35Z" /> 9 | </vector> 10 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/material_icon_skip_next.xml: -------------------------------------------------------------------------------- 1 | <vector xmlns:android="http://schemas.android.com/apk/res/android" 2 | android:width="48dp" 3 | android:height="48dp" 4 | android:viewportWidth="48" 5 | android:viewportHeight="48"> 6 | <group 7 | android:scaleX="0.75" 8 | android:scaleY="0.75" 9 | android:pivotX="24" 10 | android:pivotY="24"> 11 | <path 12 | android:fillColor="#FF000000" 13 | android:pathData="M33.9,33.35q-0.4,0 -0.65,-0.25t-0.25,-0.65L33,15.5q0,-0.35 0.25,-0.6t0.65,-0.25q0.35,0 0.6,0.25t0.25,0.6v16.95q0,0.4 -0.25,0.65t-0.6,0.25ZM15.9,31.55q-0.9,0.6 -1.775,0.15 -0.875,-0.45 -0.875,-1.45v-12.5q0,-1.05 0.875,-1.475 0.875,-0.425 1.775,0.125l9.2,6.2q0.65,0.55 0.65,1.4 0,0.85 -0.65,1.4Z" /> 14 | </group> 15 | </vector> 16 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/material_icon_skip_previous.xml: -------------------------------------------------------------------------------- 1 | <vector xmlns:android="http://schemas.android.com/apk/res/android" 2 | android:width="48dp" 3 | android:height="48dp" 4 | android:viewportWidth="48" 5 | android:viewportHeight="48"> 6 | <group 7 | android:scaleX="0.75" 8 | android:scaleY="0.75" 9 | android:pivotX="24" 10 | android:pivotY="24"> 11 | <path 12 | android:fillColor="#FF000000" 13 | android:pathData="M14.15,33.35q-0.4,0 -0.65,-0.25t-0.25,-0.65L13.25,15.5q0,-0.35 0.25,-0.6t0.65,-0.25q0.35,0 0.6,0.25t0.25,0.6v16.95q0,0.4 -0.25,0.65t-0.6,0.25ZM32.2,31.55 L22.95,25.4q-0.7,-0.55 -0.7,-1.4 0,-0.85 0.7,-1.4l9.25,-6.2q0.8,-0.55 1.675,-0.125 0.875,0.425 0.875,1.475v12.5q0,1 -0.875,1.45 -0.875,0.45 -1.675,-0.15Z" /> 14 | </group> 15 | </vector> 16 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/material_icon_stop.xml: -------------------------------------------------------------------------------- 1 | <vector xmlns:android="http://schemas.android.com/apk/res/android" 2 | android:width="48dp" 3 | android:height="48dp" 4 | android:viewportWidth="48" 5 | android:viewportHeight="48"> 6 | <group 7 | android:scaleX="0.6" 8 | android:scaleY="0.6" 9 | android:pivotX="24" 10 | android:pivotY="24"> 11 | <path 12 | android:fillColor="#FF000000" 13 | android:pathData="M17.4,33.95q-1.4,0 -2.375,-0.975Q14.05,32 14.05,30.6V17.4q0,-1.4 0.975,-2.375 0.975,-0.975 2.375,-0.975h13.2q1.4,0 2.375,0.975 0.975,0.975 0.975,2.375v13.2q0,1.4 -0.975,2.375 -0.975,0.975 -2.375,0.975Z" /> 14 | </group> 15 | </vector> 16 | -------------------------------------------------------------------------------- /app/src/main/res/font/dmsans_bold.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/zyrouge/symphony/d6ff56b54422c5db6151107be48140d6b954e860/app/src/main/res/font/dmsans_bold.ttf -------------------------------------------------------------------------------- /app/src/main/res/font/dmsans_regular.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/zyrouge/symphony/d6ff56b54422c5db6151107be48140d6b954e860/app/src/main/res/font/dmsans_regular.ttf -------------------------------------------------------------------------------- /app/src/main/res/font/inter_bold.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/zyrouge/symphony/d6ff56b54422c5db6151107be48140d6b954e860/app/src/main/res/font/inter_bold.ttf -------------------------------------------------------------------------------- /app/src/main/res/font/inter_regular.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/zyrouge/symphony/d6ff56b54422c5db6151107be48140d6b954e860/app/src/main/res/font/inter_regular.ttf -------------------------------------------------------------------------------- /app/src/main/res/font/poppins_bold.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/zyrouge/symphony/d6ff56b54422c5db6151107be48140d6b954e860/app/src/main/res/font/poppins_bold.ttf -------------------------------------------------------------------------------- /app/src/main/res/font/poppins_regular.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/zyrouge/symphony/d6ff56b54422c5db6151107be48140d6b954e860/app/src/main/res/font/poppins_regular.ttf -------------------------------------------------------------------------------- /app/src/main/res/font/productsans_bold.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/zyrouge/symphony/d6ff56b54422c5db6151107be48140d6b954e860/app/src/main/res/font/productsans_bold.ttf -------------------------------------------------------------------------------- /app/src/main/res/font/productsans_regular.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/zyrouge/symphony/d6ff56b54422c5db6151107be48140d6b954e860/app/src/main/res/font/productsans_regular.ttf -------------------------------------------------------------------------------- /app/src/main/res/font/roboto_bold.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/zyrouge/symphony/d6ff56b54422c5db6151107be48140d6b954e860/app/src/main/res/font/roboto_bold.ttf -------------------------------------------------------------------------------- /app/src/main/res/font/roboto_regular.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/zyrouge/symphony/d6ff56b54422c5db6151107be48140d6b954e860/app/src/main/res/font/roboto_regular.ttf -------------------------------------------------------------------------------- /app/src/main/res/mipmap-anydpi/ic_launcher.xml: -------------------------------------------------------------------------------- 1 | <?xml version="1.0" encoding="utf-8"?> 2 | <adaptive-icon xmlns:android="http://schemas.android.com/apk/res/android"> 3 | <background android:drawable="@color/ic_launcher_background" /> 4 | <foreground android:drawable="@drawable/ic_launcher_foreground" /> 5 | <monochrome android:drawable="@drawable/ic_launcher_monochrome" /> 6 | </adaptive-icon> -------------------------------------------------------------------------------- /app/src/main/res/mipmap-anydpi/ic_launcher_round.xml: -------------------------------------------------------------------------------- 1 | <?xml version="1.0" encoding="utf-8"?> 2 | <adaptive-icon xmlns:android="http://schemas.android.com/apk/res/android"> 3 | <background android:drawable="@color/ic_launcher_background"/> 4 | <foreground android:drawable="@drawable/ic_launcher_foreground"/> 5 | <monochrome android:drawable="@drawable/ic_launcher_monochrome"/> 6 | </adaptive-icon> -------------------------------------------------------------------------------- /app/src/main/res/mipmap-hdpi/ic_launcher.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/zyrouge/symphony/d6ff56b54422c5db6151107be48140d6b954e860/app/src/main/res/mipmap-hdpi/ic_launcher.webp -------------------------------------------------------------------------------- /app/src/main/res/mipmap-hdpi/ic_launcher_round.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/zyrouge/symphony/d6ff56b54422c5db6151107be48140d6b954e860/app/src/main/res/mipmap-hdpi/ic_launcher_round.webp -------------------------------------------------------------------------------- /app/src/main/res/mipmap-mdpi/ic_launcher.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/zyrouge/symphony/d6ff56b54422c5db6151107be48140d6b954e860/app/src/main/res/mipmap-mdpi/ic_launcher.webp -------------------------------------------------------------------------------- /app/src/main/res/mipmap-mdpi/ic_launcher_round.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/zyrouge/symphony/d6ff56b54422c5db6151107be48140d6b954e860/app/src/main/res/mipmap-mdpi/ic_launcher_round.webp -------------------------------------------------------------------------------- /app/src/main/res/mipmap-xhdpi/ic_launcher.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/zyrouge/symphony/d6ff56b54422c5db6151107be48140d6b954e860/app/src/main/res/mipmap-xhdpi/ic_launcher.webp -------------------------------------------------------------------------------- /app/src/main/res/mipmap-xhdpi/ic_launcher_round.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/zyrouge/symphony/d6ff56b54422c5db6151107be48140d6b954e860/app/src/main/res/mipmap-xhdpi/ic_launcher_round.webp -------------------------------------------------------------------------------- /app/src/main/res/mipmap-xxhdpi/ic_launcher.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/zyrouge/symphony/d6ff56b54422c5db6151107be48140d6b954e860/app/src/main/res/mipmap-xxhdpi/ic_launcher.webp -------------------------------------------------------------------------------- /app/src/main/res/mipmap-xxhdpi/ic_launcher_round.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/zyrouge/symphony/d6ff56b54422c5db6151107be48140d6b954e860/app/src/main/res/mipmap-xxhdpi/ic_launcher_round.webp -------------------------------------------------------------------------------- /app/src/main/res/mipmap-xxxhdpi/ic_launcher.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/zyrouge/symphony/d6ff56b54422c5db6151107be48140d6b954e860/app/src/main/res/mipmap-xxxhdpi/ic_launcher.webp -------------------------------------------------------------------------------- /app/src/main/res/mipmap-xxxhdpi/ic_launcher_round.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/zyrouge/symphony/d6ff56b54422c5db6151107be48140d6b954e860/app/src/main/res/mipmap-xxxhdpi/ic_launcher_round.webp -------------------------------------------------------------------------------- /app/src/main/res/raw/placeholder_dark.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/zyrouge/symphony/d6ff56b54422c5db6151107be48140d6b954e860/app/src/main/res/raw/placeholder_dark.png -------------------------------------------------------------------------------- /app/src/main/res/raw/placeholder_light.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/zyrouge/symphony/d6ff56b54422c5db6151107be48140d6b954e860/app/src/main/res/raw/placeholder_light.png -------------------------------------------------------------------------------- /app/src/main/res/values/ic_launcher_background.xml: -------------------------------------------------------------------------------- 1 | <?xml version="1.0" encoding="utf-8"?> 2 | <resources> 3 | <color name="ic_launcher_background">#121212</color> 4 | </resources> -------------------------------------------------------------------------------- /app/src/main/res/values/splash.xml: -------------------------------------------------------------------------------- 1 | <?xml version="1.0" encoding="utf-8"?> 2 | <resources> 3 | 4 | <style name="Theme.Symphony.SplashScreen" parent="Theme.SplashScreen"> 5 | <item name="windowSplashScreenAnimatedIcon">@drawable/ic_launcher_foreground</item> 6 | <item name="windowSplashScreenBackground">@color/ic_launcher_background</item> 7 | <item name="postSplashScreenTheme">@style/Theme.Symphony</item> 8 | <item name="android:windowActionBar">false</item> 9 | <item name="android:windowNoTitle">true</item> 10 | </style> 11 | </resources> 12 | -------------------------------------------------------------------------------- /app/src/main/res/values/strings.xml: -------------------------------------------------------------------------------- 1 | <resources> 2 | <string name="app_name">Symphony</string> 3 | </resources> -------------------------------------------------------------------------------- /app/src/main/res/values/themes.xml: -------------------------------------------------------------------------------- 1 | <?xml version="1.0" encoding="utf-8"?> 2 | <resources> 3 | 4 | <style name="Theme.Symphony" parent="android:Theme.Material.NoActionBar"> 5 | <item name="android:windowLayoutInDisplayCutoutMode">default</item> 6 | </style> 7 | </resources> -------------------------------------------------------------------------------- /app/src/main/res/xml/backup_rules.xml: -------------------------------------------------------------------------------- 1 | <?xml version="1.0" encoding="utf-8"?><!-- 2 | Sample backup rules file; uncomment and customize as necessary. 3 | See https://developer.android.com/guide/topics/data/autobackup 4 | for details. 5 | Note: This file is ignored for devices older that API 31 6 | See https://developer.android.com/about/versions/12/backup-restore 7 | --> 8 | <full-backup-content> 9 | <!-- 10 | <include domain="sharedpref" path="."/> 11 | <exclude domain="sharedpref" path="device.xml"/> 12 | --> 13 | </full-backup-content> -------------------------------------------------------------------------------- /app/src/main/res/xml/data_extraction_rules.xml: -------------------------------------------------------------------------------- 1 | <?xml version="1.0" encoding="utf-8"?><!-- 2 | Sample data extraction rules file; uncomment and customize as necessary. 3 | See https://developer.android.com/about/versions/12/backup-restore#xml-changes 4 | for details. 5 | --> 6 | <data-extraction-rules> 7 | <cloud-backup> 8 | <!-- TODO: Use <include> and <exclude> to control what is backed up. 9 | <include .../> 10 | <exclude .../> 11 | --> 12 | </cloud-backup> 13 | <!-- 14 | <device-transfer> 15 | <include .../> 16 | <exclude .../> 17 | </device-transfer> 18 | --> 19 | </data-extraction-rules> -------------------------------------------------------------------------------- /app/src/test/resources/audio-id3v2.3.mp3: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/zyrouge/symphony/d6ff56b54422c5db6151107be48140d6b954e860/app/src/test/resources/audio-id3v2.3.mp3 -------------------------------------------------------------------------------- /app/src/test/resources/audio-id3v2.4.mp3: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/zyrouge/symphony/d6ff56b54422c5db6151107be48140d6b954e860/app/src/test/resources/audio-id3v2.4.mp3 -------------------------------------------------------------------------------- /app/src/test/resources/audio.flac: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/zyrouge/symphony/d6ff56b54422c5db6151107be48140d6b954e860/app/src/test/resources/audio.flac -------------------------------------------------------------------------------- /app/src/test/resources/audio.m4a: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/zyrouge/symphony/d6ff56b54422c5db6151107be48140d6b954e860/app/src/test/resources/audio.m4a -------------------------------------------------------------------------------- /app/src/test/resources/audio.mp3: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/zyrouge/symphony/d6ff56b54422c5db6151107be48140d6b954e860/app/src/test/resources/audio.mp3 -------------------------------------------------------------------------------- /app/src/test/resources/audio.ogg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/zyrouge/symphony/d6ff56b54422c5db6151107be48140d6b954e860/app/src/test/resources/audio.ogg -------------------------------------------------------------------------------- /build.gradle.kts: -------------------------------------------------------------------------------- 1 | plugins { 2 | alias(libs.plugins.android.app) apply false 3 | alias(libs.plugins.android.kotlin) apply false 4 | alias(libs.plugins.compose.compiler) apply false 5 | alias(libs.plugins.kotlin.serialization) apply false 6 | alias(libs.plugins.ksp) apply false 7 | alias(libs.plugins.room) apply false 8 | alias(libs.plugins.android.library) apply false 9 | } -------------------------------------------------------------------------------- /cli/android/move-outputs.ts: -------------------------------------------------------------------------------- 1 | import path from "path"; 2 | import fs from "fs-extra"; 3 | import archiver from "archiver"; 4 | import { Paths } from "../helpers/paths"; 5 | 6 | const APP_BUILD_TYPE = process.env.APP_BUILD_TYPE ?? "release"; 7 | const APP_VERSION_NAME = process.env.APP_VERSION_NAME; 8 | const APP_ABI = ["universal", "arm64-v8a", "armeabi-v7a", "x86_64", "x86"]; 9 | 10 | const main = async () => { 11 | if (typeof APP_VERSION_NAME !== "string") { 12 | throw new Error("Missing environment variable: APP_VERSION_NAME"); 13 | } 14 | await fs.ensureDir(Paths.distDir); 15 | for (const abi of APP_ABI) { 16 | await move( 17 | path.join( 18 | Paths.appDir, 19 | `build/outputs/apk/${APP_BUILD_TYPE}/app-${abi}-${APP_BUILD_TYPE}.apk`, 20 | ), 21 | path.join( 22 | Paths.distDir, 23 | `symphony-v${APP_VERSION_NAME}-${abi}.apk`, 24 | ), 25 | ); 26 | } 27 | await move( 28 | path.join( 29 | Paths.appDir, 30 | `build/outputs/bundle/${APP_BUILD_TYPE}/app-${APP_BUILD_TYPE}.aab`, 31 | ), 32 | path.join(Paths.distDir, `symphony-v${APP_VERSION_NAME}.aab`), 33 | ); 34 | await moveZipped( 35 | path.join( 36 | Paths.appDir, 37 | `build/outputs/mapping/${APP_BUILD_TYPE}/mapping.txt`, 38 | ), 39 | path.join(Paths.distDir, "mapping.zip"), 40 | ); 41 | await move( 42 | path.join( 43 | Paths.appDir, 44 | `build/outputs/native-debug-symbols/${APP_BUILD_TYPE}/native-debug-symbols.zip`, 45 | ), 46 | path.join(Paths.distDir, "native-debug-symbols.zip"), 47 | true, 48 | ); 49 | }; 50 | 51 | main(); 52 | 53 | async function move(from: string, to: string, skippable: boolean = false) { 54 | if (skippable && !(await fs.exists(from))) { 55 | return; 56 | } 57 | await fs.move(from, to); 58 | console.log(`Moved "${from}" to "${to}".`); 59 | } 60 | 61 | async function moveZipped(from: string, to: string) { 62 | await fs.ensureFile(to); 63 | const archive = archiver.create("zip"); 64 | archive.pipe(fs.createWriteStream(to)); 65 | archive.file(from, { 66 | name: path.basename(from), 67 | }); 68 | await archive.finalize(); 69 | console.log(`Zipped "${from}" to "${to}".`); 70 | } 71 | -------------------------------------------------------------------------------- /cli/changelogs/fastlane-character-limit.ts: -------------------------------------------------------------------------------- 1 | import p from "path"; 2 | import fs from "fs-extra"; 3 | import { Paths } from "../helpers/paths"; 4 | 5 | const CHANGELOGS_CHARACTER_LIMIT = 540; 6 | 7 | const start = async () => { 8 | let failed = 0; 9 | const dirnames = await fs.readdir(Paths.metadataDir); 10 | for (const x of dirnames) { 11 | const dir = p.join(Paths.metadataDir, x, "changelogs"); 12 | for (const name of fs.readdirSync(dir)) { 13 | const path = p.join(dir, name); 14 | const content = await fs.readFile(path, "utf-8"); 15 | const length = content.length; 16 | const rpath = p.relative(Paths.metadataDir, path); 17 | if (length > CHANGELOGS_CHARACTER_LIMIT) { 18 | failed++; 19 | const excess = length - CHANGELOGS_CHARACTER_LIMIT; 20 | console.log( 21 | `${rpath} has ${length} characters (excess by ${excess} characters)`, 22 | ); 23 | } 24 | } 25 | } 26 | process.exit(failed === 0 ? 0 : 1); 27 | }; 28 | 29 | start(); 30 | -------------------------------------------------------------------------------- /cli/git/diff-files-yn.ts: -------------------------------------------------------------------------------- 1 | import { Git } from "../helpers/git"; 2 | 3 | const main = async () => { 4 | const [branch, ...pathPrefix] = process.argv.slice(2); 5 | if (!branch) throw new Error("Missing argument: tag"); 6 | if (!pathPrefix.length) throw new Error("Missing argument: pathPrefix"); 7 | const files = await Git.diffNames(branch); 8 | const affected = files.some((x) => affectsPathPrefix(pathPrefix, x)); 9 | console.log(affected ? "yes" : "no"); 10 | }; 11 | 12 | main(); 13 | 14 | function affectsPathPrefix(pathPrefix: string[], file: string) { 15 | return pathPrefix.some((x) => file.startsWith(x)); 16 | } 17 | -------------------------------------------------------------------------------- /cli/git/latest-tag.ts: -------------------------------------------------------------------------------- 1 | import { Git } from "../helpers/git"; 2 | 3 | const main = async () => { 4 | const tag = await Git.latestTag(); 5 | console.log(tag); 6 | }; 7 | 8 | main(); 9 | -------------------------------------------------------------------------------- /cli/git/tag-exists-yn.ts: -------------------------------------------------------------------------------- 1 | import { Git } from "../helpers/git"; 2 | 3 | const main = async () => { 4 | const [tag] = process.argv.slice(2); 5 | if (!tag) throw new Error("Missing argument: tag"); 6 | const exists = await Git.tagExists(tag); 7 | console.log(exists ? "yes" : "no"); 8 | }; 9 | 10 | main(); 11 | -------------------------------------------------------------------------------- /cli/git/tag-exists.ts: -------------------------------------------------------------------------------- 1 | import { Git } from "../helpers/git"; 2 | 3 | const main = async () => { 4 | const [tag] = process.argv.slice(2); 5 | if (!tag) throw new Error("Missing argument: tag"); 6 | const exists = await Git.tagExists(tag); 7 | if (exists) throw new Error(`Tag ${tag} already exists`); 8 | console.log(`Tag ${tag} is available`); 9 | }; 10 | 11 | main(); 12 | -------------------------------------------------------------------------------- /cli/helpers/git.ts: -------------------------------------------------------------------------------- 1 | import { spawnSync } from "child_process"; 2 | 3 | export interface GitSpawnResult { 4 | status: number | null; 5 | stdout: string; 6 | stderr: string; 7 | } 8 | 9 | export class Git { 10 | static async getLatestRevisionShort() { 11 | const proc = await Git.spawn(["rev-parse", "--short", "HEAD"]); 12 | if (proc.status !== 0) throw new Error(`Unable to get revision`); 13 | return proc.stdout; 14 | } 15 | 16 | static async getRevisionDate(sha: string) { 17 | const proc = await Git.spawn([ 18 | "show", 19 | "--no-patch", 20 | "--no-notes", 21 | "--pretty='%cI'", 22 | sha, 23 | ]); 24 | if (proc.status !== 0) throw new Error(`Unable to get revision date`); 25 | const isoString = proc.stdout.slice(1, -1); 26 | return new Date(isoString); 27 | } 28 | 29 | static async tagExists(tag: string) { 30 | const proc = await Git.spawn([ 31 | "ls-remote", 32 | "--exit-code", 33 | "--tags", 34 | "origin", 35 | tag, 36 | ]); 37 | return proc.status === 0; 38 | } 39 | 40 | static async diffNames(branch: string) { 41 | const proc = await Git.spawn(["diff", "--name-only", branch, "."]); 42 | if (proc.status !== 0) { 43 | throw new Error(`Unable to get diff (${proc.stderr})`); 44 | } 45 | return proc.stdout.split("\n"); 46 | } 47 | 48 | static async latestTag() { 49 | const proc = await Git.spawn(["describe", "--abbrev=0", "--tags"]); 50 | if (proc.status !== 0) { 51 | throw new Error(`Unable to get latest tag (${proc.stderr})`); 52 | } 53 | return proc.stdout; 54 | } 55 | 56 | static async spawn(args: string[]) { 57 | const proc = spawnSync("git", args); 58 | const result: GitSpawnResult = { 59 | status: proc.status, 60 | stdout: proc.stdout.toString().trim(), 61 | stderr: proc.stderr.toString().trim(), 62 | }; 63 | return result; 64 | } 65 | } 66 | -------------------------------------------------------------------------------- /cli/helpers/paths.ts: -------------------------------------------------------------------------------- 1 | import path from "path"; 2 | 3 | export class Paths { 4 | static rootDir = path.resolve(__dirname, "../../"); 5 | static appDir = path.join(Paths.rootDir, "app"); 6 | static distDir = path.join(Paths.rootDir, "dist"); 7 | static metadataDir = path.join(Paths.rootDir, "metadata"); 8 | } 9 | -------------------------------------------------------------------------------- /cli/helpers/shortcuts.ts: -------------------------------------------------------------------------------- 1 | export const s = { 2 | let: <U, V>(value: U, transform: (value: U) => V): V => transform(value), 3 | }; 4 | -------------------------------------------------------------------------------- /cli/helpers/version.ts: -------------------------------------------------------------------------------- 1 | import path from "path"; 2 | import { promises as fs } from "fs"; 3 | import { ZemVer } from "@zyrouge/zemver"; 4 | import { Paths } from "./paths"; 5 | 6 | export class Versioner { 7 | static appBuildGradlePath = path.join(Paths.appDir, "build.gradle.kts"); 8 | static versionCodeRegex = /versionCode = (\d+)/; 9 | static versionNameRegex = /versionName = "([^"]+)"/; 10 | 11 | static async getVersion() { 12 | const content = (await fs.readFile(this.appBuildGradlePath)).toString(); 13 | const versionCode = this.versionCodeRegex.exec(content)?.[1]; 14 | const versionName = this.versionNameRegex.exec(content)?.[1]; 15 | if (!versionCode) { 16 | throw new Error("Unable to parse version code"); 17 | } 18 | if (!versionName) { 19 | throw new Error("Unable to parse version name"); 20 | } 21 | const version = ZemVer.parse(versionName); 22 | if (version.code.toString() !== versionCode) { 23 | throw new Error("Mismatching version code and version name"); 24 | } 25 | return version; 26 | } 27 | 28 | static async updateVersion(version: ZemVer) { 29 | let content = (await fs.readFile(this.appBuildGradlePath)).toString(); 30 | content = content.replace( 31 | this.versionCodeRegex, 32 | `versionCode = ${version.code}`, 33 | ); 34 | content = content.replace( 35 | this.versionNameRegex, 36 | `versionName = "${version}"`, 37 | ); 38 | await fs.writeFile(this.appBuildGradlePath, content); 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /cli/version/bump.ts: -------------------------------------------------------------------------------- 1 | import pico from "picocolors"; 2 | import { Versioner } from "../helpers/version"; 3 | 4 | const main = async () => { 5 | const pVersion = await Versioner.getVersion(); 6 | const version = pVersion.bump(); 7 | await Versioner.updateVersion(version); 8 | console.log( 9 | `Version bumped from ${pico.cyan(pVersion.toString())} to ${pico.cyan(version.toString())}!`, 10 | ); 11 | }; 12 | 13 | main(); 14 | -------------------------------------------------------------------------------- /cli/version/print-canary.ts: -------------------------------------------------------------------------------- 1 | import { Git } from "../helpers/git"; 2 | import { Versioner } from "../helpers/version"; 3 | 4 | const main = async () => { 5 | const pVersion = await Versioner.getVersion(); 6 | const sha = await Git.getLatestRevisionShort(); 7 | const time = await Git.getRevisionDate(sha); 8 | const version = pVersion.copyWith({ 9 | year: time.getFullYear(), 10 | month: time.getMonth() + 1, 11 | code: pVersion.code + 1, 12 | prerelease: "nightly", 13 | build: sha, 14 | }); 15 | console.log(version.toString()); 16 | }; 17 | 18 | main(); 19 | -------------------------------------------------------------------------------- /cli/version/print-nightly.ts: -------------------------------------------------------------------------------- 1 | import { Git } from "../helpers/git"; 2 | import { Versioner } from "../helpers/version"; 3 | 4 | const main = async () => { 5 | const pVersion = await Versioner.getVersion(); 6 | const sha = await Git.getLatestRevisionShort(); 7 | const time = await Git.getRevisionDate(sha); 8 | const version = pVersion.copyWith({ 9 | year: time.getFullYear(), 10 | month: time.getMonth() + 1, 11 | code: pVersion.code + 1, 12 | prerelease: "nightly", 13 | build: sha, 14 | }); 15 | console.log(version.toString()); 16 | }; 17 | 18 | main(); 19 | -------------------------------------------------------------------------------- /cli/version/print.ts: -------------------------------------------------------------------------------- 1 | import { Versioner } from "../helpers/version"; 2 | 3 | const main = async () => { 4 | const version = await Versioner.getVersion(); 5 | console.log(version.toString()); 6 | }; 7 | 8 | main(); 9 | -------------------------------------------------------------------------------- /gradle.properties: -------------------------------------------------------------------------------- 1 | android.nonTransitiveRClass=true 2 | kotlin.code.style=official 3 | org.gradle.jvmargs=-Xmx2048m -XX:MaxMetaspaceSize=512m -XX:+HeapDumpOnOutOfMemoryError -Dfile.encoding=UTF-8 4 | android.useAndroidX=true 5 | -------------------------------------------------------------------------------- /gradle/wrapper/gradle-wrapper.jar: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/zyrouge/symphony/d6ff56b54422c5db6151107be48140d6b954e860/gradle/wrapper/gradle-wrapper.jar -------------------------------------------------------------------------------- /gradle/wrapper/gradle-wrapper.properties: -------------------------------------------------------------------------------- 1 | #Thu Apr 18 14:18:59 IST 2024 2 | distributionBase=GRADLE_USER_HOME 3 | distributionPath=wrapper/dists 4 | distributionUrl=https\://services.gradle.org/distributions/gradle-8.10.2-bin.zip 5 | zipStoreBase=GRADLE_USER_HOME 6 | zipStorePath=wrapper/dists 7 | -------------------------------------------------------------------------------- /gradlew.bat: -------------------------------------------------------------------------------- 1 | @rem 2 | @rem Copyright 2015 the original author or authors. 3 | @rem 4 | @rem Licensed under the Apache License, Version 2.0 (the "License"); 5 | @rem you may not use this file except in compliance with the License. 6 | @rem You may obtain a copy of the License at 7 | @rem 8 | @rem https://www.apache.org/licenses/LICENSE-2.0 9 | @rem 10 | @rem Unless required by applicable law or agreed to in writing, software 11 | @rem distributed under the License is distributed on an "AS IS" BASIS, 12 | @rem WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | @rem See the License for the specific language governing permissions and 14 | @rem limitations under the License. 15 | @rem 16 | 17 | @if "%DEBUG%" == "" @echo off 18 | @rem ########################################################################## 19 | @rem 20 | @rem Gradle startup script for Windows 21 | @rem 22 | @rem ########################################################################## 23 | 24 | @rem Set local scope for the variables with windows NT shell 25 | if "%OS%"=="Windows_NT" setlocal 26 | 27 | set DIRNAME=%~dp0 28 | if "%DIRNAME%" == "" set DIRNAME=. 29 | set APP_BASE_NAME=%~n0 30 | set APP_HOME=%DIRNAME% 31 | 32 | @rem Resolve any "." and ".." in APP_HOME to make it shorter. 33 | for %%i in ("%APP_HOME%") do set APP_HOME=%%~fi 34 | 35 | @rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. 36 | set DEFAULT_JVM_OPTS="-Xmx64m" "-Xms64m" 37 | 38 | @rem Find java.exe 39 | if defined JAVA_HOME goto findJavaFromJavaHome 40 | 41 | set JAVA_EXE=java.exe 42 | %JAVA_EXE% -version >NUL 2>&1 43 | if "%ERRORLEVEL%" == "0" goto execute 44 | 45 | echo. 46 | echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. 47 | echo. 48 | echo Please set the JAVA_HOME variable in your environment to match the 49 | echo location of your Java installation. 50 | 51 | goto fail 52 | 53 | :findJavaFromJavaHome 54 | set JAVA_HOME=%JAVA_HOME:"=% 55 | set JAVA_EXE=%JAVA_HOME%/bin/java.exe 56 | 57 | if exist "%JAVA_EXE%" goto execute 58 | 59 | echo. 60 | echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% 61 | echo. 62 | echo Please set the JAVA_HOME variable in your environment to match the 63 | echo location of your Java installation. 64 | 65 | goto fail 66 | 67 | :execute 68 | @rem Setup the command line 69 | 70 | set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar 71 | 72 | 73 | @rem Execute Gradle 74 | "%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %* 75 | 76 | :end 77 | @rem End local scope for the variables with windows NT shell 78 | if "%ERRORLEVEL%"=="0" goto mainEnd 79 | 80 | :fail 81 | rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of 82 | rem the _cmd.exe /c_ return code! 83 | if not "" == "%GRADLE_EXIT_CONSOLE%" exit 1 84 | exit /b 1 85 | 86 | :mainEnd 87 | if "%OS%"=="Windows_NT" endlocal 88 | 89 | :omega 90 | -------------------------------------------------------------------------------- /media/banner-16-9-compact.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/zyrouge/symphony/d6ff56b54422c5db6151107be48140d6b954e860/media/banner-16-9-compact.png -------------------------------------------------------------------------------- /media/banner-16-9.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/zyrouge/symphony/d6ff56b54422c5db6151107be48140d6b954e860/media/banner-16-9.png -------------------------------------------------------------------------------- /media/banner.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/zyrouge/symphony/d6ff56b54422c5db6151107be48140d6b954e860/media/banner.png -------------------------------------------------------------------------------- /media/icon-gray-light.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/zyrouge/symphony/d6ff56b54422c5db6151107be48140d6b954e860/media/icon-gray-light.png -------------------------------------------------------------------------------- /media/icon-gray.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/zyrouge/symphony/d6ff56b54422c5db6151107be48140d6b954e860/media/icon-gray.png -------------------------------------------------------------------------------- /media/icon-opaque-inverted.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/zyrouge/symphony/d6ff56b54422c5db6151107be48140d6b954e860/media/icon-opaque-inverted.png -------------------------------------------------------------------------------- /media/icon-opaque.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/zyrouge/symphony/d6ff56b54422c5db6151107be48140d6b954e860/media/icon-opaque.png -------------------------------------------------------------------------------- /media/icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/zyrouge/symphony/d6ff56b54422c5db6151107be48140d6b954e860/media/icon.png -------------------------------------------------------------------------------- /media/icon.svg: -------------------------------------------------------------------------------- 1 | <svg width="1000" height="1000" viewBox="0 0 1000 1000" fill="none" xmlns="http://www.w3.org/2000/svg"> 2 | <g clip-path="url(#clip0_302_529)"> 3 | <mask id="mask0_302_529" style="mask-type:alpha" maskUnits="userSpaceOnUse" x="0" y="0" width="1000" height="1000"> 4 | <rect x="887" y="425" width="100" height="150" rx="50" fill="white"/> 5 | <rect x="712" y="225" width="100" height="550" rx="50" fill="white"/> 6 | <rect x="537" y="45" width="100" height="800" rx="50" fill="white"/> 7 | <rect x="462" y="954.091" width="100" height="800" rx="50" transform="rotate(-180 462 954.091)" fill="white"/> 8 | <rect x="287" y="750" width="100" height="500" rx="50" transform="rotate(-180 287 750)" fill="white"/> 9 | <rect x="112" y="625" width="100" height="250" rx="50" transform="rotate(-180 112 625)" fill="white"/> 10 | </mask> 11 | <g mask="url(#mask0_302_529)"> 12 | <rect y="45" width="1000" height="909" fill="#D9D9D9"/> 13 | <rect y="45" width="1000" height="909" fill="url(#paint0_linear_302_529)"/> 14 | </g> 15 | </g> 16 | <defs> 17 | <linearGradient id="paint0_linear_302_529" x1="500" y1="45" x2="500" y2="954" gradientUnits="userSpaceOnUse"> 18 | <stop stop-color="#C084FC"/> 19 | <stop offset="1" stop-color="#A855F7"/> 20 | </linearGradient> 21 | <clipPath id="clip0_302_529"> 22 | <rect width="1000" height="1000" fill="white"/> 23 | </clipPath> 24 | </defs> 25 | </svg> 26 | -------------------------------------------------------------------------------- /media/playstore-feature-graphic.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/zyrouge/symphony/d6ff56b54422c5db6151107be48140d6b954e860/media/playstore-feature-graphic.png -------------------------------------------------------------------------------- /media/screenshot-graphic-1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/zyrouge/symphony/d6ff56b54422c5db6151107be48140d6b954e860/media/screenshot-graphic-1.png -------------------------------------------------------------------------------- /media/screenshot-graphic-2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/zyrouge/symphony/d6ff56b54422c5db6151107be48140d6b954e860/media/screenshot-graphic-2.png -------------------------------------------------------------------------------- /media/screenshot-graphic-3.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/zyrouge/symphony/d6ff56b54422c5db6151107be48140d6b954e860/media/screenshot-graphic-3.png -------------------------------------------------------------------------------- /media/screenshot-graphic-4.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/zyrouge/symphony/d6ff56b54422c5db6151107be48140d6b954e860/media/screenshot-graphic-4.png -------------------------------------------------------------------------------- /media/screenshots.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/zyrouge/symphony/d6ff56b54422c5db6151107be48140d6b954e860/media/screenshots.png -------------------------------------------------------------------------------- /metadata/en-US/changelogs/104.txt: -------------------------------------------------------------------------------- 1 | * Ability to set System with Dark or Black theme. 2 | * Fix crash on system theme changes. 3 | * Ability to set content and text scaling. 4 | * Fix crash when removing songs from queue. 5 | * For You page now suggests only after loading all the songs. 6 | * Ability to re-scan library. 7 | * Playback time now supports days and hours. 8 | * Fix audio focus regain. 9 | * Implement predictive back handling and edge-to-edge. 10 | * Added Spanish, Uchinaguchi, French, Japanese and Portuguese translation. 11 | -------------------------------------------------------------------------------- /metadata/en-US/changelogs/105.txt: -------------------------------------------------------------------------------- 1 | * Fix search input overflow beyond window. 2 | * Show track number in Album page. 3 | * Updated French, Japanese and Uchinaguchi translations. 4 | -------------------------------------------------------------------------------- /metadata/en-US/changelogs/106.txt: -------------------------------------------------------------------------------- 1 | - Added option to view equalizer. 2 | - Album covers are now taken directly from songs. 3 | - Playlist titles can now be edited. 4 | - Local playlists can now be exported. 5 | - Fixed delayed playlist loading indicator. 6 | - Option to pause at end of current song. 7 | - Show track number in album view. 8 | - Fixed auto play/pause by accessories. 9 | - Fixed delayed pause when audio devices are unplugged. 10 | - Fixed uncached re-scanning of blacklisted songs. 11 | - Added option to share songs. -------------------------------------------------------------------------------- /metadata/en-US/changelogs/107.txt: -------------------------------------------------------------------------------- 1 | - Tapping active home page or swiping bottom bar will show all tabs. 2 | - Context aware search button. 3 | - Search results now have a minimal score filter. 4 | - Handle playback errors. 5 | - Fixed buggy playback position slider. 6 | - Enforce text direction. 7 | - Fixed unexpected speed and pitch behavior. 8 | - Removed bluetooth permission. 9 | - Sliders in settings page now support taps. 10 | - Improved mini-player animation. 11 | -------------------------------------------------------------------------------- /metadata/en-US/changelogs/108.txt: -------------------------------------------------------------------------------- 1 | - Support synced and `.lrc` lyrics. 2 | - Added full-screen lyrics view. 3 | - Ability to parse multi-value tags. 4 | - Ability to click/copy values from song information dialog. 5 | - Option to save current queue as playlist. 6 | - Respect song sorting when using play buttons. 7 | - Option to disable mini-player text marquee. 8 | - Fix focus loss causing the player to resume. 9 | - Fix incorrect parsing of track numbers. 10 | - Responsive now playing song cover. 11 | - Fix genre page title. 12 | - Fix locale text direction. 13 | - Added mini-player swipe up/down gesture. 14 | -------------------------------------------------------------------------------- /metadata/en-US/changelogs/109.txt: -------------------------------------------------------------------------------- 1 | - Fix swapped track and disc number. 2 | - Fix track number below 1000 being ignored. 3 | - Added light mode image placeholder. 4 | - Update of East-Slavic translations. 5 | - Updated Japanese and Uchinaguchi translations. 6 | -------------------------------------------------------------------------------- /metadata/en-US/changelogs/110.txt: -------------------------------------------------------------------------------- 1 | * Added individual folders view. 2 | * Added monochrome icon back. 3 | * Fixed incorrect album songs sort. 4 | * Added Persian (Farsi) and Polish translation. 5 | * Updated Portuguese, Japanese and Uchinaguchi translations. 6 | -------------------------------------------------------------------------------- /metadata/en-US/changelogs/112.txt: -------------------------------------------------------------------------------- 1 | * **BREAKING CHANGE**: This update removes support for playlists of previous versions due to removal of `MediaStore` usage. It is HIGHLY RECOMMENDED to export the playlists using the previous version before updating. You can re-import them after updating. 2 | * Implemented custom media resolver and metadata decoder. (Experimental) 3 | * Added speed and pitch slider. 4 | * Improved M3U playlist parser. 5 | * Use Artists separator for Album Artists. 6 | * Improved fuzzy search. 7 | * Updated translations. -------------------------------------------------------------------------------- /metadata/en-US/changelogs/113.txt: -------------------------------------------------------------------------------- 1 | * Implemented custom media resolver and metadata decoder. (Experimental) 2 | * Added speed and pitch slider. 3 | * Fixed incorrect lyrics parsing when content contain CRLF. 4 | * Fixed popup overflow in Settings. 5 | * Improved M3U playlist parser. 6 | * Use Artists separator for Album Artists. 7 | * Fixed Albums categorization when Artists are different. 8 | * Improved fuzzy search. 9 | * Updated translations. 10 | * This is a hot-fix upon v2024.11.112. 11 | -------------------------------------------------------------------------------- /metadata/en-US/changelogs/115.txt: -------------------------------------------------------------------------------- 1 | * Ability to modify Album cover quality. 2 | * Added support for gapless playback. 3 | * Ability to modify grid sizes. 4 | * Added sort by album year. 5 | * Added ability to disable case-sensitive sorting. 6 | * Display album duration in Album view. 7 | * Display album year in Album view. 8 | * Added song duration setting. 9 | * Fixed playlist addition/deletion issue. 10 | * Albums now don't consider year to be a factor. 11 | * Fixed missing buttons in traditional layout. 12 | -------------------------------------------------------------------------------- /metadata/en-US/changelogs/71.txt: -------------------------------------------------------------------------------- 1 | - Fixed notification artwork being pixelated. 2 | - Initial F-droid release. 3 | -------------------------------------------------------------------------------- /metadata/en-US/changelogs/79.txt: -------------------------------------------------------------------------------- 1 | - Support for embedded lyrics. 2 | - Fix missing playback bar on notifications. 3 | - Added Speed & pitch control. 4 | - Improved animations in Now Playing page. 5 | - Improved internal MediaStore access. 6 | - Show mini-player in Search page. 7 | - Text in mini-player now scrolls when it's too long. 8 | - Clickable artist name in Now Playing page. 9 | - Identify and act upon bluetooth headphone actions. 10 | - Ability to create playlists when adding songs using "Add to playlist". 11 | - System tones are blacklisted by default. 12 | -------------------------------------------------------------------------------- /metadata/en-US/changelogs/89.txt: -------------------------------------------------------------------------------- 1 | - Fix crash during start-up and library scanning. 2 | - Add monochrome icon. 3 | - Fix pause music on bluetooth headphone disconnect. 4 | - Add search in manage playlist dialog. 5 | - Ability to completely opt-in and opt-out of version checks. 6 | - Add Turkish and German translation. 7 | - Update Belarusian, Russian and Ukrainian translations. 8 | -------------------------------------------------------------------------------- /metadata/en-US/changelogs/92.txt: -------------------------------------------------------------------------------- 1 | - Fix incorrect song displayed when shuffled. 2 | - Fix error when empty playlist is played. 3 | - Fix monochrome round icons. 4 | - Add Simplified Chinese translation. 5 | - Update German and Italian translation. 6 | -------------------------------------------------------------------------------- /metadata/en-US/changelogs/93.txt: -------------------------------------------------------------------------------- 1 | - Fix "Create new playlist" not working in "Add to playlist" dialog. 2 | - Fix undismissible Settings Multi-tile Option dialog. 3 | - Visual indication showing if a song is already in the playlist when adding song to playlist. 4 | - Add "Playlists" tab to default Home tabs. 5 | - Show "Favorites" playlist at top in "custom" sort order. 6 | -------------------------------------------------------------------------------- /metadata/en-US/full_description.txt: -------------------------------------------------------------------------------- 1 | Symphony is a lightweight, elegant music player that enhances your offline music experience. Supports Android 9 and later. 2 | 3 | -------------------------------------------------------------------------------- /metadata/en-US/images/featureGraphic.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/zyrouge/symphony/d6ff56b54422c5db6151107be48140d6b954e860/metadata/en-US/images/featureGraphic.png -------------------------------------------------------------------------------- /metadata/en-US/images/icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/zyrouge/symphony/d6ff56b54422c5db6151107be48140d6b954e860/metadata/en-US/images/icon.png -------------------------------------------------------------------------------- /metadata/en-US/images/phoneScreenshots/screenshot-album-artists.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/zyrouge/symphony/d6ff56b54422c5db6151107be48140d6b954e860/metadata/en-US/images/phoneScreenshots/screenshot-album-artists.png -------------------------------------------------------------------------------- /metadata/en-US/images/phoneScreenshots/screenshot-folders.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/zyrouge/symphony/d6ff56b54422c5db6151107be48140d6b954e860/metadata/en-US/images/phoneScreenshots/screenshot-folders.png -------------------------------------------------------------------------------- /metadata/en-US/images/phoneScreenshots/screenshot-for-you.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/zyrouge/symphony/d6ff56b54422c5db6151107be48140d6b954e860/metadata/en-US/images/phoneScreenshots/screenshot-for-you.png -------------------------------------------------------------------------------- /metadata/en-US/images/phoneScreenshots/screenshot-genres.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/zyrouge/symphony/d6ff56b54422c5db6151107be48140d6b954e860/metadata/en-US/images/phoneScreenshots/screenshot-genres.png -------------------------------------------------------------------------------- /metadata/en-US/images/phoneScreenshots/screenshot-now-playing.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/zyrouge/symphony/d6ff56b54422c5db6151107be48140d6b954e860/metadata/en-US/images/phoneScreenshots/screenshot-now-playing.png -------------------------------------------------------------------------------- /metadata/en-US/images/phoneScreenshots/screenshot-queue.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/zyrouge/symphony/d6ff56b54422c5db6151107be48140d6b954e860/metadata/en-US/images/phoneScreenshots/screenshot-queue.png -------------------------------------------------------------------------------- /metadata/en-US/images/phoneScreenshots/screenshot-songs.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/zyrouge/symphony/d6ff56b54422c5db6151107be48140d6b954e860/metadata/en-US/images/phoneScreenshots/screenshot-songs.png -------------------------------------------------------------------------------- /metadata/en-US/images/phoneScreenshots/screenshot-tree.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/zyrouge/symphony/d6ff56b54422c5db6151107be48140d6b954e860/metadata/en-US/images/phoneScreenshots/screenshot-tree.png -------------------------------------------------------------------------------- /metadata/en-US/short_description.txt: -------------------------------------------------------------------------------- 1 | Offline music playback is now a breeze 2 | -------------------------------------------------------------------------------- /metadata/en-US/title.txt: -------------------------------------------------------------------------------- 1 | Symphony 2 | -------------------------------------------------------------------------------- /metadata/ja-JP/changelogs/104.txt: -------------------------------------------------------------------------------- 1 | * システムをダークまたはブラックのテーマに設定する機能。 2 | * システムテーマ変更時のクラッシュを修正しました。 3 | * コンテンツとテキストのスケーリングを設定する機能。 4 | * キューから曲を削除する際のクラッシュを修正しました。 5 | * あなたのためにページでは、全ての曲をロードした後にのみ提案される様になりました。 6 | * ライブラリを再スキャンする機能。 7 | * 再生時間は日と時間をサポートするようになりました。 8 | * 音声フォーカスの回復を修正しました。 9 | * 予測的なバックハンドリングとエッジツーエッジを実装。 10 | * スペイン語、うちなーぐち、フランス語、日本語、ポルトガル語の翻訳を追加しました。 11 | -------------------------------------------------------------------------------- /metadata/ja-JP/changelogs/105.txt: -------------------------------------------------------------------------------- 1 | * 検索入力がウィンドウを超えてオーバーフローする問題を修正しました。 2 | * アルバムページにトラック番号を表示。 3 | * フランス語、日本語、うちなーぐちの翻訳を更新しました。 4 | -------------------------------------------------------------------------------- /metadata/ja-JP/changelogs/106.txt: -------------------------------------------------------------------------------- 1 | - イコライザーを表示するオプションを追加しました。 2 | - アルバムカバーは曲から直接取得される様になりました。 3 | - プレイリストのタイトルを編集出来る様になりました。 4 | - ローカルプレイリストをエクスポート出来る様になりました。 5 | - プレイリスト読み込みインジケーターを修正しました。 6 | - 現在の曲の終わりで一時停止するオプション。 7 | - アルバムビューでトラック番号を表示される様になりました。 8 | - アクセサリによる自動再生/一時停止を修正しました。 9 | - オーディオデバイスが接続されていない時の一時停止の遅延を修正しました。 10 | - ブラックリストに登録された曲のキャッシュされていない再スキャンを修正しました。 11 | - 曲を共有するオプションが追加されました。 12 | -------------------------------------------------------------------------------- /metadata/ja-JP/changelogs/107.txt: -------------------------------------------------------------------------------- 1 | - アクティブなホーム ページをタップするか、下部バーをスワイプすると、全てのタブが表示されます。 2 | - コンテキスト認識型の検索ボタンを追加しました。 3 | - 検索結果には最小限のフィルターが追加されました。 4 | - 再生エラーを処理します。 5 | - バグのある再生位置スライダーを修正しました。 6 | - テキストの方向を強制します。 7 | - 予期しない速度とピッチの動作を修正しました。 8 | - Bluetoothの許可を削除しました。 9 | - 設定ページのスライダーがタップをサポートする様になりました。 10 | - ミニプレーヤーのアニメーションが改善されました。 11 | -------------------------------------------------------------------------------- /metadata/ja-JP/changelogs/108.txt: -------------------------------------------------------------------------------- 1 | - 同期された歌詞と `.lrc` の歌詞をサポートしました。 2 | - 全画面での歌詞表示機能を追加しました。 3 | - 複数値のタグを解析する機能を追加しました。 4 | - 曲情報ダイアログから値をクリック/コピーする機能を追加しました。 5 | - 現在のキューをプレイリストとして保存するオプション。 6 | - 再生ボタンを使用するときは、曲の並べ替えを尊重する様に。 7 | - ミニプレーヤーのテキストマーキーを無効にするオプションを追加しました。 8 | - プレーヤーが再開する原因となるフォーカスの損失を修正しました。 9 | - トラック番号の誤った解析を修正しました。 10 | - レスポンシブでカバー曲を再生中。 11 | - ジャンルページでのタイトルを修正しました。 12 | - ロケールでのテキストの方向を修正しました。 13 | - ミニプレーヤーの上下スワイプジェスチャーを追加しました。 14 | - 「あなたのために」ページで空のメッセージが表示される問題を修正しました。 15 | -------------------------------------------------------------------------------- /metadata/ja-JP/changelogs/109.txt: -------------------------------------------------------------------------------- 1 | - スワップされたトラックとディスク番号を修正しました。 2 | - 1000未満のトラック番号が無視される問題を修正しました。 3 | - ライトモード画像プレースホルダーを追加しました。 4 | - 東スラブ語の翻訳を更新しました。 5 | - 日本語とうちなーぐちの翻訳を更新しました。 6 | -------------------------------------------------------------------------------- /metadata/ja-JP/changelogs/71.txt: -------------------------------------------------------------------------------- 1 | - 通知アートワークがピクセル化される問題を修正しました。 2 | - F-droidでの初期リリース。 3 | -------------------------------------------------------------------------------- /metadata/ja-JP/changelogs/79.txt: -------------------------------------------------------------------------------- 1 | - 埋め込み歌詞をサポート。 2 | - 再生バーが通知に表示されない問題を修正しました。 3 | - スピードとピッチのコントロール機能を追加しました。 4 | - 「再生中」ページのアニメーションが改善されました。 5 | - 内部へのアクセスが改善されました。 6 | - 検索ページにミニプレーヤーを表示します。 7 | - ミニプレーヤーのテキストが長すぎる場合、スクロールするようになりました。 8 | - 再生中ページでクリック可能なアーティスト名。 9 | - Bluetoothヘッドフォンのアクションを特定し、それに基づいて行動する様になりました。 10 | - 「プレイリストに追加」を使用して曲を追加するときにプレイリストを作成する機能。 11 | - システム トーンはデフォルトでブラックリストに登録される様になりました。 12 | - 「プレイリストから削除」オプションを追加しました。 13 | - アルバムアーティストが間違っている問題を修正しました。 14 | - デフォルトのプレイリストの並べ替え順序をカスタムに変更しました。 15 | - 黒のテーマがより暗くなりました。 16 | - 「あなたのために」ページにおすすめのアルバムアーティスト。 17 | - あなたのためにページでカスタマイズが可能になりました。 18 | -------------------------------------------------------------------------------- /metadata/ja-JP/changelogs/89.txt: -------------------------------------------------------------------------------- 1 | - 起動時およびライブラリのスキャン中のクラッシュを修正しました。 2 | - モノクロアイコンを追加しました。 3 | - Bluetoothヘッドフォンの接続が切断されたときに音楽が一時停止する問題を修正しました。 4 | - プレイリストの管理ダイアログに検索を追加しました。 5 | - バージョンチェックを完全にオプトインおよびオプトアウトする機能。 6 | - トルコ語とドイツ語の翻訳を追加しました。 7 | - ベラルーシ語、ロシア語、ウクライナ語の翻訳を更新しました。 8 | -------------------------------------------------------------------------------- /metadata/ja-JP/changelogs/92.txt: -------------------------------------------------------------------------------- 1 | - シャッフル時に表示される曲が間違っていたのを修正しました。 2 | - 空のプレイリストが再生される時のエラーを修正しました。 3 | - モノクロの丸いアイコンを修正しました。 4 | - 簡体字中国語の翻訳を追加しました。 5 | - ドイツ語とイタリア語の翻訳を更新しました。 6 | -------------------------------------------------------------------------------- /metadata/ja-JP/changelogs/93.txt: -------------------------------------------------------------------------------- 1 | - 「プレイリストに追加」ダイアログで「新しいプレイリストの作成」が機能しない問題を修正しました 2 | - オプションダイアログを閉じることができない問題を修正しました 3 | - プレイリストに曲を追加する時に、曲が既にプレイリストにあるかどうかを視覚的に示します。 4 | - デフォルトのホームタブに「プレイリスト」タブを追加しました 5 | - 「お気に入り」プレイリストを「カスタム」ソート順で一番上に表示します。 6 | -------------------------------------------------------------------------------- /metadata/ja-JP/full_description.txt: -------------------------------------------------------------------------------- 1 | Symphonyは、あなたのオフラインでの音楽体験を向上させる軽量でエレガントな音楽プレーヤーです。Android 9以降をサポートしています。 2 | -------------------------------------------------------------------------------- /metadata/ja-JP/images/phoneScreenshots/a: -------------------------------------------------------------------------------- 1 | 2 | -------------------------------------------------------------------------------- /metadata/ja-JP/images/phoneScreenshots/screenshot-for-you.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/zyrouge/symphony/d6ff56b54422c5db6151107be48140d6b954e860/metadata/ja-JP/images/phoneScreenshots/screenshot-for-you.png -------------------------------------------------------------------------------- /metadata/ja-JP/short_description.txt: -------------------------------------------------------------------------------- 1 | オフラインでの音楽再生がより簡単になりました 2 | -------------------------------------------------------------------------------- /metadata/ja-JP/title.txt: -------------------------------------------------------------------------------- 1 | Symphony 2 | -------------------------------------------------------------------------------- /metaphony/.gitignore: -------------------------------------------------------------------------------- 1 | /build -------------------------------------------------------------------------------- /metaphony/build.gradle.kts: -------------------------------------------------------------------------------- 1 | plugins { 2 | alias(libs.plugins.android.library) 3 | alias(libs.plugins.android.kotlin) 4 | } 5 | 6 | android { 7 | namespace = "me.zyrouge.symphony.metaphony" 8 | compileSdk = libs.versions.compile.sdk.get().toInt() 9 | 10 | defaultConfig { 11 | minSdk = libs.versions.min.sdk.get().toInt() 12 | 13 | testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner" 14 | consumerProguardFiles("consumer-rules.pro") 15 | externalNativeBuild { 16 | cmake { 17 | cppFlags("") 18 | } 19 | } 20 | } 21 | 22 | buildTypes { 23 | release { 24 | isMinifyEnabled = false 25 | proguardFiles( 26 | getDefaultProguardFile("proguard-android-optimize.txt"), 27 | "proguard-rules.pro" 28 | ) 29 | } 30 | create("nightly") { 31 | initWith(getByName("release")) 32 | } 33 | create("canary") { 34 | initWith(getByName("release")) 35 | } 36 | } 37 | 38 | externalNativeBuild { 39 | cmake { 40 | path("src/main/cpp/CMakeLists.txt") 41 | version = "3.22.1" 42 | } 43 | } 44 | 45 | compileOptions { 46 | sourceCompatibility = JavaVersion.VERSION_17 47 | targetCompatibility = JavaVersion.VERSION_17 48 | } 49 | 50 | kotlinOptions { 51 | jvmTarget = "17" 52 | freeCompilerArgs += "-opt-in=kotlin.RequiresOptIn" 53 | } 54 | 55 | testOptions { 56 | androidResources { 57 | noCompress.addAll(listOf("flac", "ogg", "mp3")) 58 | } 59 | } 60 | } 61 | 62 | dependencies { 63 | implementation(libs.core) 64 | implementation(libs.appcompat) 65 | implementation(libs.material) 66 | 67 | testImplementation(libs.junit) 68 | 69 | androidTestImplementation(libs.ext.junit) 70 | androidTestImplementation(libs.espresso.core) 71 | } -------------------------------------------------------------------------------- /metaphony/consumer-rules.pro: -------------------------------------------------------------------------------- 1 | -keep class me.zyrouge.symphony.metaphony.AudioMetadataParser { 2 | void putTag(java.lang.String, java.lang.String); 3 | void putPicture(java.lang.String, java.lang.String, byte[]); 4 | void putAudioProperty(java.lang.String, int); 5 | boolean readMetadata(java.lang.String, int); 6 | } -------------------------------------------------------------------------------- /metaphony/proguard-rules.pro: -------------------------------------------------------------------------------- 1 | -keep class me.zyrouge.symphony.metaphony.AudioMetadataParser { 2 | void putTag(java.lang.String, java.lang.String); 3 | void putPicture(java.lang.String, java.lang.String, byte[]); 4 | void putAudioProperty(java.lang.String, int); 5 | boolean readMetadata(java.lang.String, int); 6 | } -------------------------------------------------------------------------------- /metaphony/src/main/AndroidManifest.xml: -------------------------------------------------------------------------------- 1 | <?xml version="1.0" encoding="utf-8"?> 2 | <manifest xmlns:android="http://schemas.android.com/apk/res/android"> 3 | 4 | </manifest> -------------------------------------------------------------------------------- /metaphony/src/main/assets/audio-id3v2.3.mp3: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/zyrouge/symphony/d6ff56b54422c5db6151107be48140d6b954e860/metaphony/src/main/assets/audio-id3v2.3.mp3 -------------------------------------------------------------------------------- /metaphony/src/main/assets/audio-id3v2.4.mp3: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/zyrouge/symphony/d6ff56b54422c5db6151107be48140d6b954e860/metaphony/src/main/assets/audio-id3v2.4.mp3 -------------------------------------------------------------------------------- /metaphony/src/main/assets/audio.flac: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/zyrouge/symphony/d6ff56b54422c5db6151107be48140d6b954e860/metaphony/src/main/assets/audio.flac -------------------------------------------------------------------------------- /metaphony/src/main/assets/audio.mp3: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/zyrouge/symphony/d6ff56b54422c5db6151107be48140d6b954e860/metaphony/src/main/assets/audio.mp3 -------------------------------------------------------------------------------- /metaphony/src/main/cpp/CMakeLists.txt: -------------------------------------------------------------------------------- 1 | cmake_minimum_required(VERSION 3.22.1) 2 | 3 | project("metaphony") 4 | 5 | add_subdirectory(taglib) 6 | 7 | include_directories(taglib/taglib 8 | taglib/taglib/ape 9 | taglib/taglib/asf 10 | taglib/taglib/dsdiff 11 | taglib/taglib/dsf 12 | taglib/taglib/flac 13 | taglib/taglib/it 14 | taglib/taglib/mod 15 | taglib/taglib/mp4 16 | taglib/taglib/mpc 17 | taglib/taglib/mpeg 18 | taglib/taglib/mpeg/id3v1 19 | taglib/taglib/mpeg/id3v2 20 | taglib/taglib/mpeg/id3v2/frames 21 | taglib/taglib/ogg 22 | taglib/taglib/ogg/flac 23 | taglib/taglib/ogg/opus 24 | taglib/taglib/ogg/speex 25 | taglib/taglib/ogg/vorbis 26 | taglib/taglib/riff 27 | taglib/taglib/riff/aiff 28 | taglib/taglib/riff/wav 29 | taglib/taglib/s3m 30 | taglib/taglib/toolkit 31 | taglib/taglib/trueaudio 32 | taglib/taglib/wavpack 33 | taglib/taglib/xm) 34 | 35 | add_library(${CMAKE_PROJECT_NAME} SHARED 36 | AudioMetadataParser.cpp 37 | TagLibHelper.cpp) 38 | 39 | target_link_libraries(${CMAKE_PROJECT_NAME} 40 | android 41 | log 42 | tag) 43 | -------------------------------------------------------------------------------- /metaphony/src/main/cpp/TagLibHelper.h: -------------------------------------------------------------------------------- 1 | // 2 | // Created by Zyrouge on 26-Dec-24. 3 | // 4 | 5 | #ifndef SYMPHONY_TAGLIBHELPER_H 6 | #define SYMPHONY_TAGLIBHELPER_H 7 | 8 | #include "audioproperties.h" 9 | #include "tfile.h" 10 | 11 | namespace TagLibHelper { 12 | TagLib::File *detectParser( 13 | TagLib::FileName filename, 14 | TagLib::IOStream *stream, 15 | bool readAudioProperties, 16 | TagLib::AudioProperties::ReadStyle audioPropertiesStyle); 17 | 18 | TagLib::File *detectByExtension( 19 | TagLib::FileName filename, 20 | TagLib::IOStream *stream, 21 | bool readAudioProperties, 22 | TagLib::AudioProperties::ReadStyle audioPropertiesStyle); 23 | 24 | TagLib::File *detectByContent( 25 | TagLib::IOStream *stream, 26 | bool readAudioProperties, 27 | TagLib::AudioProperties::ReadStyle audioPropertiesStyle); 28 | } 29 | 30 | #endif //SYMPHONY_TAGLIBHELPER_H 31 | -------------------------------------------------------------------------------- /metaphony/src/main/java/me/zyrouge/symphony/metaphony/AudioMetadata.kt: -------------------------------------------------------------------------------- 1 | package me.zyrouge.symphony.metaphony 2 | 3 | import java.time.LocalDate 4 | 5 | data class AudioMetadata( 6 | val title: String?, 7 | val album: String?, 8 | val artists: Set<String>, 9 | val albumArtists: Set<String>, 10 | val composers: Set<String>, 11 | val genres: Set<String>, 12 | val discNumber: Int?, 13 | val discTotal: Int?, 14 | val trackNumber: Int?, 15 | val trackTotal: Int?, 16 | val date: LocalDate?, 17 | val lyrics: String?, 18 | val encoding: String?, 19 | val bitrate: Int?, 20 | val lengthInSeconds: Int?, 21 | val sampleRate: Int?, 22 | val channels: Int?, 23 | val pictures: List<Picture>, 24 | ) { 25 | data class Picture(val pictureType: String, val mimeType: String, val data: ByteArray) 26 | } 27 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@zyrouge/symphony-cli", 3 | "version": "0.0.0", 4 | "private": true, 5 | "scripts": { 6 | "i18n:build": "phrasey build -p ./.phrasey/config.toml -f toml", 7 | "i18n:summary": "tsx ./cli/i18n/summary.ts", 8 | "version:bump": "tsx ./cli/version/bump.ts", 9 | "version:print": "tsx ./cli/version/print.ts", 10 | "version:print-nightly": "tsx ./cli/version/print-nightly.ts", 11 | "version:print-canary": "tsx ./cli/version/print-canary.ts", 12 | "git:tag-exists": "tsx ./cli/git/tag-exists.ts", 13 | "git:tag-exists-yn": "tsx ./cli/git/tag-exists-yn.ts", 14 | "git:latest-tag": "tsx ./cli/git/latest-tag.ts", 15 | "git:diff-files-yn": "tsx ./cli/git/diff-files-yn.ts", 16 | "android:move-outputs": "tsx ./cli/android/move-outputs.ts", 17 | "changelogs:fastlane-character-limit": "tsx ./cli/changelogs/fastlane-character-limit.ts", 18 | "prebuild": "npm run i18n:build", 19 | "postbuild": "npm run android:move-outputs", 20 | "release": "gh workflow run release" 21 | }, 22 | "repository": { 23 | "type": "git", 24 | "url": "git+https://github.com/zyrouge/symphony.git" 25 | }, 26 | "author": "Zyrouge", 27 | "license": "AGPL-3.0", 28 | "devDependencies": { 29 | "@types/archiver": "^6.0.3", 30 | "@types/fs-extra": "^11.0.4", 31 | "@types/node": "^22.10.2", 32 | "@zyrouge/phrasey-json": "^1.0.3", 33 | "@zyrouge/phrasey-locales-builder": "^1.1.10", 34 | "@zyrouge/phrasey-toml": "^1.0.3", 35 | "@zyrouge/zemver": "^1.0.0", 36 | "archiver": "^7.0.1", 37 | "fs-extra": "^11.2.0", 38 | "phrasey": "^2.0.27", 39 | "picocolors": "^1.1.1", 40 | "prettier": "^3.4.2", 41 | "prettier-plugin-toml": "^2.0.1", 42 | "tsx": "^4.19.2", 43 | "typescript": "^5.7.2" 44 | } 45 | } 46 | -------------------------------------------------------------------------------- /secrets/secrets.txt: -------------------------------------------------------------------------------- 1 | SIGNING_KEYSTORE_FILE=./secrets/signing_key.jks 2 | SIGNING_KEYSTORE_PASSWORD=Symphony1234! 3 | SIGNING_KEY_PASSWORD=Symphony1234! 4 | SIGNING_KEY_ALIAS=key0 5 | -------------------------------------------------------------------------------- /secrets/signing_key.jks: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/zyrouge/symphony/d6ff56b54422c5db6151107be48140d6b954e860/secrets/signing_key.jks -------------------------------------------------------------------------------- /settings.gradle.kts: -------------------------------------------------------------------------------- 1 | pluginManagement { 2 | repositories { 3 | gradlePluginPortal() 4 | google() 5 | mavenCentral() 6 | } 7 | } 8 | 9 | dependencyResolutionManagement { 10 | repositoriesMode.set(RepositoriesMode.FAIL_ON_PROJECT_REPOS) 11 | repositories { 12 | google() 13 | mavenCentral() 14 | } 15 | } 16 | 17 | rootProject.name = "Symphony" 18 | 19 | include(":app") 20 | include(":metaphony") 21 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "es2016", 4 | "module": "commonjs", 5 | "noEmit": true, 6 | "esModuleInterop": true, 7 | "forceConsistentCasingInFileNames": true, 8 | "strict": true, 9 | "noImplicitAny": true, 10 | "strictNullChecks": true, 11 | "noImplicitThis": true, 12 | "useUnknownInCatchVariables": true, 13 | "alwaysStrict": true, 14 | "noUnusedLocals": true, 15 | "noFallthroughCasesInSwitch": true, 16 | "noUncheckedIndexedAccess": true, 17 | "skipLibCheck": true, 18 | "resolveJsonModule": true, 19 | "checkJs": true 20 | } 21 | } 22 | --------------------------------------------------------------------------------