├── .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] "
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 |
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 |
2 |
3 |
4 |
5 |
--------------------------------------------------------------------------------
/.idea/dictionaries/zyrouge.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
--------------------------------------------------------------------------------
/.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 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
11 |
18 |
19 |
20 | false
21 | true
22 | false
23 | true
24 |
25 |
26 |
--------------------------------------------------------------------------------
/.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 |
2 | Symphony (Canary)
3 |
--------------------------------------------------------------------------------
/app/src/debug/res/values/strings.xml:
--------------------------------------------------------------------------------
1 |
2 | Symphony (Debug)
3 |
--------------------------------------------------------------------------------
/app/src/main/AndroidManifest.xml:
--------------------------------------------------------------------------------
1 |
2 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
20 |
27 |
28 |
29 |
30 |
31 |
32 |
35 |
36 |
37 |
38 |
42 |
43 |
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,
13 | val granted: List,
14 | val denied: List,
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 {
29 | val required = mutableListOf()
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()
39 | val denied = mutableListOf()
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 = 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) = 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
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
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): 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,
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()
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()
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()
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,
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) {
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,
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 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) -> Unit
4 | typealias EventUnsubscribeFn = () -> Unit
5 |
6 | class Eventer {
7 | private val subscribers = mutableListOf>()
8 |
9 | fun subscribe(subscriber: EventSubscriber): EventUnsubscribeFn {
10 | subscribers.add(subscriber)
11 | return { unsubscribe(subscriber) }
12 | }
13 |
14 | fun unsubscribe(subscriber: EventSubscriber) {
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): 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(
22 | val match: FuzzySearchComparator.(T) -> Int?,
23 | val weight: Int = 1,
24 | )
25 |
26 | data class FuzzyResultEntity(
27 | val score: Int,
28 | val entity: T,
29 | )
30 |
31 | class FuzzySearcher(val options: List>) {
32 | fun search(
33 | terms: String,
34 | entities: List,
35 | maxLength: Int = -1,
36 | ): List> {
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 {
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 List.subListNonStrict(length: Int, start: Int = 0) =
9 | subList(start, min(start + length, size))
10 |
11 | fun List.randomSubList(length: Int): List {
12 | val mut = toMutableList()
13 | val out = mutableListOf()
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 List.mutate(fn: MutableList.() -> Unit): List {
23 | val out = toMutableList()
24 | fn.invoke(out)
25 | return out
26 | }
27 |
28 | fun concurrentListOf(): MutableList = CopyOnWriteArrayList(mutableListOf())
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) = range.endInclusive - range.start
5 |
6 | fun calculateRatioFromValue(value: Float, range: ClosedFloatingPointRange) =
7 | (value - range.start) / calculateGap(range)
8 |
9 | fun calculateValueFromRatio(ratio: Float, range: ClosedFloatingPointRange) =
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) = Json.encodeToString(value)
18 |
19 | @TypeConverter
20 | fun deserializeStringSet(value: String) = Json.decodeFromString>(value)
21 |
22 | @TypeConverter
23 | fun serializeStringList(value: List) = Json.encodeToString(value)
24 |
25 | @TypeConverter
26 | fun deserializeStringList(value: String) = Json.decodeFromString>(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 runIfOrDefault(value: Boolean, defaultValue: T, fn: () -> T) = when {
4 | value -> fn()
5 | else -> defaultValue
6 | }
7 |
8 | fun 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.joinToStringIfNotEmpty() = if (isNotEmpty()) joinToString() else null
6 | fun Set.joinToStringIfNotEmpty(sensitive: Boolean) =
7 | joinToStringIfNotEmpty()?.withCase(sensitive)
8 |
9 | typealias ConcurrentSet = ConcurrentHashMap.KeySetView
10 |
11 | fun concurrentSetOf(vararg elements: T): ConcurrentSet =
12 | ConcurrentHashMap.newKeySet().apply { addAll(elements) }
13 |
14 | fun concurrentSetOf(elements: Collection): ConcurrentSet =
15 | ConcurrentHashMap.newKeySet().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 = 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) {
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()) { prev, curr ->
25 | prev + curr.split("/", "\\")
26 | }
27 |
28 | private fun n(parts: List): List {
29 | val normalized = mutableListOf()
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, by: SortBy, reverse: Boolean): List {
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>) {
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 |
7 |
11 |
12 |
14 |
15 |
17 |
20 |
22 |
23 |
29 |
30 |
31 |
32 |
33 |
34 |
35 |
36 |
37 |
38 |
--------------------------------------------------------------------------------
/app/src/main/res/drawable/ic_launcher_monochrome.xml:
--------------------------------------------------------------------------------
1 |
6 |
10 |
13 |
16 |
19 |
22 |
25 |
28 |
29 |
30 |
--------------------------------------------------------------------------------
/app/src/main/res/drawable/material_icon_close.xml:
--------------------------------------------------------------------------------
1 |
6 |
11 |
14 |
15 |
16 |
--------------------------------------------------------------------------------
/app/src/main/res/drawable/material_icon_music_note.xml:
--------------------------------------------------------------------------------
1 |
6 |
9 |
10 |
--------------------------------------------------------------------------------
/app/src/main/res/drawable/material_icon_pause.xml:
--------------------------------------------------------------------------------
1 |
6 |
11 |
14 |
15 |
16 |
--------------------------------------------------------------------------------
/app/src/main/res/drawable/material_icon_play.xml:
--------------------------------------------------------------------------------
1 |
6 |
9 |
10 |
--------------------------------------------------------------------------------
/app/src/main/res/drawable/material_icon_skip_next.xml:
--------------------------------------------------------------------------------
1 |
6 |
11 |
14 |
15 |
16 |
--------------------------------------------------------------------------------
/app/src/main/res/drawable/material_icon_skip_previous.xml:
--------------------------------------------------------------------------------
1 |
6 |
11 |
14 |
15 |
16 |
--------------------------------------------------------------------------------
/app/src/main/res/drawable/material_icon_stop.xml:
--------------------------------------------------------------------------------
1 |
6 |
11 |
14 |
15 |
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 |
2 |
3 |
4 |
5 |
6 |
--------------------------------------------------------------------------------
/app/src/main/res/mipmap-anydpi/ic_launcher_round.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
--------------------------------------------------------------------------------
/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 |
2 |
3 | #121212
4 |
--------------------------------------------------------------------------------
/app/src/main/res/values/splash.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
11 |
12 |
--------------------------------------------------------------------------------
/app/src/main/res/values/strings.xml:
--------------------------------------------------------------------------------
1 |
2 | Symphony
3 |
--------------------------------------------------------------------------------
/app/src/main/res/values/themes.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
7 |
--------------------------------------------------------------------------------
/app/src/main/res/xml/backup_rules.xml:
--------------------------------------------------------------------------------
1 |
8 |
9 |
13 |
--------------------------------------------------------------------------------
/app/src/main/res/xml/data_extraction_rules.xml:
--------------------------------------------------------------------------------
1 |
6 |
7 |
8 |
12 |
13 |
19 |
--------------------------------------------------------------------------------
/app/src/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: (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 |
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 |
2 |
3 |
4 |
--------------------------------------------------------------------------------
/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,
9 | val albumArtists: Set,
10 | val composers: Set,
11 | val genres: Set,
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,
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 |
--------------------------------------------------------------------------------