start kotlin/jetpack compose rewrite
replace the flutter project with a gradle/compose android app: - nix flake dev shell (jdk21, android sdk 35, build-tools 37, gradle) - material3 app with a placeholder screen - roborazzi/robolectric screenshot tests with one golden - carry over the existing launcher/notification assets
@@ -1,4 +0,0 @@
|
||||
TEST_SERVER_NAME=Subsonic Demo
|
||||
TEST_SERVER_URL=http://demo.subsonic.org
|
||||
TEST_SERVER_USERNAME=guest
|
||||
TEST_SERVER_PASSWORD=guest
|
||||
@@ -1,4 +0,0 @@
|
||||
{
|
||||
"flutterSdkVersion": "3.7.11",
|
||||
"flavors": {}
|
||||
}
|
||||
@@ -1,78 +0,0 @@
|
||||
name: build-flutter-release
|
||||
|
||||
on:
|
||||
push:
|
||||
tags: ['v*']
|
||||
|
||||
jobs:
|
||||
build-apk:
|
||||
runs-on: ubuntu-22.04
|
||||
steps:
|
||||
- uses: actions/checkout@v3
|
||||
|
||||
- name: Get subtracks version
|
||||
run: echo "SUBTRACKS_VERSION=$(yq '.version' pubspec.yaml | cut -d '+' -f 1)" >> $GITHUB_ENV
|
||||
|
||||
- uses: subosito/flutter-action@v2
|
||||
with:
|
||||
flutter-version: '3.7.11'
|
||||
channel: 'stable'
|
||||
cache: true
|
||||
|
||||
- run: cp .env.example .env
|
||||
- run: flutter pub get
|
||||
|
||||
- name: Retrieve subtracks.keystore
|
||||
env:
|
||||
SUBTRACKS_KEYSTORE_BASE64: ${{ secrets.SUBTRACKS_KEYSTORE_BASE64 }}
|
||||
run: |
|
||||
echo "$SUBTRACKS_KEYSTORE_BASE64" | base64 --decode > android/subtracks.keystore
|
||||
- name: Retrieve key.properties
|
||||
env:
|
||||
SUBTRACKS_KEYPROPERTIES_BASE64: ${{ secrets.SUBTRACKS_KEYPROPERTIES_BASE64 }}
|
||||
run: |
|
||||
echo "$SUBTRACKS_KEYPROPERTIES_BASE64" | base64 --decode > android/key.properties
|
||||
|
||||
- run: flutter build apk -PsignRelease
|
||||
|
||||
- run: mv build/app/outputs/flutter-apk/app-release.apk subtracks-${SUBTRACKS_VERSION}.apk
|
||||
- uses: actions/upload-artifact@v3
|
||||
with:
|
||||
name: subtracks-release-apk
|
||||
path: subtracks-*.apk
|
||||
|
||||
build-aab:
|
||||
runs-on: ubuntu-22.04
|
||||
steps:
|
||||
- uses: actions/checkout@v3
|
||||
|
||||
- name: Get subtracks version
|
||||
run: echo "SUBTRACKS_VERSION=$(yq '.version' pubspec.yaml)" >> $GITHUB_ENV
|
||||
|
||||
- uses: subosito/flutter-action@v2
|
||||
with:
|
||||
flutter-version: '3.7.11'
|
||||
channel: 'stable'
|
||||
cache: true
|
||||
|
||||
- run: cp .env.example .env
|
||||
- run: flutter pub get
|
||||
|
||||
- name: Retrieve subtracks.keystore
|
||||
env:
|
||||
SUBTRACKS_KEYSTORE_BASE64: ${{ secrets.SUBTRACKS_KEYSTORE_BASE64 }}
|
||||
run: |
|
||||
echo "$SUBTRACKS_KEYSTORE_BASE64" | base64 --decode > android/subtracks.keystore
|
||||
- name: Retrieve key.properties
|
||||
env:
|
||||
SUBTRACKS_KEYPROPERTIES_BASE64: ${{ secrets.SUBTRACKS_KEYPROPERTIES_BASE64 }}
|
||||
run: |
|
||||
echo "$SUBTRACKS_KEYPROPERTIES_BASE64" | base64 --decode > android/key.properties
|
||||
|
||||
- run: flutter build appbundle -PsignRelease
|
||||
|
||||
- run: mv build/app/outputs/bundle/release/app-release.aab subtracks-${SUBTRACKS_VERSION}.aab
|
||||
- uses: actions/upload-artifact@v3
|
||||
with:
|
||||
name: subtracks-release-aab
|
||||
path: subtracks-*.aab
|
||||
@@ -1,30 +0,0 @@
|
||||
name: build-flutter
|
||||
|
||||
on:
|
||||
push:
|
||||
branches: ['main']
|
||||
pull_request:
|
||||
branches: ['main']
|
||||
|
||||
jobs:
|
||||
build-apk:
|
||||
runs-on: ubuntu-22.04
|
||||
steps:
|
||||
- uses: actions/checkout@v3
|
||||
|
||||
- uses: subosito/flutter-action@v2
|
||||
with:
|
||||
flutter-version: '3.7.11'
|
||||
channel: 'stable'
|
||||
cache: true
|
||||
|
||||
- run: cp .env.example .env
|
||||
- run: flutter pub get
|
||||
- run: flutter analyze
|
||||
- run: flutter build apk
|
||||
|
||||
- run: mv build/app/outputs/flutter-apk/app-release.apk subtracks-$(git rev-parse --short HEAD).apk
|
||||
- uses: actions/upload-artifact@v3
|
||||
with:
|
||||
name: subtracks-testing
|
||||
path: subtracks-*.apk
|
||||
@@ -1,49 +1,17 @@
|
||||
# Miscellaneous
|
||||
*.class
|
||||
*.log
|
||||
*.pyc
|
||||
*.swp
|
||||
.DS_Store
|
||||
.atom/
|
||||
.buildlog/
|
||||
.history
|
||||
.svn/
|
||||
migrate_working_dir/
|
||||
.gradle/
|
||||
build/
|
||||
local.properties
|
||||
|
||||
*.apk
|
||||
*.aab
|
||||
captures/
|
||||
.cxx/
|
||||
|
||||
# IntelliJ related
|
||||
*.iml
|
||||
*.ipr
|
||||
*.iws
|
||||
.idea/
|
||||
*.iml
|
||||
.vscode/
|
||||
|
||||
# The .vscode folder contains launch configuration and tasks you configure in
|
||||
# VS Code which you may wish to be included in version control, so this line
|
||||
# is commented out by default.
|
||||
#.vscode/
|
||||
.kotlin/
|
||||
.preview/
|
||||
|
||||
# Flutter/Dart/Pub related
|
||||
**/doc/api/
|
||||
**/ios/Flutter/.last_build_id
|
||||
.dart_tool/
|
||||
.flutter-plugins
|
||||
.flutter-plugins-dependencies
|
||||
.packages
|
||||
.pub-cache/
|
||||
.pub/
|
||||
/build/
|
||||
|
||||
# Symbolication related
|
||||
app.*.symbols
|
||||
|
||||
# Obfuscation related
|
||||
app.*.map.json
|
||||
|
||||
# Android Studio will place build artifacts here
|
||||
/android/app/debug
|
||||
/android/app/profile
|
||||
/android/app/release
|
||||
|
||||
/.env
|
||||
*.sqlite*
|
||||
/.fvm/flutter_sdk
|
||||
*.keystore
|
||||
.DS_Store
|
||||
|
||||
@@ -1,30 +0,0 @@
|
||||
# This file tracks properties of this Flutter project.
|
||||
# Used by Flutter tool to assess capabilities and perform upgrades etc.
|
||||
#
|
||||
# This file should be version controlled.
|
||||
|
||||
version:
|
||||
revision: 9944297138845a94256f1cf37beb88ff9a8e811a
|
||||
channel: stable
|
||||
|
||||
project_type: app
|
||||
|
||||
# Tracks metadata for the flutter migrate command
|
||||
migration:
|
||||
platforms:
|
||||
- platform: root
|
||||
create_revision: 9944297138845a94256f1cf37beb88ff9a8e811a
|
||||
base_revision: 9944297138845a94256f1cf37beb88ff9a8e811a
|
||||
- platform: android
|
||||
create_revision: 9944297138845a94256f1cf37beb88ff9a8e811a
|
||||
base_revision: 9944297138845a94256f1cf37beb88ff9a8e811a
|
||||
|
||||
# User provided section
|
||||
|
||||
# List of Local paths (relative to this file) that should be
|
||||
# ignored by the migrate tool.
|
||||
#
|
||||
# Files that are not part of the templates will be ignored by default.
|
||||
unmanaged_files:
|
||||
- 'lib/main.dart'
|
||||
- 'ios/Runner.xcodeproj/project.pbxproj'
|
||||
@@ -1,500 +0,0 @@
|
||||
{
|
||||
"ar": [
|
||||
"actionsCancel",
|
||||
"actionsDelete",
|
||||
"actionsDownload",
|
||||
"actionsDownloadCancel",
|
||||
"actionsDownloadDelete",
|
||||
"actionsOk",
|
||||
"controlsShuffle",
|
||||
"resourcesAlbumCount",
|
||||
"resourcesArtistCount",
|
||||
"resourcesFilterAlbum",
|
||||
"resourcesFilterArtist",
|
||||
"resourcesFilterOwner",
|
||||
"resourcesFilterYear",
|
||||
"resourcesPlaylistCount",
|
||||
"resourcesSongCount",
|
||||
"resourcesSongListDeleteAllContent",
|
||||
"resourcesSongListDeleteAllTitle",
|
||||
"resourcesSortByAlbum",
|
||||
"resourcesSortByAlbumCount",
|
||||
"resourcesSortByTitle",
|
||||
"resourcesSortByUpdated",
|
||||
"settingsAboutActionsSupport",
|
||||
"settingsAboutShareLogs",
|
||||
"settingsAboutChooseLog",
|
||||
"settingsNetworkOptionsOfflineMode",
|
||||
"settingsNetworkOptionsOfflineModeOff",
|
||||
"settingsNetworkOptionsOfflineModeOn",
|
||||
"settingsNetworkOptionsStreamFormat",
|
||||
"settingsNetworkOptionsStreamFormatServerDefault",
|
||||
"settingsServersFieldsName"
|
||||
],
|
||||
|
||||
"ca": [
|
||||
"actionsCancel",
|
||||
"actionsDelete",
|
||||
"actionsDownload",
|
||||
"actionsDownloadCancel",
|
||||
"actionsDownloadDelete",
|
||||
"actionsOk",
|
||||
"controlsShuffle",
|
||||
"resourcesAlbumCount",
|
||||
"resourcesArtistCount",
|
||||
"resourcesFilterAlbum",
|
||||
"resourcesFilterArtist",
|
||||
"resourcesFilterOwner",
|
||||
"resourcesFilterYear",
|
||||
"resourcesPlaylistCount",
|
||||
"resourcesSongCount",
|
||||
"resourcesSongListDeleteAllContent",
|
||||
"resourcesSongListDeleteAllTitle",
|
||||
"resourcesSortByAlbum",
|
||||
"resourcesSortByAlbumCount",
|
||||
"resourcesSortByTitle",
|
||||
"resourcesSortByUpdated",
|
||||
"settingsAboutActionsSupport",
|
||||
"settingsAboutShareLogs",
|
||||
"settingsAboutChooseLog",
|
||||
"settingsNetworkOptionsOfflineMode",
|
||||
"settingsNetworkOptionsOfflineModeOff",
|
||||
"settingsNetworkOptionsOfflineModeOn",
|
||||
"settingsNetworkOptionsStreamFormat",
|
||||
"settingsNetworkOptionsStreamFormatServerDefault",
|
||||
"settingsServersFieldsName"
|
||||
],
|
||||
|
||||
"cs": [
|
||||
"resourcesAlbumCount",
|
||||
"resourcesArtistCount",
|
||||
"resourcesPlaylistCount",
|
||||
"resourcesSongCount",
|
||||
"settingsAboutShareLogs",
|
||||
"settingsAboutChooseLog",
|
||||
"settingsMusicName",
|
||||
"settingsMusicOptionsScrobbleDescriptionOff",
|
||||
"settingsMusicOptionsScrobbleDescriptionOn",
|
||||
"settingsMusicOptionsScrobbleTitle",
|
||||
"settingsNetworkOptionsMaxBufferTitle",
|
||||
"settingsNetworkOptionsMinBufferTitle",
|
||||
"settingsNetworkOptionsOfflineMode",
|
||||
"settingsNetworkOptionsOfflineModeOff",
|
||||
"settingsNetworkOptionsOfflineModeOn"
|
||||
],
|
||||
|
||||
"da": [
|
||||
"actionsCancel",
|
||||
"actionsDelete",
|
||||
"actionsDownload",
|
||||
"actionsDownloadCancel",
|
||||
"actionsDownloadDelete",
|
||||
"actionsOk",
|
||||
"actionsStar",
|
||||
"actionsUnstar",
|
||||
"controlsShuffle",
|
||||
"resourcesAlbumCount",
|
||||
"resourcesArtistCount",
|
||||
"resourcesFilterAlbum",
|
||||
"resourcesFilterArtist",
|
||||
"resourcesFilterOwner",
|
||||
"resourcesFilterStarred",
|
||||
"resourcesFilterYear",
|
||||
"resourcesPlaylistCount",
|
||||
"resourcesSongCount",
|
||||
"resourcesSongListDeleteAllContent",
|
||||
"resourcesSongListDeleteAllTitle",
|
||||
"resourcesSortByAdded",
|
||||
"resourcesSortByAlbum",
|
||||
"resourcesSortByAlbumCount",
|
||||
"resourcesSortByFrequentlyPlayed",
|
||||
"resourcesSortByRecentlyPlayed",
|
||||
"resourcesSortByTitle",
|
||||
"resourcesSortByUpdated",
|
||||
"settingsAboutActionsSupport",
|
||||
"settingsAboutShareLogs",
|
||||
"settingsAboutChooseLog",
|
||||
"settingsMusicOptionsScrobbleDescriptionOff",
|
||||
"settingsNetworkOptionsOfflineMode",
|
||||
"settingsNetworkOptionsOfflineModeOff",
|
||||
"settingsNetworkOptionsOfflineModeOn",
|
||||
"settingsNetworkOptionsStreamFormat",
|
||||
"settingsNetworkOptionsStreamFormatServerDefault",
|
||||
"settingsServersFieldsName",
|
||||
"settingsServersOptionsForcePlaintextPasswordDescriptionOff",
|
||||
"settingsServersOptionsForcePlaintextPasswordDescriptionOn",
|
||||
"settingsServersOptionsForcePlaintextPasswordTitle"
|
||||
],
|
||||
|
||||
"de": [
|
||||
"settingsAboutShareLogs",
|
||||
"settingsAboutChooseLog"
|
||||
],
|
||||
|
||||
"es": [
|
||||
"resourcesAlbumCount",
|
||||
"resourcesArtistCount",
|
||||
"resourcesFilterAlbum",
|
||||
"resourcesFilterArtist",
|
||||
"resourcesFilterOwner",
|
||||
"resourcesFilterYear",
|
||||
"resourcesPlaylistCount",
|
||||
"resourcesSongCount",
|
||||
"resourcesSongListDeleteAllContent",
|
||||
"resourcesSongListDeleteAllTitle",
|
||||
"resourcesSortByAlbum",
|
||||
"resourcesSortByAlbumCount",
|
||||
"resourcesSortByTitle",
|
||||
"resourcesSortByUpdated",
|
||||
"settingsAboutActionsSupport",
|
||||
"settingsAboutShareLogs",
|
||||
"settingsAboutChooseLog",
|
||||
"settingsNetworkOptionsOfflineMode",
|
||||
"settingsNetworkOptionsOfflineModeOff",
|
||||
"settingsNetworkOptionsOfflineModeOn",
|
||||
"settingsNetworkOptionsStreamFormat",
|
||||
"settingsNetworkOptionsStreamFormatServerDefault",
|
||||
"settingsServersFieldsName"
|
||||
],
|
||||
|
||||
"fr": [
|
||||
"actionsCancel",
|
||||
"actionsDelete",
|
||||
"actionsDownload",
|
||||
"actionsDownloadCancel",
|
||||
"actionsDownloadDelete",
|
||||
"actionsOk",
|
||||
"controlsShuffle",
|
||||
"resourcesAlbumCount",
|
||||
"resourcesArtistCount",
|
||||
"resourcesFilterAlbum",
|
||||
"resourcesFilterArtist",
|
||||
"resourcesFilterOwner",
|
||||
"resourcesFilterYear",
|
||||
"resourcesPlaylistCount",
|
||||
"resourcesSongCount",
|
||||
"resourcesSongListDeleteAllContent",
|
||||
"resourcesSongListDeleteAllTitle",
|
||||
"resourcesSortByAlbum",
|
||||
"resourcesSortByAlbumCount",
|
||||
"resourcesSortByTitle",
|
||||
"resourcesSortByUpdated",
|
||||
"settingsAboutActionsSupport",
|
||||
"settingsAboutShareLogs",
|
||||
"settingsAboutChooseLog",
|
||||
"settingsNetworkOptionsOfflineMode",
|
||||
"settingsNetworkOptionsOfflineModeOff",
|
||||
"settingsNetworkOptionsOfflineModeOn",
|
||||
"settingsNetworkOptionsStreamFormat",
|
||||
"settingsNetworkOptionsStreamFormatServerDefault",
|
||||
"settingsServersFieldsName"
|
||||
],
|
||||
|
||||
"it": [
|
||||
"actionsCancel",
|
||||
"actionsDelete",
|
||||
"actionsDownload",
|
||||
"actionsDownloadCancel",
|
||||
"actionsDownloadDelete",
|
||||
"actionsOk",
|
||||
"controlsShuffle",
|
||||
"resourcesAlbumCount",
|
||||
"resourcesArtistCount",
|
||||
"resourcesFilterAlbum",
|
||||
"resourcesFilterArtist",
|
||||
"resourcesFilterOwner",
|
||||
"resourcesFilterYear",
|
||||
"resourcesPlaylistCount",
|
||||
"resourcesSongCount",
|
||||
"resourcesSongListDeleteAllContent",
|
||||
"resourcesSongListDeleteAllTitle",
|
||||
"resourcesSortByAlbum",
|
||||
"resourcesSortByAlbumCount",
|
||||
"resourcesSortByTitle",
|
||||
"resourcesSortByUpdated",
|
||||
"settingsAboutActionsSupport",
|
||||
"settingsAboutShareLogs",
|
||||
"settingsAboutChooseLog",
|
||||
"settingsNetworkOptionsOfflineMode",
|
||||
"settingsNetworkOptionsOfflineModeOff",
|
||||
"settingsNetworkOptionsOfflineModeOn",
|
||||
"settingsNetworkOptionsStreamFormat",
|
||||
"settingsNetworkOptionsStreamFormatServerDefault",
|
||||
"settingsServersFieldsName"
|
||||
],
|
||||
|
||||
"ja": [
|
||||
"actionsCancel",
|
||||
"actionsDelete",
|
||||
"actionsDownload",
|
||||
"actionsDownloadCancel",
|
||||
"actionsDownloadDelete",
|
||||
"actionsOk",
|
||||
"actionsStar",
|
||||
"actionsUnstar",
|
||||
"controlsShuffle",
|
||||
"messagesNothingHere",
|
||||
"resourcesAlbumActionsPlay",
|
||||
"resourcesAlbumActionsView",
|
||||
"resourcesAlbumCount",
|
||||
"resourcesAlbumListsSort",
|
||||
"resourcesArtistActionsView",
|
||||
"resourcesArtistCount",
|
||||
"resourcesArtistListsSort",
|
||||
"resourcesFilterAlbum",
|
||||
"resourcesFilterArtist",
|
||||
"resourcesFilterGenre",
|
||||
"resourcesFilterOwner",
|
||||
"resourcesFilterYear",
|
||||
"resourcesPlaylistActionsPlay",
|
||||
"resourcesPlaylistCount",
|
||||
"resourcesQueueName",
|
||||
"resourcesSongCount",
|
||||
"resourcesSongListDeleteAllContent",
|
||||
"resourcesSongListDeleteAllTitle",
|
||||
"resourcesSortByAdded",
|
||||
"resourcesSortByAlbum",
|
||||
"resourcesSortByAlbumCount",
|
||||
"resourcesSortByArtist",
|
||||
"resourcesSortByName",
|
||||
"resourcesSortByTitle",
|
||||
"resourcesSortByUpdated",
|
||||
"resourcesSortByYear",
|
||||
"searchHeaderTitle",
|
||||
"searchMoreResults",
|
||||
"searchNowPlayingContext",
|
||||
"settingsAboutActionsLicenses",
|
||||
"settingsAboutActionsSupport",
|
||||
"settingsAboutName",
|
||||
"settingsAboutShareLogs",
|
||||
"settingsAboutChooseLog",
|
||||
"settingsAboutVersion",
|
||||
"settingsMusicOptionsScrobbleDescriptionOff",
|
||||
"settingsMusicOptionsScrobbleDescriptionOn",
|
||||
"settingsMusicOptionsScrobbleTitle",
|
||||
"settingsNetworkOptionsMaxBitrateMobileTitle",
|
||||
"settingsNetworkOptionsMaxBitrateWifiTitle",
|
||||
"settingsNetworkOptionsMaxBufferTitle",
|
||||
"settingsNetworkOptionsMinBufferTitle",
|
||||
"settingsNetworkOptionsOfflineMode",
|
||||
"settingsNetworkOptionsOfflineModeOff",
|
||||
"settingsNetworkOptionsOfflineModeOn",
|
||||
"settingsNetworkOptionsStreamFormat",
|
||||
"settingsNetworkOptionsStreamFormatServerDefault",
|
||||
"settingsNetworkValuesKbps",
|
||||
"settingsNetworkValuesSeconds",
|
||||
"settingsNetworkValuesUnlimitedKbps",
|
||||
"settingsResetActionsClearImageCache",
|
||||
"settingsServersActionsAdd",
|
||||
"settingsServersActionsDelete",
|
||||
"settingsServersActionsEdit",
|
||||
"settingsServersActionsSave",
|
||||
"settingsServersActionsTestConnection",
|
||||
"settingsServersFieldsAddress",
|
||||
"settingsServersFieldsName",
|
||||
"settingsServersFieldsPassword",
|
||||
"settingsServersFieldsUsername",
|
||||
"settingsServersMessagesConnectionFailed",
|
||||
"settingsServersMessagesConnectionOk",
|
||||
"settingsServersOptionsForcePlaintextPasswordDescriptionOff",
|
||||
"settingsServersOptionsForcePlaintextPasswordDescriptionOn",
|
||||
"settingsServersOptionsForcePlaintextPasswordTitle"
|
||||
],
|
||||
|
||||
"nb": [
|
||||
"actionsCancel",
|
||||
"actionsDelete",
|
||||
"actionsDownload",
|
||||
"actionsDownloadCancel",
|
||||
"actionsDownloadDelete",
|
||||
"actionsOk",
|
||||
"controlsShuffle",
|
||||
"resourcesAlbumCount",
|
||||
"resourcesArtistCount",
|
||||
"resourcesFilterAlbum",
|
||||
"resourcesFilterArtist",
|
||||
"resourcesFilterOwner",
|
||||
"resourcesFilterYear",
|
||||
"resourcesPlaylistCount",
|
||||
"resourcesSongCount",
|
||||
"resourcesSongListDeleteAllContent",
|
||||
"resourcesSongListDeleteAllTitle",
|
||||
"resourcesSortByAlbum",
|
||||
"resourcesSortByAlbumCount",
|
||||
"resourcesSortByTitle",
|
||||
"resourcesSortByUpdated",
|
||||
"settingsAboutActionsSupport",
|
||||
"settingsAboutShareLogs",
|
||||
"settingsAboutChooseLog",
|
||||
"settingsNetworkOptionsOfflineMode",
|
||||
"settingsNetworkOptionsOfflineModeOff",
|
||||
"settingsNetworkOptionsOfflineModeOn",
|
||||
"settingsNetworkOptionsStreamFormat",
|
||||
"settingsNetworkOptionsStreamFormatServerDefault",
|
||||
"settingsServersFieldsName"
|
||||
],
|
||||
|
||||
"pa": [
|
||||
"actionsCancel",
|
||||
"actionsDelete",
|
||||
"actionsDownload",
|
||||
"actionsDownloadCancel",
|
||||
"actionsDownloadDelete",
|
||||
"actionsOk",
|
||||
"controlsShuffle",
|
||||
"resourcesAlbumCount",
|
||||
"resourcesArtistCount",
|
||||
"resourcesFilterAlbum",
|
||||
"resourcesFilterArtist",
|
||||
"resourcesFilterOwner",
|
||||
"resourcesFilterYear",
|
||||
"resourcesPlaylistCount",
|
||||
"resourcesSongCount",
|
||||
"resourcesSongListDeleteAllContent",
|
||||
"resourcesSongListDeleteAllTitle",
|
||||
"resourcesSortByAlbum",
|
||||
"resourcesSortByAlbumCount",
|
||||
"resourcesSortByTitle",
|
||||
"resourcesSortByUpdated",
|
||||
"settingsAboutActionsSupport",
|
||||
"settingsAboutShareLogs",
|
||||
"settingsAboutChooseLog",
|
||||
"settingsNetworkOptionsOfflineMode",
|
||||
"settingsNetworkOptionsOfflineModeOff",
|
||||
"settingsNetworkOptionsOfflineModeOn",
|
||||
"settingsNetworkOptionsStreamFormat",
|
||||
"settingsNetworkOptionsStreamFormatServerDefault",
|
||||
"settingsServersFieldsName"
|
||||
],
|
||||
|
||||
"pl": [
|
||||
"actionsCancel",
|
||||
"actionsDelete",
|
||||
"actionsDownload",
|
||||
"actionsDownloadCancel",
|
||||
"actionsDownloadDelete",
|
||||
"actionsOk",
|
||||
"controlsShuffle",
|
||||
"resourcesAlbumCount",
|
||||
"resourcesArtistCount",
|
||||
"resourcesFilterAlbum",
|
||||
"resourcesFilterArtist",
|
||||
"resourcesFilterOwner",
|
||||
"resourcesFilterYear",
|
||||
"resourcesPlaylistCount",
|
||||
"resourcesSongCount",
|
||||
"resourcesSongListDeleteAllContent",
|
||||
"resourcesSongListDeleteAllTitle",
|
||||
"resourcesSortByAlbum",
|
||||
"resourcesSortByAlbumCount",
|
||||
"resourcesSortByTitle",
|
||||
"resourcesSortByUpdated",
|
||||
"settingsAboutActionsSupport",
|
||||
"settingsAboutShareLogs",
|
||||
"settingsAboutChooseLog",
|
||||
"settingsNetworkOptionsOfflineMode",
|
||||
"settingsNetworkOptionsOfflineModeOff",
|
||||
"settingsNetworkOptionsOfflineModeOn",
|
||||
"settingsNetworkOptionsStreamFormat",
|
||||
"settingsNetworkOptionsStreamFormatServerDefault",
|
||||
"settingsServersFieldsName"
|
||||
],
|
||||
|
||||
"pt": [
|
||||
"resourcesAlbumCount",
|
||||
"resourcesArtistCount",
|
||||
"resourcesFilterOwner",
|
||||
"resourcesPlaylistCount",
|
||||
"resourcesSongCount",
|
||||
"resourcesSongListDeleteAllContent",
|
||||
"resourcesSongListDeleteAllTitle",
|
||||
"resourcesSortByAlbumCount",
|
||||
"resourcesSortByUpdated",
|
||||
"settingsAboutShareLogs",
|
||||
"settingsAboutChooseLog",
|
||||
"settingsNetworkOptionsStreamFormatServerDefault",
|
||||
"settingsServersFieldsName"
|
||||
],
|
||||
|
||||
"tr": [
|
||||
"actionsCancel",
|
||||
"actionsDelete",
|
||||
"actionsDownload",
|
||||
"actionsDownloadCancel",
|
||||
"actionsDownloadDelete",
|
||||
"actionsOk",
|
||||
"controlsShuffle",
|
||||
"resourcesAlbumCount",
|
||||
"resourcesArtistCount",
|
||||
"resourcesFilterAlbum",
|
||||
"resourcesFilterArtist",
|
||||
"resourcesFilterOwner",
|
||||
"resourcesFilterYear",
|
||||
"resourcesPlaylistCount",
|
||||
"resourcesSongCount",
|
||||
"resourcesSongListDeleteAllContent",
|
||||
"resourcesSongListDeleteAllTitle",
|
||||
"resourcesSortByAlbum",
|
||||
"resourcesSortByAlbumCount",
|
||||
"resourcesSortByTitle",
|
||||
"resourcesSortByUpdated",
|
||||
"settingsAboutActionsSupport",
|
||||
"settingsAboutShareLogs",
|
||||
"settingsAboutChooseLog",
|
||||
"settingsNetworkOptionsOfflineMode",
|
||||
"settingsNetworkOptionsOfflineModeOff",
|
||||
"settingsNetworkOptionsOfflineModeOn",
|
||||
"settingsNetworkOptionsStreamFormat",
|
||||
"settingsNetworkOptionsStreamFormatServerDefault",
|
||||
"settingsServersFieldsName"
|
||||
],
|
||||
|
||||
"vi": [
|
||||
"actionsCancel",
|
||||
"actionsDelete",
|
||||
"actionsDownload",
|
||||
"actionsDownloadCancel",
|
||||
"actionsDownloadDelete",
|
||||
"actionsOk",
|
||||
"controlsShuffle",
|
||||
"resourcesAlbumCount",
|
||||
"resourcesArtistCount",
|
||||
"resourcesFilterAlbum",
|
||||
"resourcesFilterArtist",
|
||||
"resourcesFilterOwner",
|
||||
"resourcesFilterYear",
|
||||
"resourcesPlaylistCount",
|
||||
"resourcesSongCount",
|
||||
"resourcesSongListDeleteAllContent",
|
||||
"resourcesSongListDeleteAllTitle",
|
||||
"resourcesSortByAlbum",
|
||||
"resourcesSortByAlbumCount",
|
||||
"resourcesSortByTitle",
|
||||
"resourcesSortByUpdated",
|
||||
"settingsAboutActionsSupport",
|
||||
"settingsAboutShareLogs",
|
||||
"settingsAboutChooseLog",
|
||||
"settingsNetworkOptionsOfflineMode",
|
||||
"settingsNetworkOptionsOfflineModeOff",
|
||||
"settingsNetworkOptionsOfflineModeOn",
|
||||
"settingsNetworkOptionsStreamFormat",
|
||||
"settingsNetworkOptionsStreamFormatServerDefault",
|
||||
"settingsServersFieldsName"
|
||||
],
|
||||
|
||||
"zh": [
|
||||
"controlsShuffle",
|
||||
"resourcesAlbumCount",
|
||||
"resourcesArtistCount",
|
||||
"resourcesPlaylistCount",
|
||||
"resourcesSongCount",
|
||||
"settingsAboutShareLogs",
|
||||
"settingsAboutChooseLog",
|
||||
"settingsNetworkOptionsOfflineMode",
|
||||
"settingsNetworkOptionsOfflineModeOff",
|
||||
"settingsNetworkOptionsOfflineModeOn",
|
||||
"settingsNetworkOptionsStreamFormat",
|
||||
"settingsNetworkOptionsStreamFormatServerDefault",
|
||||
"settingsServersFieldsName"
|
||||
]
|
||||
}
|
||||
@@ -1,26 +0,0 @@
|
||||
{
|
||||
// Use IntelliSense to learn about possible attributes.
|
||||
// Hover to view descriptions of existing attributes.
|
||||
// For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387
|
||||
"version": "0.2.0",
|
||||
"configurations": [
|
||||
{
|
||||
"name": "debug",
|
||||
"request": "launch",
|
||||
"type": "dart",
|
||||
"flutterMode": "debug"
|
||||
},
|
||||
{
|
||||
"name": "profile mode",
|
||||
"request": "launch",
|
||||
"type": "dart",
|
||||
"flutterMode": "profile"
|
||||
},
|
||||
{
|
||||
"name": "release mode",
|
||||
"request": "launch",
|
||||
"type": "dart",
|
||||
"flutterMode": "release"
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -1,30 +0,0 @@
|
||||
## To-do
|
||||
- Star/unstar
|
||||
- Context menus
|
||||
- Download actions for song
|
||||
- Playlist management
|
||||
- Add to playlist (from context)
|
||||
- Queue management
|
||||
- View playing queue
|
||||
- Re-order queue
|
||||
- Add to queue (from context)
|
||||
- Remove from queue
|
||||
- Scrobbling
|
||||
- Library filters (year/genre/etc)
|
||||
- Library list display modes
|
||||
- Search
|
||||
- Individual "more" results pages
|
||||
- Now playing gestures
|
||||
- Swipe bar/album to skip
|
||||
- Double-tap to seek forward/back (bar only)
|
||||
- Settings
|
||||
- Music
|
||||
- Scrobble
|
||||
- Downloads
|
||||
- Used/available space
|
||||
- Clear downloads
|
||||
- Clear images
|
||||
- About
|
||||
- Licenses
|
||||
- Welcome/setup flow
|
||||
- Proper loading screen/animation
|
||||
@@ -1,19 +0,0 @@
|
||||
include: package:flutter_lints/flutter.yaml
|
||||
|
||||
linter:
|
||||
rules:
|
||||
prefer_relative_imports: true
|
||||
|
||||
analyzer:
|
||||
exclude:
|
||||
- '**.freezed.dart'
|
||||
- '**.g.dart'
|
||||
- '**.gr.dart'
|
||||
plugins:
|
||||
# broken currently and may not get fixed
|
||||
# https://github.com/simolus3/drift/issues/2342
|
||||
# - drift
|
||||
|
||||
# also broken but only recently reported
|
||||
# https://github.com/rrousselGit/riverpod/issues/2180
|
||||
# - custom_lint
|
||||
@@ -1,13 +0,0 @@
|
||||
gradle-wrapper.jar
|
||||
/.gradle
|
||||
/captures/
|
||||
/gradlew
|
||||
/gradlew.bat
|
||||
/local.properties
|
||||
GeneratedPluginRegistrant.java
|
||||
|
||||
# Remember to never publicly share your keystore.
|
||||
# See https://flutter.dev/docs/deployment/android#reference-the-keystore-from-the-app
|
||||
key.properties
|
||||
**/*.keystore
|
||||
**/*.jks
|
||||
@@ -1,88 +0,0 @@
|
||||
def localProperties = new Properties()
|
||||
def localPropertiesFile = rootProject.file('local.properties')
|
||||
if (localPropertiesFile.exists()) {
|
||||
localPropertiesFile.withReader('UTF-8') { reader ->
|
||||
localProperties.load(reader)
|
||||
}
|
||||
}
|
||||
|
||||
def flutterRoot = localProperties.getProperty('flutter.sdk')
|
||||
if (flutterRoot == null) {
|
||||
throw new GradleException("Flutter SDK not found. Define location with flutter.sdk in the local.properties file.")
|
||||
}
|
||||
|
||||
def flutterVersionCode = localProperties.getProperty('flutter.versionCode')
|
||||
if (flutterVersionCode == null) {
|
||||
flutterVersionCode = '1'
|
||||
}
|
||||
|
||||
def flutterVersionName = localProperties.getProperty('flutter.versionName')
|
||||
if (flutterVersionName == null) {
|
||||
flutterVersionName = '1.0'
|
||||
}
|
||||
|
||||
apply plugin: 'com.android.application'
|
||||
apply plugin: 'kotlin-android'
|
||||
apply from: "$flutterRoot/packages/flutter_tools/gradle/flutter.gradle"
|
||||
|
||||
def keystoreProperties = new Properties()
|
||||
def keystorePropertiesFile = rootProject.file('key.properties')
|
||||
if (keystorePropertiesFile.exists()) {
|
||||
keystoreProperties.load(new FileInputStream(keystorePropertiesFile))
|
||||
}
|
||||
|
||||
android {
|
||||
compileSdkVersion flutter.compileSdkVersion
|
||||
ndkVersion flutter.ndkVersion
|
||||
|
||||
compileOptions {
|
||||
sourceCompatibility JavaVersion.VERSION_1_8
|
||||
targetCompatibility JavaVersion.VERSION_1_8
|
||||
}
|
||||
|
||||
kotlinOptions {
|
||||
jvmTarget = '1.8'
|
||||
}
|
||||
|
||||
sourceSets {
|
||||
main.java.srcDirs += 'src/main/kotlin'
|
||||
}
|
||||
|
||||
defaultConfig {
|
||||
// TODO: Specify your own unique Application ID (https://developer.android.com/studio/build/application-id.html).
|
||||
applicationId "com.subtracks2"
|
||||
// You can update the following values to match your application needs.
|
||||
// For more information, see: https://docs.flutter.dev/deployment/android#reviewing-the-gradle-build-configuration.
|
||||
minSdkVersion 19
|
||||
targetSdkVersion flutter.targetSdkVersion
|
||||
versionCode flutterVersionCode.toInteger()
|
||||
versionName flutterVersionName
|
||||
}
|
||||
|
||||
signingConfigs {
|
||||
release {
|
||||
keyAlias keystoreProperties['keyAlias']
|
||||
keyPassword keystoreProperties['keyPassword']
|
||||
storeFile keystoreProperties['storeFile'] ? file(keystoreProperties['storeFile']) : null
|
||||
storePassword keystoreProperties['storePassword']
|
||||
}
|
||||
}
|
||||
|
||||
buildTypes {
|
||||
release {
|
||||
if (project.hasProperty("signRelease")) {
|
||||
signingConfig signingConfigs.release
|
||||
} else {
|
||||
signingConfig signingConfigs.debug
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
flutter {
|
||||
source '../..'
|
||||
}
|
||||
|
||||
dependencies {
|
||||
implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk7:$kotlin_version"
|
||||
}
|
||||
@@ -1,8 +0,0 @@
|
||||
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
package="com.subtracks2">
|
||||
<!-- The INTERNET permission is required for development. Specifically,
|
||||
the Flutter tool needs it to communicate with the running application
|
||||
to allow setting breakpoints, to provide hot reload, etc.
|
||||
-->
|
||||
<uses-permission android:name="android.permission.INTERNET"/>
|
||||
</manifest>
|
||||
@@ -1,72 +0,0 @@
|
||||
<manifest
|
||||
xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
xmlns:tools="http://schemas.android.com/tools"
|
||||
package="com.subtracks2">
|
||||
<application
|
||||
android:label="subtracks"
|
||||
android:name="${applicationName}"
|
||||
android:icon="@mipmap/ic_launcher"
|
||||
android:usesCleartextTraffic="true">
|
||||
<activity
|
||||
android:name="com.ryanheise.audioservice.AudioServiceActivity"
|
||||
android:exported="true"
|
||||
android:launchMode="singleTop"
|
||||
android:theme="@style/LaunchTheme"
|
||||
android:configChanges="orientation|keyboardHidden|keyboard|screenSize|smallestScreenSize|locale|layoutDirection|fontScale|screenLayout|density|uiMode"
|
||||
android:hardwareAccelerated="true"
|
||||
android:windowSoftInputMode="adjustResize">
|
||||
<!-- Specifies an Android theme to apply to this Activity as soon as
|
||||
the Android process has started. This theme is visible to the user
|
||||
while the Flutter UI initializes. After that, this theme continues
|
||||
to determine the Window background behind the Flutter UI. -->
|
||||
<meta-data
|
||||
android:name="io.flutter.embedding.android.NormalTheme"
|
||||
android:resource="@style/NormalTheme" />
|
||||
<intent-filter>
|
||||
<action android:name="android.intent.action.MAIN" />
|
||||
<category android:name="android.intent.category.LAUNCHER" />
|
||||
</intent-filter>
|
||||
</activity>
|
||||
<service
|
||||
android:name="com.ryanheise.audioservice.AudioService"
|
||||
android:foregroundServiceType="mediaPlayback"
|
||||
android:exported="true"
|
||||
tools:ignore="Instantiatable">
|
||||
<intent-filter>
|
||||
<action android:name="android.media.browse.MediaBrowserService" />
|
||||
</intent-filter>
|
||||
</service>
|
||||
<service
|
||||
android:name="com.ryanheise.audioservice.AudioService"
|
||||
android:foregroundServiceType="mediaPlayback"
|
||||
android:exported="true"
|
||||
tools:ignore="Instantiatable">
|
||||
<intent-filter>
|
||||
<action android:name="android.media.browse.MediaBrowserService" />
|
||||
</intent-filter>
|
||||
</service>
|
||||
<receiver
|
||||
android:name="com.ryanheise.audioservice.MediaButtonReceiver"
|
||||
android:exported="true"
|
||||
tools:ignore="Instantiatable">
|
||||
<intent-filter>
|
||||
<action android:name="android.intent.action.MEDIA_BUTTON" />
|
||||
</intent-filter>
|
||||
</receiver>
|
||||
|
||||
<!-- Don't delete the meta-data below.
|
||||
This is used by the Flutter tool to generate GeneratedPluginRegistrant.java -->
|
||||
<meta-data
|
||||
android:name="flutterEmbedding"
|
||||
android:value="2" />
|
||||
<!-- <meta-data
|
||||
android:name="io.flutter.embedding.android.EnableImpeller"
|
||||
android:value="true" /> -->
|
||||
</application>
|
||||
|
||||
<uses-permission android:name="android.permission.INTERNET" />
|
||||
|
||||
<!-- audio_service -->
|
||||
<uses-permission android:name="android.permission.WAKE_LOCK" />
|
||||
<uses-permission android:name="android.permission.FOREGROUND_SERVICE" />
|
||||
</manifest>
|
||||
|
Before Width: | Height: | Size: 13 KiB |
@@ -1,25 +0,0 @@
|
||||
// Generated file.
|
||||
//
|
||||
// If you wish to remove Flutter's multidex support, delete this entire file.
|
||||
//
|
||||
// Modifications to this file should be done in a copy under a different name
|
||||
// as this file may be regenerated.
|
||||
|
||||
package io.flutter.app;
|
||||
|
||||
import android.app.Application;
|
||||
import android.content.Context;
|
||||
import androidx.annotation.CallSuper;
|
||||
import androidx.multidex.MultiDex;
|
||||
|
||||
/**
|
||||
* Extension of {@link android.app.Application}, adding multidex support.
|
||||
*/
|
||||
public class FlutterMultiDexApplication extends Application {
|
||||
@Override
|
||||
@CallSuper
|
||||
protected void attachBaseContext(Context base) {
|
||||
super.attachBaseContext(base);
|
||||
MultiDex.install(this);
|
||||
}
|
||||
}
|
||||
@@ -1,6 +0,0 @@
|
||||
package com.subtracks2
|
||||
|
||||
import io.flutter.embedding.android.FlutterActivity
|
||||
|
||||
class MainActivity: FlutterActivity() {
|
||||
}
|
||||
@@ -1,29 +0,0 @@
|
||||
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:width="24dp"
|
||||
android:height="24dp"
|
||||
android:viewportWidth="299.84"
|
||||
android:viewportHeight="219.51"
|
||||
android:tint="#333333"
|
||||
android:alpha="0.6">
|
||||
<group android:scaleY="0.7320905"
|
||||
android:translateY="29.404413">
|
||||
<path
|
||||
android:pathData="m23.84,-0c-12.76,0 -23.44,13 -23.65,25.12 -0.42,79.03 0,160.73 0,170.4 0,12.59 9.8,23.99 23.41,23.99l253.58,0c11.4,0 22.66,-12.38 22.66,-22.79L299.84,24.12c0,-10.8 -10.24,-23.81 -21.84,-23.81 -11.6,0 -254.16,-0.31 -254.16,-0.31zM39.05,25.28c0,0 217.71,0.71 223.67,0.71 6.07,0 12.17,6.83 12.17,12.06l0,142.25c0,8.97 -6.97,14.63 -12.48,14.64l-33.42,0.07c-5.32,0.01 -9.93,-7.19 -11.35,-10.61 -1.89,-4.55 -11.08,-25.75 -12.1,-28.1 -0.6,-1.38 -2.9,-2.83 -5.89,-2.83L98.33,153.45c-2.68,0 -4.89,2.87 -5.67,4.71 -1.12,2.66 -9.02,21.39 -11.63,27.57 -2.73,6.46 -4.37,9.06 -11.47,9.06l-30.97,0c-7.44,0 -13.69,-7.17 -13.67,-15.18l0.28,-139.51c0,-6.88 5.54,-14.83 13.85,-14.83z"
|
||||
android:strokeWidth="0.9"
|
||||
android:fillColor="#ffffff"
|
||||
android:fillType="nonZero"
|
||||
android:strokeColor="#00000000"/>
|
||||
<path
|
||||
android:pathData="M90.09,95.03m-32.1,0a32.42,32.1 90,1 1,64.21 0a32.42,32.1 90,1 1,-64.21 0"
|
||||
android:strokeWidth="0.86"
|
||||
android:fillColor="#ffffff"
|
||||
android:fillType="nonZero"
|
||||
android:strokeColor="#00000000"/>
|
||||
<path
|
||||
android:pathData="M209.94,95.03m-32.1,0a32.42,32.1 90,1 1,64.21 0a32.42,32.1 90,1 1,-64.21 0"
|
||||
android:strokeWidth="0.86"
|
||||
android:fillColor="#ffffff"
|
||||
android:fillType="nonZero"
|
||||
android:strokeColor="#00000000"/>
|
||||
</group>
|
||||
</vector>
|
||||
@@ -1,12 +0,0 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<!-- Modify this file to customize your launch splash screen -->
|
||||
<layer-list xmlns:android="http://schemas.android.com/apk/res/android">
|
||||
<item android:drawable="?android:colorBackground" />
|
||||
|
||||
<!-- You can insert your own image assets here -->
|
||||
<!-- <item>
|
||||
<bitmap
|
||||
android:gravity="center"
|
||||
android:src="@mipmap/launch_image" />
|
||||
</item> -->
|
||||
</layer-list>
|
||||
@@ -1,12 +0,0 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<!-- Modify this file to customize your launch splash screen -->
|
||||
<layer-list xmlns:android="http://schemas.android.com/apk/res/android">
|
||||
<item android:drawable="@android:color/white" />
|
||||
|
||||
<!-- You can insert your own image assets here -->
|
||||
<!-- <item>
|
||||
<bitmap
|
||||
android:gravity="center"
|
||||
android:src="@mipmap/launch_image" />
|
||||
</item> -->
|
||||
</layer-list>
|
||||
@@ -1,4 +0,0 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<resources
|
||||
xmlns:tools="http://schemas.android.com/tools"
|
||||
tools:keep="@drawable/*" />
|
||||
@@ -1,18 +0,0 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<resources>
|
||||
<!-- Theme applied to the Android Window while the process is starting when the OS's Dark Mode setting is on -->
|
||||
<style name="LaunchTheme" parent="@android:style/Theme.Black.NoTitleBar">
|
||||
<!-- Show a splash screen on the activity. Automatically removed when
|
||||
the Flutter engine draws its first frame -->
|
||||
<item name="android:windowBackground">@drawable/launch_background</item>
|
||||
</style>
|
||||
<!-- Theme applied to the Android Window as soon as the process has started.
|
||||
This theme determines the color of the Android Window while your
|
||||
Flutter UI initializes, as well as behind your Flutter UI while its
|
||||
running.
|
||||
|
||||
This Theme is only used starting with V2 of Flutter's Android embedding. -->
|
||||
<style name="NormalTheme" parent="@android:style/Theme.Black.NoTitleBar">
|
||||
<item name="android:windowBackground">?android:colorBackground</item>
|
||||
</style>
|
||||
</resources>
|
||||
@@ -1,18 +0,0 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<resources>
|
||||
<!-- Theme applied to the Android Window while the process is starting when the OS's Dark Mode setting is off -->
|
||||
<style name="LaunchTheme" parent="@android:style/Theme.Light.NoTitleBar">
|
||||
<!-- Show a splash screen on the activity. Automatically removed when
|
||||
the Flutter engine draws its first frame -->
|
||||
<item name="android:windowBackground">@drawable/launch_background</item>
|
||||
</style>
|
||||
<!-- Theme applied to the Android Window as soon as the process has started.
|
||||
This theme determines the color of the Android Window while your
|
||||
Flutter UI initializes, as well as behind your Flutter UI while its
|
||||
running.
|
||||
|
||||
This Theme is only used starting with V2 of Flutter's Android embedding. -->
|
||||
<style name="NormalTheme" parent="@android:style/Theme.Light.NoTitleBar">
|
||||
<item name="android:windowBackground">?android:colorBackground</item>
|
||||
</style>
|
||||
</resources>
|
||||
@@ -1,8 +0,0 @@
|
||||
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
package="com.subtracks2">
|
||||
<!-- The INTERNET permission is required for development. Specifically,
|
||||
the Flutter tool needs it to communicate with the running application
|
||||
to allow setting breakpoints, to provide hot reload, etc.
|
||||
-->
|
||||
<uses-permission android:name="android.permission.INTERNET"/>
|
||||
</manifest>
|
||||
@@ -1,31 +0,0 @@
|
||||
buildscript {
|
||||
ext.kotlin_version = '1.7.10'
|
||||
repositories {
|
||||
google()
|
||||
mavenCentral()
|
||||
}
|
||||
|
||||
dependencies {
|
||||
classpath 'com.android.tools.build:gradle:7.2.0'
|
||||
classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlin_version"
|
||||
}
|
||||
}
|
||||
|
||||
allprojects {
|
||||
repositories {
|
||||
google()
|
||||
mavenCentral()
|
||||
}
|
||||
}
|
||||
|
||||
rootProject.buildDir = '../build'
|
||||
subprojects {
|
||||
project.buildDir = "${rootProject.buildDir}/${project.name}"
|
||||
}
|
||||
subprojects {
|
||||
project.evaluationDependsOn(':app')
|
||||
}
|
||||
|
||||
task clean(type: Delete) {
|
||||
delete rootProject.buildDir
|
||||
}
|
||||
@@ -1,3 +0,0 @@
|
||||
org.gradle.jvmargs=-Xmx1536M
|
||||
android.useAndroidX=true
|
||||
android.enableJetifier=true
|
||||
@@ -1,160 +0,0 @@
|
||||
#!/usr/bin/env bash
|
||||
|
||||
##############################################################################
|
||||
##
|
||||
## Gradle start up script for UN*X
|
||||
##
|
||||
##############################################################################
|
||||
|
||||
# Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
|
||||
DEFAULT_JVM_OPTS=""
|
||||
|
||||
APP_NAME="Gradle"
|
||||
APP_BASE_NAME=`basename "$0"`
|
||||
|
||||
# Use the maximum available, or set MAX_FD != -1 to use that value.
|
||||
MAX_FD="maximum"
|
||||
|
||||
warn ( ) {
|
||||
echo "$*"
|
||||
}
|
||||
|
||||
die ( ) {
|
||||
echo
|
||||
echo "$*"
|
||||
echo
|
||||
exit 1
|
||||
}
|
||||
|
||||
# OS specific support (must be 'true' or 'false').
|
||||
cygwin=false
|
||||
msys=false
|
||||
darwin=false
|
||||
case "`uname`" in
|
||||
CYGWIN* )
|
||||
cygwin=true
|
||||
;;
|
||||
Darwin* )
|
||||
darwin=true
|
||||
;;
|
||||
MINGW* )
|
||||
msys=true
|
||||
;;
|
||||
esac
|
||||
|
||||
# Attempt to set APP_HOME
|
||||
# Resolve links: $0 may be a link
|
||||
PRG="$0"
|
||||
# Need this for relative symlinks.
|
||||
while [ -h "$PRG" ] ; do
|
||||
ls=`ls -ld "$PRG"`
|
||||
link=`expr "$ls" : '.*-> \(.*\)$'`
|
||||
if expr "$link" : '/.*' > /dev/null; then
|
||||
PRG="$link"
|
||||
else
|
||||
PRG=`dirname "$PRG"`"/$link"
|
||||
fi
|
||||
done
|
||||
SAVED="`pwd`"
|
||||
cd "`dirname \"$PRG\"`/" >/dev/null
|
||||
APP_HOME="`pwd -P`"
|
||||
cd "$SAVED" >/dev/null
|
||||
|
||||
CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar
|
||||
|
||||
# Determine the Java command to use to start the JVM.
|
||||
if [ -n "$JAVA_HOME" ] ; then
|
||||
if [ -x "$JAVA_HOME/jre/sh/java" ] ; then
|
||||
# IBM's JDK on AIX uses strange locations for the executables
|
||||
JAVACMD="$JAVA_HOME/jre/sh/java"
|
||||
else
|
||||
JAVACMD="$JAVA_HOME/bin/java"
|
||||
fi
|
||||
if [ ! -x "$JAVACMD" ] ; then
|
||||
die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME
|
||||
|
||||
Please set the JAVA_HOME variable in your environment to match the
|
||||
location of your Java installation."
|
||||
fi
|
||||
else
|
||||
JAVACMD="java"
|
||||
which java >/dev/null 2>&1 || die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.
|
||||
|
||||
Please set the JAVA_HOME variable in your environment to match the
|
||||
location of your Java installation."
|
||||
fi
|
||||
|
||||
# Increase the maximum file descriptors if we can.
|
||||
if [ "$cygwin" = "false" -a "$darwin" = "false" ] ; then
|
||||
MAX_FD_LIMIT=`ulimit -H -n`
|
||||
if [ $? -eq 0 ] ; then
|
||||
if [ "$MAX_FD" = "maximum" -o "$MAX_FD" = "max" ] ; then
|
||||
MAX_FD="$MAX_FD_LIMIT"
|
||||
fi
|
||||
ulimit -n $MAX_FD
|
||||
if [ $? -ne 0 ] ; then
|
||||
warn "Could not set maximum file descriptor limit: $MAX_FD"
|
||||
fi
|
||||
else
|
||||
warn "Could not query maximum file descriptor limit: $MAX_FD_LIMIT"
|
||||
fi
|
||||
fi
|
||||
|
||||
# For Darwin, add options to specify how the application appears in the dock
|
||||
if $darwin; then
|
||||
GRADLE_OPTS="$GRADLE_OPTS \"-Xdock:name=$APP_NAME\" \"-Xdock:icon=$APP_HOME/media/gradle.icns\""
|
||||
fi
|
||||
|
||||
# For Cygwin, switch paths to Windows format before running java
|
||||
if $cygwin ; then
|
||||
APP_HOME=`cygpath --path --mixed "$APP_HOME"`
|
||||
CLASSPATH=`cygpath --path --mixed "$CLASSPATH"`
|
||||
JAVACMD=`cygpath --unix "$JAVACMD"`
|
||||
|
||||
# We build the pattern for arguments to be converted via cygpath
|
||||
ROOTDIRSRAW=`find -L / -maxdepth 1 -mindepth 1 -type d 2>/dev/null`
|
||||
SEP=""
|
||||
for dir in $ROOTDIRSRAW ; do
|
||||
ROOTDIRS="$ROOTDIRS$SEP$dir"
|
||||
SEP="|"
|
||||
done
|
||||
OURCYGPATTERN="(^($ROOTDIRS))"
|
||||
# Add a user-defined pattern to the cygpath arguments
|
||||
if [ "$GRADLE_CYGPATTERN" != "" ] ; then
|
||||
OURCYGPATTERN="$OURCYGPATTERN|($GRADLE_CYGPATTERN)"
|
||||
fi
|
||||
# Now convert the arguments - kludge to limit ourselves to /bin/sh
|
||||
i=0
|
||||
for arg in "$@" ; do
|
||||
CHECK=`echo "$arg"|egrep -c "$OURCYGPATTERN" -`
|
||||
CHECK2=`echo "$arg"|egrep -c "^-"` ### Determine if an option
|
||||
|
||||
if [ $CHECK -ne 0 ] && [ $CHECK2 -eq 0 ] ; then ### Added a condition
|
||||
eval `echo args$i`=`cygpath --path --ignore --mixed "$arg"`
|
||||
else
|
||||
eval `echo args$i`="\"$arg\""
|
||||
fi
|
||||
i=$((i+1))
|
||||
done
|
||||
case $i in
|
||||
(0) set -- ;;
|
||||
(1) set -- "$args0" ;;
|
||||
(2) set -- "$args0" "$args1" ;;
|
||||
(3) set -- "$args0" "$args1" "$args2" ;;
|
||||
(4) set -- "$args0" "$args1" "$args2" "$args3" ;;
|
||||
(5) set -- "$args0" "$args1" "$args2" "$args3" "$args4" ;;
|
||||
(6) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" ;;
|
||||
(7) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" ;;
|
||||
(8) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" ;;
|
||||
(9) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" "$args8" ;;
|
||||
esac
|
||||
fi
|
||||
|
||||
# Split up the JVM_OPTS And GRADLE_OPTS values into an array, following the shell quoting and substitution rules
|
||||
function splitJvmOpts() {
|
||||
JVM_OPTS=("$@")
|
||||
}
|
||||
eval splitJvmOpts $DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS
|
||||
JVM_OPTS[${#JVM_OPTS[*]}]="-Dorg.gradle.appname=$APP_BASE_NAME"
|
||||
|
||||
exec "$JAVACMD" "${JVM_OPTS[@]}" -classpath "$CLASSPATH" org.gradle.wrapper.GradleWrapperMain "$@"
|
||||
@@ -1,90 +0,0 @@
|
||||
@if "%DEBUG%" == "" @echo off
|
||||
@rem ##########################################################################
|
||||
@rem
|
||||
@rem Gradle startup script for Windows
|
||||
@rem
|
||||
@rem ##########################################################################
|
||||
|
||||
@rem Set local scope for the variables with windows NT shell
|
||||
if "%OS%"=="Windows_NT" setlocal
|
||||
|
||||
@rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
|
||||
set DEFAULT_JVM_OPTS=
|
||||
|
||||
set DIRNAME=%~dp0
|
||||
if "%DIRNAME%" == "" set DIRNAME=.
|
||||
set APP_BASE_NAME=%~n0
|
||||
set APP_HOME=%DIRNAME%
|
||||
|
||||
@rem Find java.exe
|
||||
if defined JAVA_HOME goto findJavaFromJavaHome
|
||||
|
||||
set JAVA_EXE=java.exe
|
||||
%JAVA_EXE% -version >NUL 2>&1
|
||||
if "%ERRORLEVEL%" == "0" goto init
|
||||
|
||||
echo.
|
||||
echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.
|
||||
echo.
|
||||
echo Please set the JAVA_HOME variable in your environment to match the
|
||||
echo location of your Java installation.
|
||||
|
||||
goto fail
|
||||
|
||||
:findJavaFromJavaHome
|
||||
set JAVA_HOME=%JAVA_HOME:"=%
|
||||
set JAVA_EXE=%JAVA_HOME%/bin/java.exe
|
||||
|
||||
if exist "%JAVA_EXE%" goto init
|
||||
|
||||
echo.
|
||||
echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME%
|
||||
echo.
|
||||
echo Please set the JAVA_HOME variable in your environment to match the
|
||||
echo location of your Java installation.
|
||||
|
||||
goto fail
|
||||
|
||||
:init
|
||||
@rem Get command-line arguments, handling Windowz variants
|
||||
|
||||
if not "%OS%" == "Windows_NT" goto win9xME_args
|
||||
if "%@eval[2+2]" == "4" goto 4NT_args
|
||||
|
||||
:win9xME_args
|
||||
@rem Slurp the command line arguments.
|
||||
set CMD_LINE_ARGS=
|
||||
set _SKIP=2
|
||||
|
||||
:win9xME_args_slurp
|
||||
if "x%~1" == "x" goto execute
|
||||
|
||||
set CMD_LINE_ARGS=%*
|
||||
goto execute
|
||||
|
||||
:4NT_args
|
||||
@rem Get arguments from the 4NT Shell from JP Software
|
||||
set CMD_LINE_ARGS=%$
|
||||
|
||||
:execute
|
||||
@rem Setup the command line
|
||||
|
||||
set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar
|
||||
|
||||
@rem Execute Gradle
|
||||
"%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %CMD_LINE_ARGS%
|
||||
|
||||
:end
|
||||
@rem End local scope for the variables with windows NT shell
|
||||
if "%ERRORLEVEL%"=="0" goto mainEnd
|
||||
|
||||
:fail
|
||||
rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of
|
||||
rem the _cmd.exe /c_ return code!
|
||||
if not "" == "%GRADLE_EXIT_CONSOLE%" exit 1
|
||||
exit /b 1
|
||||
|
||||
:mainEnd
|
||||
if "%OS%"=="Windows_NT" endlocal
|
||||
|
||||
:omega
|
||||
@@ -1,11 +0,0 @@
|
||||
include ':app'
|
||||
|
||||
def localPropertiesFile = new File(rootProject.projectDir, "local.properties")
|
||||
def properties = new Properties()
|
||||
|
||||
assert localPropertiesFile.exists()
|
||||
localPropertiesFile.withReader("UTF-8") { reader -> properties.load(reader) }
|
||||
|
||||
def flutterSdkPath = properties.getProperty("flutter.sdk")
|
||||
assert flutterSdkPath != null, "flutter.sdk not set in local.properties"
|
||||
apply from: "$flutterSdkPath/packages/flutter_tools/gradle/app_plugin_loader.gradle"
|
||||
@@ -0,0 +1,79 @@
|
||||
plugins {
|
||||
alias(libs.plugins.android.application)
|
||||
alias(libs.plugins.kotlin.android)
|
||||
alias(libs.plugins.compose.compiler)
|
||||
alias(libs.plugins.roborazzi)
|
||||
}
|
||||
|
||||
android {
|
||||
namespace = "com.subtracks"
|
||||
compileSdk = 35
|
||||
buildToolsVersion = "37.0.0"
|
||||
|
||||
defaultConfig {
|
||||
applicationId = "com.subtracks.next"
|
||||
minSdk = 24
|
||||
targetSdk = 35
|
||||
versionCode = 1
|
||||
versionName = "0.1.0"
|
||||
testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner"
|
||||
}
|
||||
|
||||
buildFeatures {
|
||||
compose = true
|
||||
}
|
||||
|
||||
compileOptions {
|
||||
sourceCompatibility = JavaVersion.VERSION_17
|
||||
targetCompatibility = JavaVersion.VERSION_17
|
||||
}
|
||||
|
||||
kotlinOptions {
|
||||
jvmTarget = "17"
|
||||
}
|
||||
|
||||
testOptions {
|
||||
unitTests {
|
||||
isIncludeAndroidResources = true
|
||||
}
|
||||
}
|
||||
|
||||
buildTypes {
|
||||
release {
|
||||
isMinifyEnabled = false
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
roborazzi {
|
||||
outputDir.set(file("src/test/screenshots"))
|
||||
}
|
||||
|
||||
dependencies {
|
||||
implementation(platform(libs.androidx.compose.bom))
|
||||
implementation(libs.androidx.core.ktx)
|
||||
implementation(libs.androidx.lifecycle.runtime.ktx)
|
||||
implementation(libs.androidx.lifecycle.viewmodel.compose)
|
||||
implementation(libs.androidx.activity.compose)
|
||||
implementation(libs.androidx.compose.ui)
|
||||
implementation(libs.androidx.compose.ui.graphics)
|
||||
implementation(libs.androidx.compose.ui.tooling.preview)
|
||||
implementation(libs.androidx.compose.material3)
|
||||
implementation(libs.androidx.navigation.compose)
|
||||
implementation(libs.koin.android)
|
||||
implementation(libs.koin.androidx.compose)
|
||||
implementation(libs.kotlinx.coroutines.android)
|
||||
implementation(libs.kotlinx.serialization.json)
|
||||
|
||||
debugImplementation(libs.androidx.compose.ui.tooling)
|
||||
debugImplementation(libs.androidx.compose.ui.test.manifest)
|
||||
|
||||
testImplementation(libs.junit)
|
||||
testImplementation(libs.robolectric)
|
||||
testImplementation(libs.roborazzi)
|
||||
testImplementation(libs.roborazzi.compose)
|
||||
testImplementation(libs.roborazzi.junit.rule)
|
||||
testImplementation(libs.androidx.compose.ui.test.junit4)
|
||||
testImplementation(libs.androidx.test.ext.junit)
|
||||
testImplementation(libs.androidx.test.core)
|
||||
}
|
||||
@@ -0,0 +1,34 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<manifest xmlns:android="http://schemas.android.com/apk/res/android">
|
||||
|
||||
<uses-permission android:name="android.permission.INTERNET" />
|
||||
<uses-permission android:name="android.permission.ACCESS_NETWORK_STATE" />
|
||||
<uses-permission android:name="android.permission.WAKE_LOCK" />
|
||||
<uses-permission android:name="android.permission.FOREGROUND_SERVICE" />
|
||||
<uses-permission android:name="android.permission.FOREGROUND_SERVICE_MEDIA_PLAYBACK" />
|
||||
<uses-permission android:name="android.permission.POST_NOTIFICATIONS" />
|
||||
|
||||
<application
|
||||
android:name=".SubtracksApp"
|
||||
android:allowBackup="true"
|
||||
android:icon="@mipmap/ic_launcher"
|
||||
android:label="@string/app_name"
|
||||
android:roundIcon="@mipmap/ic_launcher_round"
|
||||
android:supportsRtl="true"
|
||||
android:theme="@style/Theme.Subtracks"
|
||||
android:usesCleartextTraffic="true">
|
||||
|
||||
<activity
|
||||
android:name=".MainActivity"
|
||||
android:exported="true"
|
||||
android:launchMode="singleTop"
|
||||
android:theme="@style/Theme.Subtracks"
|
||||
android:windowSoftInputMode="adjustResize">
|
||||
<intent-filter>
|
||||
<action android:name="android.intent.action.MAIN" />
|
||||
<category android:name="android.intent.category.LAUNCHER" />
|
||||
</intent-filter>
|
||||
</activity>
|
||||
</application>
|
||||
|
||||
</manifest>
|
||||
@@ -0,0 +1,20 @@
|
||||
package com.subtracks
|
||||
|
||||
import android.os.Bundle
|
||||
import androidx.activity.ComponentActivity
|
||||
import androidx.activity.compose.setContent
|
||||
import androidx.activity.enableEdgeToEdge
|
||||
import com.subtracks.ui.HelloScreen
|
||||
import com.subtracks.ui.theme.SubtracksTheme
|
||||
|
||||
class MainActivity : ComponentActivity() {
|
||||
override fun onCreate(savedInstanceState: Bundle?) {
|
||||
super.onCreate(savedInstanceState)
|
||||
enableEdgeToEdge()
|
||||
setContent {
|
||||
SubtracksTheme {
|
||||
HelloScreen()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,5 @@
|
||||
package com.subtracks
|
||||
|
||||
import android.app.Application
|
||||
|
||||
class SubtracksApp : Application()
|
||||
@@ -0,0 +1,53 @@
|
||||
package com.subtracks.ui
|
||||
|
||||
import androidx.compose.foundation.layout.Arrangement
|
||||
import androidx.compose.foundation.layout.Column
|
||||
import androidx.compose.foundation.layout.Spacer
|
||||
import androidx.compose.foundation.layout.fillMaxSize
|
||||
import androidx.compose.foundation.layout.height
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.material3.MaterialTheme
|
||||
import androidx.compose.material3.Surface
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.tooling.preview.Preview
|
||||
import androidx.compose.ui.unit.dp
|
||||
import com.subtracks.ui.theme.SubtracksTheme
|
||||
|
||||
@Composable
|
||||
fun HelloScreen(modifier: Modifier = Modifier) {
|
||||
Surface(
|
||||
modifier = modifier.fillMaxSize(),
|
||||
color = MaterialTheme.colorScheme.background,
|
||||
) {
|
||||
Column(
|
||||
modifier = Modifier
|
||||
.fillMaxSize()
|
||||
.padding(24.dp),
|
||||
verticalArrangement = Arrangement.Center,
|
||||
horizontalAlignment = Alignment.CenterHorizontally,
|
||||
) {
|
||||
Text(
|
||||
text = "Subtracks",
|
||||
style = MaterialTheme.typography.headlineLarge,
|
||||
color = MaterialTheme.colorScheme.onBackground,
|
||||
)
|
||||
Spacer(Modifier.height(8.dp))
|
||||
Text(
|
||||
text = "Kotlin / Jetpack Compose rewrite",
|
||||
style = MaterialTheme.typography.bodyMedium,
|
||||
color = MaterialTheme.colorScheme.onSurfaceVariant,
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Preview
|
||||
@Composable
|
||||
private fun HelloScreenPreview() {
|
||||
SubtracksTheme {
|
||||
HelloScreen()
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
package com.subtracks.ui.theme
|
||||
|
||||
import androidx.compose.ui.graphics.Color
|
||||
|
||||
val Purple80 = Color(0xFFD0BCFF)
|
||||
val PurpleGrey80 = Color(0xFFCCC2DC)
|
||||
val Pink80 = Color(0xFFEFB8C8)
|
||||
|
||||
val Purple40 = Color(0xFF6650A4)
|
||||
val PurpleGrey40 = Color(0xFF625B71)
|
||||
val Pink40 = Color(0xFF7D5260)
|
||||
@@ -0,0 +1,20 @@
|
||||
package com.subtracks.ui.theme
|
||||
|
||||
import androidx.compose.material3.MaterialTheme
|
||||
import androidx.compose.material3.darkColorScheme
|
||||
import androidx.compose.runtime.Composable
|
||||
|
||||
private val SubtracksDarkColorScheme = darkColorScheme(
|
||||
primary = Purple80,
|
||||
secondary = PurpleGrey80,
|
||||
tertiary = Pink80,
|
||||
)
|
||||
|
||||
@Composable
|
||||
fun SubtracksTheme(content: @Composable () -> Unit) {
|
||||
MaterialTheme(
|
||||
colorScheme = SubtracksDarkColorScheme,
|
||||
typography = SubtracksTypography,
|
||||
content = content,
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,12 @@
|
||||
package com.subtracks.ui.theme
|
||||
|
||||
import androidx.compose.material3.Typography
|
||||
import androidx.compose.ui.text.font.FontWeight
|
||||
|
||||
val SubtracksTypography = Typography().let { base ->
|
||||
base.copy(
|
||||
headlineLarge = base.headlineLarge.copy(fontWeight = FontWeight.W800),
|
||||
headlineMedium = base.headlineMedium.copy(fontWeight = FontWeight.W700),
|
||||
headlineSmall = base.headlineSmall.copy(fontWeight = FontWeight.W600),
|
||||
)
|
||||
}
|
||||
|
Before Width: | Height: | Size: 441 B After Width: | Height: | Size: 441 B |
|
Before Width: | Height: | Size: 401 B After Width: | Height: | Size: 401 B |
|
Before Width: | Height: | Size: 318 B After Width: | Height: | Size: 318 B |
|
Before Width: | Height: | Size: 284 B After Width: | Height: | Size: 284 B |
|
Before Width: | Height: | Size: 600 B After Width: | Height: | Size: 600 B |
|
Before Width: | Height: | Size: 575 B After Width: | Height: | Size: 575 B |
|
Before Width: | Height: | Size: 925 B After Width: | Height: | Size: 925 B |
|
Before Width: | Height: | Size: 921 B After Width: | Height: | Size: 921 B |
|
Before Width: | Height: | Size: 207 KiB After Width: | Height: | Size: 207 KiB |
|
Before Width: | Height: | Size: 13 KiB After Width: | Height: | Size: 13 KiB |
|
Before Width: | Height: | Size: 1.4 KiB After Width: | Height: | Size: 1.4 KiB |
|
Before Width: | Height: | Size: 4.2 KiB After Width: | Height: | Size: 4.2 KiB |
|
Before Width: | Height: | Size: 3.4 KiB After Width: | Height: | Size: 3.4 KiB |
|
Before Width: | Height: | Size: 1.1 KiB After Width: | Height: | Size: 1.1 KiB |
|
Before Width: | Height: | Size: 2.6 KiB After Width: | Height: | Size: 2.6 KiB |
|
Before Width: | Height: | Size: 2.1 KiB After Width: | Height: | Size: 2.1 KiB |
|
Before Width: | Height: | Size: 2.1 KiB After Width: | Height: | Size: 2.1 KiB |
|
Before Width: | Height: | Size: 6.2 KiB After Width: | Height: | Size: 6.2 KiB |
|
Before Width: | Height: | Size: 4.9 KiB After Width: | Height: | Size: 4.9 KiB |
|
Before Width: | Height: | Size: 3.2 KiB After Width: | Height: | Size: 3.2 KiB |
|
Before Width: | Height: | Size: 11 KiB After Width: | Height: | Size: 11 KiB |
|
Before Width: | Height: | Size: 7.7 KiB After Width: | Height: | Size: 7.7 KiB |
|
Before Width: | Height: | Size: 4.6 KiB After Width: | Height: | Size: 4.6 KiB |
|
Before Width: | Height: | Size: 16 KiB After Width: | Height: | Size: 16 KiB |
|
Before Width: | Height: | Size: 11 KiB After Width: | Height: | Size: 11 KiB |
@@ -0,0 +1,4 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<resources>
|
||||
<string name="app_name">Subtracks</string>
|
||||
</resources>
|
||||
@@ -0,0 +1,8 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<resources>
|
||||
<style name="Theme.Subtracks" parent="android:Theme.Material.NoActionBar">
|
||||
<item name="android:windowBackground">@android:color/black</item>
|
||||
<item name="android:statusBarColor">@android:color/transparent</item>
|
||||
<item name="android:navigationBarColor">@android:color/transparent</item>
|
||||
</style>
|
||||
</resources>
|
||||
@@ -0,0 +1,32 @@
|
||||
package com.subtracks.ui
|
||||
|
||||
import androidx.activity.ComponentActivity
|
||||
import androidx.compose.ui.test.junit4.createAndroidComposeRule
|
||||
import androidx.compose.ui.test.onRoot
|
||||
import androidx.test.ext.junit.runners.AndroidJUnit4
|
||||
import com.github.takahirom.roborazzi.RobolectricDeviceQualifiers
|
||||
import com.github.takahirom.roborazzi.captureRoboImage
|
||||
import com.subtracks.ui.theme.SubtracksTheme
|
||||
import org.junit.Rule
|
||||
import org.junit.Test
|
||||
import org.junit.runner.RunWith
|
||||
import org.robolectric.annotation.Config
|
||||
import org.robolectric.annotation.GraphicsMode
|
||||
|
||||
@RunWith(AndroidJUnit4::class)
|
||||
@GraphicsMode(GraphicsMode.Mode.NATIVE)
|
||||
@Config(qualifiers = RobolectricDeviceQualifiers.PixelXL)
|
||||
class HelloScreenshotTest {
|
||||
@get:Rule
|
||||
val composeRule = createAndroidComposeRule<ComponentActivity>()
|
||||
|
||||
@Test
|
||||
fun hello() {
|
||||
composeRule.setContent {
|
||||
SubtracksTheme {
|
||||
HelloScreen()
|
||||
}
|
||||
}
|
||||
composeRule.onRoot().captureRoboImage("src/test/screenshots/hello.png")
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1 @@
|
||||
sdk=34
|
||||
@@ -1 +0,0 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" height="24" viewBox="0 96 960 960" width="24"><path d="M290 896q-19 0-31-15t-7-33l28-112h-89q-20 0-32-15.5t-7-34.5q3-14 14-22t25-8h109l40-160H231q-20 0-32-15.5t-7-34.5q3-14 14-22t25-8h129l33-131q3-13 13-21t24-8q19 0 31 15t7 33l-28 112h160l33-131q3-13 13-21t24-8q19 0 31 15t7 33l-28 112h89q20 0 32 15.5t7 34.5q-3 14-14 22t-25 8H660l-40 160h109q20 0 32 15.5t7 34.5q-3 14-14 22t-25 8H600l-33 131q-3 13-13 21t-24 8q-19 0-31-15t-7-33l28-112H360l-33 131q-3 13-13 21t-24 8Zm90-240h160l40-160H420l-40 160Z"/></svg>
|
||||
|
Before Width: | Height: | Size: 546 B |
@@ -0,0 +1,7 @@
|
||||
plugins {
|
||||
alias(libs.plugins.android.application) apply false
|
||||
alias(libs.plugins.kotlin.android) apply false
|
||||
alias(libs.plugins.kotlin.serialization) apply false
|
||||
alias(libs.plugins.compose.compiler) apply false
|
||||
alias(libs.plugins.roborazzi) apply false
|
||||
}
|
||||
@@ -1,11 +0,0 @@
|
||||
targets:
|
||||
$default:
|
||||
builders:
|
||||
drift_dev:
|
||||
options:
|
||||
sql:
|
||||
dialect: sqlite
|
||||
options:
|
||||
version: "3.38"
|
||||
modules:
|
||||
- fts5
|
||||
@@ -0,0 +1,27 @@
|
||||
{
|
||||
"nodes": {
|
||||
"nixpkgs": {
|
||||
"locked": {
|
||||
"lastModified": 1780453794,
|
||||
"narHash": "sha256-bXMRa9VTsHSPXL4Cw8R6JJLQeY3Y/IP4+YJCYVmQ7FY=",
|
||||
"owner": "NixOS",
|
||||
"repo": "nixpkgs",
|
||||
"rev": "6b316287bae2ee04c9b93c8c858d930fd07d7338",
|
||||
"type": "github"
|
||||
},
|
||||
"original": {
|
||||
"owner": "NixOS",
|
||||
"repo": "nixpkgs",
|
||||
"rev": "6b316287bae2ee04c9b93c8c858d930fd07d7338",
|
||||
"type": "github"
|
||||
}
|
||||
},
|
||||
"root": {
|
||||
"inputs": {
|
||||
"nixpkgs": "nixpkgs"
|
||||
}
|
||||
}
|
||||
},
|
||||
"root": "root",
|
||||
"version": 7
|
||||
}
|
||||
@@ -0,0 +1,58 @@
|
||||
{
|
||||
description = "Subtracks - Kotlin/Jetpack Compose Android development shell";
|
||||
|
||||
inputs = {
|
||||
nixpkgs.url = "github:NixOS/nixpkgs/6b316287bae2ee04c9b93c8c858d930fd07d7338";
|
||||
};
|
||||
|
||||
outputs =
|
||||
{ nixpkgs, ... }:
|
||||
let
|
||||
systems = [ "x86_64-linux" ];
|
||||
forEachSystem = nixpkgs.lib.genAttrs systems;
|
||||
in
|
||||
{
|
||||
devShells = forEachSystem (
|
||||
system:
|
||||
let
|
||||
pkgs = import nixpkgs {
|
||||
inherit system;
|
||||
config = {
|
||||
android_sdk.accept_license = true;
|
||||
allowUnfree = true;
|
||||
};
|
||||
};
|
||||
android = pkgs.androidenv.composeAndroidPackages {
|
||||
platformVersions = [ "35" ];
|
||||
buildToolsVersions = [ "latest" ];
|
||||
includeEmulator = false;
|
||||
includeSystemImages = false;
|
||||
includeNDK = false;
|
||||
includeSources = false;
|
||||
includeCmake = false;
|
||||
};
|
||||
androidSdk = android.androidsdk;
|
||||
in
|
||||
{
|
||||
default = pkgs.mkShell {
|
||||
packages = [
|
||||
pkgs.jdk21
|
||||
pkgs.gradle
|
||||
pkgs.git
|
||||
androidSdk
|
||||
android.platform-tools
|
||||
];
|
||||
|
||||
ANDROID_HOME = "${androidSdk}/libexec/android-sdk";
|
||||
ANDROID_SDK_ROOT = "${androidSdk}/libexec/android-sdk";
|
||||
|
||||
shellHook = ''
|
||||
echo "subtracks dev shell"
|
||||
echo " java: $(java -version 2>&1 | head -n1)"
|
||||
echo " ANDROID_HOME: $ANDROID_HOME"
|
||||
'';
|
||||
};
|
||||
}
|
||||
);
|
||||
};
|
||||
}
|
||||
@@ -0,0 +1,9 @@
|
||||
org.gradle.jvmargs=-Xmx4g -Dfile.encoding=UTF-8
|
||||
org.gradle.parallel=true
|
||||
org.gradle.caching=true
|
||||
|
||||
android.useAndroidX=true
|
||||
android.nonTransitiveRClass=true
|
||||
android.builder.sdkDownload=false
|
||||
|
||||
kotlin.code.style=official
|
||||
@@ -0,0 +1,74 @@
|
||||
[versions]
|
||||
agp = "8.7.3"
|
||||
kotlin = "2.1.0"
|
||||
coreKtx = "1.15.0"
|
||||
lifecycle = "2.8.7"
|
||||
activityCompose = "1.9.3"
|
||||
composeBom = "2024.12.01"
|
||||
navigation = "2.8.5"
|
||||
paging = "3.3.5"
|
||||
koin = "4.0.0"
|
||||
coroutines = "1.9.0"
|
||||
serialization = "1.7.3"
|
||||
sqldelight = "2.0.2"
|
||||
okhttp = "4.12.0"
|
||||
retrofit = "2.11.0"
|
||||
coil = "3.0.4"
|
||||
media3 = "1.5.0"
|
||||
datastore = "1.1.1"
|
||||
material = "1.12.0"
|
||||
roborazzi = "1.32.2"
|
||||
robolectric = "4.14.1"
|
||||
junit = "4.13.2"
|
||||
androidxTestExtJunit = "1.2.1"
|
||||
androidxTestCore = "1.6.1"
|
||||
androidxTestRunner = "1.6.2"
|
||||
|
||||
[libraries]
|
||||
androidx-core-ktx = { group = "androidx.core", name = "core-ktx", version.ref = "coreKtx" }
|
||||
androidx-lifecycle-runtime-ktx = { group = "androidx.lifecycle", name = "lifecycle-runtime-ktx", version.ref = "lifecycle" }
|
||||
androidx-lifecycle-viewmodel-compose = { group = "androidx.lifecycle", name = "lifecycle-viewmodel-compose", version.ref = "lifecycle" }
|
||||
androidx-activity-compose = { group = "androidx.activity", name = "activity-compose", version.ref = "activityCompose" }
|
||||
androidx-compose-bom = { group = "androidx.compose", name = "compose-bom", version.ref = "composeBom" }
|
||||
androidx-compose-ui = { group = "androidx.compose.ui", name = "ui" }
|
||||
androidx-compose-ui-graphics = { group = "androidx.compose.ui", name = "ui-graphics" }
|
||||
androidx-compose-ui-tooling = { group = "androidx.compose.ui", name = "ui-tooling" }
|
||||
androidx-compose-ui-tooling-preview = { group = "androidx.compose.ui", name = "ui-tooling-preview" }
|
||||
androidx-compose-material3 = { group = "androidx.compose.material3", name = "material3" }
|
||||
androidx-compose-ui-test-junit4 = { group = "androidx.compose.ui", name = "ui-test-junit4" }
|
||||
androidx-compose-ui-test-manifest = { group = "androidx.compose.ui", name = "ui-test-manifest" }
|
||||
androidx-navigation-compose = { group = "androidx.navigation", name = "navigation-compose", version.ref = "navigation" }
|
||||
androidx-paging-runtime = { group = "androidx.paging", name = "paging-runtime", version.ref = "paging" }
|
||||
androidx-paging-compose = { group = "androidx.paging", name = "paging-compose", version.ref = "paging" }
|
||||
androidx-datastore-preferences = { group = "androidx.datastore", name = "datastore-preferences", version.ref = "datastore" }
|
||||
koin-android = { group = "io.insert-koin", name = "koin-android", version.ref = "koin" }
|
||||
koin-androidx-compose = { group = "io.insert-koin", name = "koin-androidx-compose", version.ref = "koin" }
|
||||
kotlinx-coroutines-android = { group = "org.jetbrains.kotlinx", name = "kotlinx-coroutines-android", version.ref = "coroutines" }
|
||||
kotlinx-serialization-json = { group = "org.jetbrains.kotlinx", name = "kotlinx-serialization-json", version.ref = "serialization" }
|
||||
sqldelight-android-driver = { group = "app.cash.sqldelight", name = "android-driver", version.ref = "sqldelight" }
|
||||
sqldelight-sqlite-driver = { group = "app.cash.sqldelight", name = "sqlite-driver", version.ref = "sqldelight" }
|
||||
okhttp = { group = "com.squareup.okhttp3", name = "okhttp", version.ref = "okhttp" }
|
||||
okhttp-logging = { group = "com.squareup.okhttp3", name = "logging-interceptor", version.ref = "okhttp" }
|
||||
retrofit = { group = "com.squareup.retrofit2", name = "retrofit", version.ref = "retrofit" }
|
||||
coil-compose = { group = "io.coil-kt.coil3", name = "coil-compose", version.ref = "coil" }
|
||||
coil-network-okhttp = { group = "io.coil-kt.coil3", name = "coil-network-okhttp", version.ref = "coil" }
|
||||
media3-exoplayer = { group = "androidx.media3", name = "media3-exoplayer", version.ref = "media3" }
|
||||
media3-session = { group = "androidx.media3", name = "media3-session", version.ref = "media3" }
|
||||
media3-ui = { group = "androidx.media3", name = "media3-ui", version.ref = "media3" }
|
||||
material = { group = "com.google.android.material", name = "material", version.ref = "material" }
|
||||
roborazzi = { group = "io.github.takahirom.roborazzi", name = "roborazzi", version.ref = "roborazzi" }
|
||||
roborazzi-compose = { group = "io.github.takahirom.roborazzi", name = "roborazzi-compose", version.ref = "roborazzi" }
|
||||
roborazzi-junit-rule = { group = "io.github.takahirom.roborazzi", name = "roborazzi-junit-rule", version.ref = "roborazzi" }
|
||||
robolectric = { group = "org.robolectric", name = "robolectric", version.ref = "robolectric" }
|
||||
junit = { group = "junit", name = "junit", version.ref = "junit" }
|
||||
androidx-test-ext-junit = { group = "androidx.test.ext", name = "junit", version.ref = "androidxTestExtJunit" }
|
||||
androidx-test-core = { group = "androidx.test", name = "core", version.ref = "androidxTestCore" }
|
||||
androidx-test-runner = { group = "androidx.test", name = "runner", version.ref = "androidxTestRunner" }
|
||||
|
||||
[plugins]
|
||||
android-application = { id = "com.android.application", version.ref = "agp" }
|
||||
kotlin-android = { id = "org.jetbrains.kotlin.android", version.ref = "kotlin" }
|
||||
kotlin-serialization = { id = "org.jetbrains.kotlin.plugin.serialization", version.ref = "kotlin" }
|
||||
compose-compiler = { id = "org.jetbrains.kotlin.plugin.compose", version.ref = "kotlin" }
|
||||
roborazzi = { id = "io.github.takahirom.roborazzi", version.ref = "roborazzi" }
|
||||
sqldelight = { id = "app.cash.sqldelight", version.ref = "sqldelight" }
|
||||
@@ -1,5 +1,7 @@
|
||||
distributionBase=GRADLE_USER_HOME
|
||||
distributionPath=wrapper/dists
|
||||
distributionUrl=https\://services.gradle.org/distributions/gradle-8.11.1-bin.zip
|
||||
networkTimeout=10000
|
||||
validateDistributionUrl=true
|
||||
zipStoreBase=GRADLE_USER_HOME
|
||||
zipStorePath=wrapper/dists
|
||||
distributionUrl=https\://services.gradle.org/distributions/gradle-7.5-all.zip
|
||||
@@ -0,0 +1,251 @@
|
||||
#!/bin/sh
|
||||
|
||||
#
|
||||
# Copyright © 2015-2021 the original authors.
|
||||
#
|
||||
# Licensed under the Apache License, Version 2.0 (the "License");
|
||||
# you may not use this file except in compliance with the License.
|
||||
# You may obtain a copy of the License at
|
||||
#
|
||||
# https://www.apache.org/licenses/LICENSE-2.0
|
||||
#
|
||||
# Unless required by applicable law or agreed to in writing, software
|
||||
# distributed under the License is distributed on an "AS IS" BASIS,
|
||||
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
# See the License for the specific language governing permissions and
|
||||
# limitations under the License.
|
||||
#
|
||||
# SPDX-License-Identifier: Apache-2.0
|
||||
#
|
||||
|
||||
##############################################################################
|
||||
#
|
||||
# Gradle start up script for POSIX generated by Gradle.
|
||||
#
|
||||
# Important for running:
|
||||
#
|
||||
# (1) You need a POSIX-compliant shell to run this script. If your /bin/sh is
|
||||
# noncompliant, but you have some other compliant shell such as ksh or
|
||||
# bash, then to run this script, type that shell name before the whole
|
||||
# command line, like:
|
||||
#
|
||||
# ksh Gradle
|
||||
#
|
||||
# Busybox and similar reduced shells will NOT work, because this script
|
||||
# requires all of these POSIX shell features:
|
||||
# * functions;
|
||||
# * expansions «$var», «${var}», «${var:-default}», «${var+SET}»,
|
||||
# «${var#prefix}», «${var%suffix}», and «$( cmd )»;
|
||||
# * compound commands having a testable exit status, especially «case»;
|
||||
# * various built-in commands including «command», «set», and «ulimit».
|
||||
#
|
||||
# Important for patching:
|
||||
#
|
||||
# (2) This script targets any POSIX shell, so it avoids extensions provided
|
||||
# by Bash, Ksh, etc; in particular arrays are avoided.
|
||||
#
|
||||
# The "traditional" practice of packing multiple parameters into a
|
||||
# space-separated string is a well documented source of bugs and security
|
||||
# problems, so this is (mostly) avoided, by progressively accumulating
|
||||
# options in "$@", and eventually passing that to Java.
|
||||
#
|
||||
# Where the inherited environment variables (DEFAULT_JVM_OPTS, JAVA_OPTS,
|
||||
# and GRADLE_OPTS) rely on word-splitting, this is performed explicitly;
|
||||
# see the in-line comments for details.
|
||||
#
|
||||
# There are tweaks for specific operating systems such as AIX, CygWin,
|
||||
# Darwin, MinGW, and NonStop.
|
||||
#
|
||||
# (3) This script is generated from the Groovy template
|
||||
# https://github.com/gradle/gradle/blob/HEAD/platforms/jvm/plugins-application/src/main/resources/org/gradle/api/internal/plugins/unixStartScript.txt
|
||||
# within the Gradle project.
|
||||
#
|
||||
# You can find Gradle at https://github.com/gradle/gradle/.
|
||||
#
|
||||
##############################################################################
|
||||
|
||||
# Attempt to set APP_HOME
|
||||
|
||||
# Resolve links: $0 may be a link
|
||||
app_path=$0
|
||||
|
||||
# Need this for daisy-chained symlinks.
|
||||
while
|
||||
APP_HOME=${app_path%"${app_path##*/}"} # leaves a trailing /; empty if no leading path
|
||||
[ -h "$app_path" ]
|
||||
do
|
||||
ls=$( ls -ld "$app_path" )
|
||||
link=${ls#*' -> '}
|
||||
case $link in #(
|
||||
/*) app_path=$link ;; #(
|
||||
*) app_path=$APP_HOME$link ;;
|
||||
esac
|
||||
done
|
||||
|
||||
# This is normally unused
|
||||
# shellcheck disable=SC2034
|
||||
APP_BASE_NAME=${0##*/}
|
||||
# Discard cd standard output in case $CDPATH is set (https://github.com/gradle/gradle/issues/25036)
|
||||
APP_HOME=$( cd -P "${APP_HOME:-./}" > /dev/null && printf '%s\n' "$PWD" ) || exit
|
||||
|
||||
# Use the maximum available, or set MAX_FD != -1 to use that value.
|
||||
MAX_FD=maximum
|
||||
|
||||
warn () {
|
||||
echo "$*"
|
||||
} >&2
|
||||
|
||||
die () {
|
||||
echo
|
||||
echo "$*"
|
||||
echo
|
||||
exit 1
|
||||
} >&2
|
||||
|
||||
# OS specific support (must be 'true' or 'false').
|
||||
cygwin=false
|
||||
msys=false
|
||||
darwin=false
|
||||
nonstop=false
|
||||
case "$( uname )" in #(
|
||||
CYGWIN* ) cygwin=true ;; #(
|
||||
Darwin* ) darwin=true ;; #(
|
||||
MSYS* | MINGW* ) msys=true ;; #(
|
||||
NONSTOP* ) nonstop=true ;;
|
||||
esac
|
||||
|
||||
CLASSPATH="\\\"\\\""
|
||||
|
||||
|
||||
# Determine the Java command to use to start the JVM.
|
||||
if [ -n "$JAVA_HOME" ] ; then
|
||||
if [ -x "$JAVA_HOME/jre/sh/java" ] ; then
|
||||
# IBM's JDK on AIX uses strange locations for the executables
|
||||
JAVACMD=$JAVA_HOME/jre/sh/java
|
||||
else
|
||||
JAVACMD=$JAVA_HOME/bin/java
|
||||
fi
|
||||
if [ ! -x "$JAVACMD" ] ; then
|
||||
die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME
|
||||
|
||||
Please set the JAVA_HOME variable in your environment to match the
|
||||
location of your Java installation."
|
||||
fi
|
||||
else
|
||||
JAVACMD=java
|
||||
if ! command -v java >/dev/null 2>&1
|
||||
then
|
||||
die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.
|
||||
|
||||
Please set the JAVA_HOME variable in your environment to match the
|
||||
location of your Java installation."
|
||||
fi
|
||||
fi
|
||||
|
||||
# Increase the maximum file descriptors if we can.
|
||||
if ! "$cygwin" && ! "$darwin" && ! "$nonstop" ; then
|
||||
case $MAX_FD in #(
|
||||
max*)
|
||||
# In POSIX sh, ulimit -H is undefined. That's why the result is checked to see if it worked.
|
||||
# shellcheck disable=SC2039,SC3045
|
||||
MAX_FD=$( ulimit -H -n ) ||
|
||||
warn "Could not query maximum file descriptor limit"
|
||||
esac
|
||||
case $MAX_FD in #(
|
||||
'' | soft) :;; #(
|
||||
*)
|
||||
# In POSIX sh, ulimit -n is undefined. That's why the result is checked to see if it worked.
|
||||
# shellcheck disable=SC2039,SC3045
|
||||
ulimit -n "$MAX_FD" ||
|
||||
warn "Could not set maximum file descriptor limit to $MAX_FD"
|
||||
esac
|
||||
fi
|
||||
|
||||
# Collect all arguments for the java command, stacking in reverse order:
|
||||
# * args from the command line
|
||||
# * the main class name
|
||||
# * -classpath
|
||||
# * -D...appname settings
|
||||
# * --module-path (only if needed)
|
||||
# * DEFAULT_JVM_OPTS, JAVA_OPTS, and GRADLE_OPTS environment variables.
|
||||
|
||||
# For Cygwin or MSYS, switch paths to Windows format before running java
|
||||
if "$cygwin" || "$msys" ; then
|
||||
APP_HOME=$( cygpath --path --mixed "$APP_HOME" )
|
||||
CLASSPATH=$( cygpath --path --mixed "$CLASSPATH" )
|
||||
|
||||
JAVACMD=$( cygpath --unix "$JAVACMD" )
|
||||
|
||||
# Now convert the arguments - kludge to limit ourselves to /bin/sh
|
||||
for arg do
|
||||
if
|
||||
case $arg in #(
|
||||
-*) false ;; # don't mess with options #(
|
||||
/?*) t=${arg#/} t=/${t%%/*} # looks like a POSIX filepath
|
||||
[ -e "$t" ] ;; #(
|
||||
*) false ;;
|
||||
esac
|
||||
then
|
||||
arg=$( cygpath --path --ignore --mixed "$arg" )
|
||||
fi
|
||||
# Roll the args list around exactly as many times as the number of
|
||||
# args, so each arg winds up back in the position where it started, but
|
||||
# possibly modified.
|
||||
#
|
||||
# NB: a `for` loop captures its iteration list before it begins, so
|
||||
# changing the positional parameters here affects neither the number of
|
||||
# iterations, nor the values presented in `arg`.
|
||||
shift # remove old arg
|
||||
set -- "$@" "$arg" # push replacement arg
|
||||
done
|
||||
fi
|
||||
|
||||
|
||||
# Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
|
||||
DEFAULT_JVM_OPTS='"-Xmx64m" "-Xms64m"'
|
||||
|
||||
# Collect all arguments for the java command:
|
||||
# * DEFAULT_JVM_OPTS, JAVA_OPTS, and optsEnvironmentVar are not allowed to contain shell fragments,
|
||||
# and any embedded shellness will be escaped.
|
||||
# * For example: A user cannot expect ${Hostname} to be expanded, as it is an environment variable and will be
|
||||
# treated as '${Hostname}' itself on the command line.
|
||||
|
||||
set -- \
|
||||
"-Dorg.gradle.appname=$APP_BASE_NAME" \
|
||||
-classpath "$CLASSPATH" \
|
||||
-jar "$APP_HOME/gradle/wrapper/gradle-wrapper.jar" \
|
||||
"$@"
|
||||
|
||||
# Stop when "xargs" is not available.
|
||||
if ! command -v xargs >/dev/null 2>&1
|
||||
then
|
||||
die "xargs is not available"
|
||||
fi
|
||||
|
||||
# Use "xargs" to parse quoted args.
|
||||
#
|
||||
# With -n1 it outputs one arg per line, with the quotes and backslashes removed.
|
||||
#
|
||||
# In Bash we could simply go:
|
||||
#
|
||||
# readarray ARGS < <( xargs -n1 <<<"$var" ) &&
|
||||
# set -- "${ARGS[@]}" "$@"
|
||||
#
|
||||
# but POSIX shell has neither arrays nor command substitution, so instead we
|
||||
# post-process each arg (as a line of input to sed) to backslash-escape any
|
||||
# character that might be a shell metacharacter, then use eval to reverse
|
||||
# that process (while maintaining the separation between arguments), and wrap
|
||||
# the whole thing up as a single "set" statement.
|
||||
#
|
||||
# This will of course break if any of these variables contains a newline or
|
||||
# an unmatched quote.
|
||||
#
|
||||
|
||||
eval "set -- $(
|
||||
printf '%s\n' "$DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS" |
|
||||
xargs -n1 |
|
||||
sed ' s~[^-[:alnum:]+,./:=@_]~\\&~g; ' |
|
||||
tr '\n' ' '
|
||||
)" '"$@"'
|
||||
|
||||
exec "$JAVACMD" "$@"
|
||||
@@ -0,0 +1,94 @@
|
||||
@rem
|
||||
@rem Copyright 2015 the original author or authors.
|
||||
@rem
|
||||
@rem Licensed under the Apache License, Version 2.0 (the "License");
|
||||
@rem you may not use this file except in compliance with the License.
|
||||
@rem You may obtain a copy of the License at
|
||||
@rem
|
||||
@rem https://www.apache.org/licenses/LICENSE-2.0
|
||||
@rem
|
||||
@rem Unless required by applicable law or agreed to in writing, software
|
||||
@rem distributed under the License is distributed on an "AS IS" BASIS,
|
||||
@rem WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
@rem See the License for the specific language governing permissions and
|
||||
@rem limitations under the License.
|
||||
@rem
|
||||
@rem SPDX-License-Identifier: Apache-2.0
|
||||
@rem
|
||||
|
||||
@if "%DEBUG%"=="" @echo off
|
||||
@rem ##########################################################################
|
||||
@rem
|
||||
@rem Gradle startup script for Windows
|
||||
@rem
|
||||
@rem ##########################################################################
|
||||
|
||||
@rem Set local scope for the variables with windows NT shell
|
||||
if "%OS%"=="Windows_NT" setlocal
|
||||
|
||||
set DIRNAME=%~dp0
|
||||
if "%DIRNAME%"=="" set DIRNAME=.
|
||||
@rem This is normally unused
|
||||
set APP_BASE_NAME=%~n0
|
||||
set APP_HOME=%DIRNAME%
|
||||
|
||||
@rem Resolve any "." and ".." in APP_HOME to make it shorter.
|
||||
for %%i in ("%APP_HOME%") do set APP_HOME=%%~fi
|
||||
|
||||
@rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
|
||||
set DEFAULT_JVM_OPTS="-Xmx64m" "-Xms64m"
|
||||
|
||||
@rem Find java.exe
|
||||
if defined JAVA_HOME goto findJavaFromJavaHome
|
||||
|
||||
set JAVA_EXE=java.exe
|
||||
%JAVA_EXE% -version >NUL 2>&1
|
||||
if %ERRORLEVEL% equ 0 goto execute
|
||||
|
||||
echo. 1>&2
|
||||
echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. 1>&2
|
||||
echo. 1>&2
|
||||
echo Please set the JAVA_HOME variable in your environment to match the 1>&2
|
||||
echo location of your Java installation. 1>&2
|
||||
|
||||
goto fail
|
||||
|
||||
:findJavaFromJavaHome
|
||||
set JAVA_HOME=%JAVA_HOME:"=%
|
||||
set JAVA_EXE=%JAVA_HOME%/bin/java.exe
|
||||
|
||||
if exist "%JAVA_EXE%" goto execute
|
||||
|
||||
echo. 1>&2
|
||||
echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% 1>&2
|
||||
echo. 1>&2
|
||||
echo Please set the JAVA_HOME variable in your environment to match the 1>&2
|
||||
echo location of your Java installation. 1>&2
|
||||
|
||||
goto fail
|
||||
|
||||
:execute
|
||||
@rem Setup the command line
|
||||
|
||||
set CLASSPATH=
|
||||
|
||||
|
||||
@rem Execute Gradle
|
||||
"%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" -jar "%APP_HOME%\gradle\wrapper\gradle-wrapper.jar" %*
|
||||
|
||||
:end
|
||||
@rem End local scope for the variables with windows NT shell
|
||||
if %ERRORLEVEL% equ 0 goto mainEnd
|
||||
|
||||
:fail
|
||||
rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of
|
||||
rem the _cmd.exe /c_ return code!
|
||||
set EXIT_CODE=%ERRORLEVEL%
|
||||
if %EXIT_CODE% equ 0 set EXIT_CODE=1
|
||||
if not ""=="%GRADLE_EXIT_CONSOLE%" exit %EXIT_CODE%
|
||||
exit /b %EXIT_CODE%
|
||||
|
||||
:mainEnd
|
||||
if "%OS%"=="Windows_NT" endlocal
|
||||
|
||||
:omega
|
||||
@@ -1,4 +0,0 @@
|
||||
arb-dir: lib/l10n
|
||||
template-arb-file: app_en.arb
|
||||
nullable-getter: false
|
||||
untranslated-messages-file: .untranslated-messages.json
|
||||
@@ -1,85 +0,0 @@
|
||||
import 'package:fast_immutable_collections/fast_immutable_collections.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_gen/gen_l10n/app_localizations.dart';
|
||||
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
||||
import 'package:riverpod_annotation/riverpod_annotation.dart';
|
||||
|
||||
import '../database/database.dart';
|
||||
import '../services/settings_service.dart';
|
||||
import '../state/init.dart';
|
||||
import '../state/theme.dart';
|
||||
|
||||
part 'app.g.dart';
|
||||
|
||||
class MyApp extends HookConsumerWidget {
|
||||
const MyApp({super.key});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context, WidgetRef ref) {
|
||||
final init = ref.watch(initProvider);
|
||||
return init.when(
|
||||
data: (_) => const App(),
|
||||
error: (e, s) => Directionality(
|
||||
textDirection: TextDirection.ltr,
|
||||
child: Container(
|
||||
color: Colors.red[900],
|
||||
child: Column(children: [
|
||||
const SizedBox(height: 100),
|
||||
Text(e.toString()),
|
||||
Text(s.toString()),
|
||||
]),
|
||||
),
|
||||
),
|
||||
loading: () => const CircularProgressIndicator(),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@Riverpod(keepAlive: true)
|
||||
class LastPath extends _$LastPath {
|
||||
@override
|
||||
String build() {
|
||||
return '/settings';
|
||||
}
|
||||
|
||||
Future<void> init() async {
|
||||
final db = ref.read(databaseProvider);
|
||||
final lastBottomNav = await db.getLastBottomNavState().getSingleOrNull();
|
||||
final lastLibrary = await db.getLastLibraryState().getSingleOrNull();
|
||||
|
||||
if (lastBottomNav == null || lastLibrary == null) return;
|
||||
|
||||
// TODO: replace this with a proper first-time setup flow
|
||||
final hasActiveSource = ref.read(settingsServiceProvider.select(
|
||||
(value) => value.activeSource != null,
|
||||
));
|
||||
if (!hasActiveSource) return;
|
||||
|
||||
state = lastBottomNav.tab == 'library'
|
||||
? '/library/${lastLibrary.tab}'
|
||||
: '/${lastBottomNav.tab}';
|
||||
}
|
||||
}
|
||||
|
||||
class App extends HookConsumerWidget {
|
||||
const App({super.key});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context, WidgetRef ref) {
|
||||
final appRouter = ref.watch(routerProvider);
|
||||
final base = ref.watch(baseThemeProvider);
|
||||
final lastPath = ref.watch(lastPathProvider);
|
||||
|
||||
return MaterialApp.router(
|
||||
theme: base.theme,
|
||||
debugShowCheckedModeBanner: false,
|
||||
routerDelegate: appRouter.delegate(
|
||||
initialDeepLink: lastPath,
|
||||
),
|
||||
routeInformationParser: appRouter.defaultRouteParser(),
|
||||
localizationsDelegates: AppLocalizations.localizationsDelegates,
|
||||
supportedLocales: [...AppLocalizations.supportedLocales]
|
||||
..moveToTheFront(const Locale('en')),
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -1,23 +0,0 @@
|
||||
// GENERATED CODE - DO NOT MODIFY BY HAND
|
||||
|
||||
part of 'app.dart';
|
||||
|
||||
// **************************************************************************
|
||||
// RiverpodGenerator
|
||||
// **************************************************************************
|
||||
|
||||
String _$lastPathHash() => r'25ba5df6bd984fcce011eec40a12fb74627a790a';
|
||||
|
||||
/// See also [LastPath].
|
||||
@ProviderFor(LastPath)
|
||||
final lastPathProvider = NotifierProvider<LastPath, String>.internal(
|
||||
LastPath.new,
|
||||
name: r'lastPathProvider',
|
||||
debugGetCreateSourceHash:
|
||||
const bool.fromEnvironment('dart.vm.product') ? null : _$lastPathHash,
|
||||
dependencies: null,
|
||||
allTransitiveDependencies: null,
|
||||
);
|
||||
|
||||
typedef _$LastPath = Notifier<String>;
|
||||
// ignore_for_file: unnecessary_raw_strings, subtype_of_sealed_class, invalid_use_of_internal_member, do_not_use_environment, prefer_const_constructors, public_member_api_docs, avoid_private_typedef_functions
|
||||
@@ -1,140 +0,0 @@
|
||||
// ignore_for_file: use_key_in_widget_constructors
|
||||
|
||||
import 'dart:async';
|
||||
|
||||
import 'package:auto_route/auto_route.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
import 'pages/artist_page.dart';
|
||||
import 'pages/bottom_nav_page.dart';
|
||||
import 'pages/browse_page.dart';
|
||||
import 'pages/library_albums_page.dart';
|
||||
import 'pages/library_artists_page.dart';
|
||||
import 'pages/library_page.dart';
|
||||
import 'pages/library_playlists_page.dart';
|
||||
import 'pages/library_songs_page.dart';
|
||||
import 'pages/now_playing_page.dart';
|
||||
import 'pages/search_page.dart';
|
||||
import 'pages/settings_page.dart';
|
||||
import 'pages/songs_page.dart';
|
||||
import 'pages/source_page.dart';
|
||||
|
||||
part 'app_router.gr.dart';
|
||||
|
||||
const kCustomTransitionBuilder = TransitionsBuilders.slideRightWithFade;
|
||||
const kCustomTransitionDuration = 160;
|
||||
|
||||
const itemRoutes = [
|
||||
CustomRoute(
|
||||
path: 'album/:id',
|
||||
page: AlbumSongsPage,
|
||||
transitionsBuilder: kCustomTransitionBuilder,
|
||||
durationInMilliseconds: kCustomTransitionDuration,
|
||||
reverseDurationInMilliseconds: kCustomTransitionDuration,
|
||||
),
|
||||
CustomRoute(
|
||||
path: 'artist/:id',
|
||||
page: ArtistPage,
|
||||
transitionsBuilder: kCustomTransitionBuilder,
|
||||
durationInMilliseconds: kCustomTransitionDuration,
|
||||
reverseDurationInMilliseconds: kCustomTransitionDuration,
|
||||
),
|
||||
CustomRoute(
|
||||
path: 'playlist/:id',
|
||||
page: PlaylistSongsPage,
|
||||
transitionsBuilder: kCustomTransitionBuilder,
|
||||
durationInMilliseconds: kCustomTransitionDuration,
|
||||
reverseDurationInMilliseconds: kCustomTransitionDuration,
|
||||
),
|
||||
CustomRoute(
|
||||
path: 'genre/:genre',
|
||||
page: GenreSongsPage,
|
||||
transitionsBuilder: kCustomTransitionBuilder,
|
||||
durationInMilliseconds: kCustomTransitionDuration,
|
||||
reverseDurationInMilliseconds: kCustomTransitionDuration,
|
||||
),
|
||||
];
|
||||
|
||||
class EmptyRouterPage extends AutoRouter {
|
||||
const EmptyRouterPage({Key? key})
|
||||
: super(
|
||||
key: key,
|
||||
inheritNavigatorObservers: false,
|
||||
);
|
||||
}
|
||||
|
||||
@MaterialAutoRouter(
|
||||
replaceInRouteName: 'Page,Route',
|
||||
routes: <AutoRoute>[
|
||||
AutoRoute(path: '/', name: 'RootRouter', page: EmptyRouterPage, children: [
|
||||
AutoRoute(path: '', page: BottomNavTabsPage, children: [
|
||||
AutoRoute(
|
||||
path: 'library',
|
||||
name: 'LibraryRouter',
|
||||
page: EmptyRouterPage,
|
||||
children: [
|
||||
AutoRoute(path: '', page: LibraryTabsPage, children: [
|
||||
AutoRoute(path: 'albums', page: LibraryAlbumsPage),
|
||||
AutoRoute(path: 'artists', page: LibraryArtistsPage),
|
||||
AutoRoute(path: 'playlists', page: LibraryPlaylistsPage),
|
||||
AutoRoute(path: 'songs', page: LibrarySongsPage),
|
||||
]),
|
||||
...itemRoutes,
|
||||
]),
|
||||
AutoRoute(
|
||||
path: 'browse',
|
||||
name: 'BrowseRouter',
|
||||
page: EmptyRouterPage,
|
||||
children: [
|
||||
AutoRoute(path: '', page: BrowsePage),
|
||||
...itemRoutes,
|
||||
]),
|
||||
AutoRoute(
|
||||
path: 'search',
|
||||
name: 'SearchRouter',
|
||||
page: EmptyRouterPage,
|
||||
children: [
|
||||
AutoRoute(path: '', page: SearchPage),
|
||||
...itemRoutes,
|
||||
]),
|
||||
AutoRoute(
|
||||
path: 'settings',
|
||||
name: 'SettingsRouter',
|
||||
page: EmptyRouterPage,
|
||||
children: [
|
||||
AutoRoute(path: '', page: SettingsPage),
|
||||
CustomRoute(
|
||||
path: 'source/:id',
|
||||
page: SourcePage,
|
||||
transitionsBuilder: kCustomTransitionBuilder,
|
||||
durationInMilliseconds: kCustomTransitionDuration,
|
||||
reverseDurationInMilliseconds: kCustomTransitionDuration,
|
||||
),
|
||||
]),
|
||||
]),
|
||||
]),
|
||||
CustomRoute(
|
||||
path: '/now-playing',
|
||||
page: NowPlayingPage,
|
||||
transitionsBuilder: TransitionsBuilders.slideBottom,
|
||||
durationInMilliseconds: 200,
|
||||
reverseDurationInMilliseconds: 160,
|
||||
),
|
||||
],
|
||||
)
|
||||
class AppRouter extends _$AppRouter {}
|
||||
|
||||
class TabObserver extends AutoRouterObserver {
|
||||
final StreamController<String> _controller = StreamController.broadcast();
|
||||
Stream<String> get path => _controller.stream;
|
||||
|
||||
@override
|
||||
void didInitTabRoute(TabPageRoute route, TabPageRoute? previousRoute) {
|
||||
_controller.add(route.path);
|
||||
}
|
||||
|
||||
@override
|
||||
void didChangeTabRoute(TabPageRoute route, TabPageRoute previousRoute) {
|
||||
_controller.add(route.path);
|
||||
}
|
||||
}
|
||||
@@ -1,720 +0,0 @@
|
||||
// **************************************************************************
|
||||
// AutoRouteGenerator
|
||||
// **************************************************************************
|
||||
|
||||
// GENERATED CODE - DO NOT MODIFY BY HAND
|
||||
|
||||
// **************************************************************************
|
||||
// AutoRouteGenerator
|
||||
// **************************************************************************
|
||||
//
|
||||
// ignore_for_file: type=lint
|
||||
|
||||
part of 'app_router.dart';
|
||||
|
||||
class _$AppRouter extends RootStackRouter {
|
||||
_$AppRouter([GlobalKey<NavigatorState>? navigatorKey]) : super(navigatorKey);
|
||||
|
||||
@override
|
||||
final Map<String, PageFactory> pagesMap = {
|
||||
RootRouter.name: (routeData) {
|
||||
return MaterialPageX<dynamic>(
|
||||
routeData: routeData,
|
||||
child: const EmptyRouterPage(),
|
||||
);
|
||||
},
|
||||
NowPlayingRoute.name: (routeData) {
|
||||
return CustomPage<dynamic>(
|
||||
routeData: routeData,
|
||||
child: const NowPlayingPage(),
|
||||
transitionsBuilder: TransitionsBuilders.slideBottom,
|
||||
durationInMilliseconds: 200,
|
||||
reverseDurationInMilliseconds: 160,
|
||||
opaque: true,
|
||||
barrierDismissible: false,
|
||||
);
|
||||
},
|
||||
BottomNavTabsRoute.name: (routeData) {
|
||||
return MaterialPageX<dynamic>(
|
||||
routeData: routeData,
|
||||
child: const BottomNavTabsPage(),
|
||||
);
|
||||
},
|
||||
LibraryRouter.name: (routeData) {
|
||||
return MaterialPageX<dynamic>(
|
||||
routeData: routeData,
|
||||
child: const EmptyRouterPage(),
|
||||
);
|
||||
},
|
||||
BrowseRouter.name: (routeData) {
|
||||
return MaterialPageX<dynamic>(
|
||||
routeData: routeData,
|
||||
child: const EmptyRouterPage(),
|
||||
);
|
||||
},
|
||||
SearchRouter.name: (routeData) {
|
||||
return MaterialPageX<dynamic>(
|
||||
routeData: routeData,
|
||||
child: const EmptyRouterPage(),
|
||||
);
|
||||
},
|
||||
SettingsRouter.name: (routeData) {
|
||||
return MaterialPageX<dynamic>(
|
||||
routeData: routeData,
|
||||
child: const EmptyRouterPage(),
|
||||
);
|
||||
},
|
||||
LibraryTabsRoute.name: (routeData) {
|
||||
return MaterialPageX<dynamic>(
|
||||
routeData: routeData,
|
||||
child: const LibraryTabsPage(),
|
||||
);
|
||||
},
|
||||
AlbumSongsRoute.name: (routeData) {
|
||||
final pathParams = routeData.inheritedPathParams;
|
||||
final args = routeData.argsAs<AlbumSongsRouteArgs>(
|
||||
orElse: () => AlbumSongsRouteArgs(id: pathParams.getString('id')));
|
||||
return CustomPage<dynamic>(
|
||||
routeData: routeData,
|
||||
child: AlbumSongsPage(
|
||||
key: args.key,
|
||||
id: args.id,
|
||||
),
|
||||
transitionsBuilder: TransitionsBuilders.slideRightWithFade,
|
||||
durationInMilliseconds: 160,
|
||||
reverseDurationInMilliseconds: 160,
|
||||
opaque: true,
|
||||
barrierDismissible: false,
|
||||
);
|
||||
},
|
||||
ArtistRoute.name: (routeData) {
|
||||
final pathParams = routeData.inheritedPathParams;
|
||||
final args = routeData.argsAs<ArtistRouteArgs>(
|
||||
orElse: () => ArtistRouteArgs(id: pathParams.getString('id')));
|
||||
return CustomPage<dynamic>(
|
||||
routeData: routeData,
|
||||
child: ArtistPage(
|
||||
key: args.key,
|
||||
id: args.id,
|
||||
),
|
||||
transitionsBuilder: TransitionsBuilders.slideRightWithFade,
|
||||
durationInMilliseconds: 160,
|
||||
reverseDurationInMilliseconds: 160,
|
||||
opaque: true,
|
||||
barrierDismissible: false,
|
||||
);
|
||||
},
|
||||
PlaylistSongsRoute.name: (routeData) {
|
||||
final pathParams = routeData.inheritedPathParams;
|
||||
final args = routeData.argsAs<PlaylistSongsRouteArgs>(
|
||||
orElse: () => PlaylistSongsRouteArgs(id: pathParams.getString('id')));
|
||||
return CustomPage<dynamic>(
|
||||
routeData: routeData,
|
||||
child: PlaylistSongsPage(
|
||||
key: args.key,
|
||||
id: args.id,
|
||||
),
|
||||
transitionsBuilder: TransitionsBuilders.slideRightWithFade,
|
||||
durationInMilliseconds: 160,
|
||||
reverseDurationInMilliseconds: 160,
|
||||
opaque: true,
|
||||
barrierDismissible: false,
|
||||
);
|
||||
},
|
||||
GenreSongsRoute.name: (routeData) {
|
||||
final pathParams = routeData.inheritedPathParams;
|
||||
final args = routeData.argsAs<GenreSongsRouteArgs>(
|
||||
orElse: () =>
|
||||
GenreSongsRouteArgs(genre: pathParams.getString('genre')));
|
||||
return CustomPage<dynamic>(
|
||||
routeData: routeData,
|
||||
child: GenreSongsPage(
|
||||
key: args.key,
|
||||
genre: args.genre,
|
||||
),
|
||||
transitionsBuilder: TransitionsBuilders.slideRightWithFade,
|
||||
durationInMilliseconds: 160,
|
||||
reverseDurationInMilliseconds: 160,
|
||||
opaque: true,
|
||||
barrierDismissible: false,
|
||||
);
|
||||
},
|
||||
LibraryAlbumsRoute.name: (routeData) {
|
||||
return MaterialPageX<dynamic>(
|
||||
routeData: routeData,
|
||||
child: const LibraryAlbumsPage(),
|
||||
);
|
||||
},
|
||||
LibraryArtistsRoute.name: (routeData) {
|
||||
return MaterialPageX<dynamic>(
|
||||
routeData: routeData,
|
||||
child: const LibraryArtistsPage(),
|
||||
);
|
||||
},
|
||||
LibraryPlaylistsRoute.name: (routeData) {
|
||||
return MaterialPageX<dynamic>(
|
||||
routeData: routeData,
|
||||
child: const LibraryPlaylistsPage(),
|
||||
);
|
||||
},
|
||||
LibrarySongsRoute.name: (routeData) {
|
||||
return MaterialPageX<dynamic>(
|
||||
routeData: routeData,
|
||||
child: const LibrarySongsPage(),
|
||||
);
|
||||
},
|
||||
BrowseRoute.name: (routeData) {
|
||||
return MaterialPageX<dynamic>(
|
||||
routeData: routeData,
|
||||
child: const BrowsePage(),
|
||||
);
|
||||
},
|
||||
SearchRoute.name: (routeData) {
|
||||
return MaterialPageX<dynamic>(
|
||||
routeData: routeData,
|
||||
child: const SearchPage(),
|
||||
);
|
||||
},
|
||||
SettingsRoute.name: (routeData) {
|
||||
return MaterialPageX<dynamic>(
|
||||
routeData: routeData,
|
||||
child: const SettingsPage(),
|
||||
);
|
||||
},
|
||||
SourceRoute.name: (routeData) {
|
||||
final pathParams = routeData.inheritedPathParams;
|
||||
final args = routeData.argsAs<SourceRouteArgs>(
|
||||
orElse: () => SourceRouteArgs(id: pathParams.optInt('id')));
|
||||
return CustomPage<dynamic>(
|
||||
routeData: routeData,
|
||||
child: SourcePage(
|
||||
key: args.key,
|
||||
id: args.id,
|
||||
),
|
||||
transitionsBuilder: TransitionsBuilders.slideRightWithFade,
|
||||
durationInMilliseconds: 160,
|
||||
reverseDurationInMilliseconds: 160,
|
||||
opaque: true,
|
||||
barrierDismissible: false,
|
||||
);
|
||||
},
|
||||
};
|
||||
|
||||
@override
|
||||
List<RouteConfig> get routes => [
|
||||
RouteConfig(
|
||||
RootRouter.name,
|
||||
path: '/',
|
||||
children: [
|
||||
RouteConfig(
|
||||
BottomNavTabsRoute.name,
|
||||
path: '',
|
||||
parent: RootRouter.name,
|
||||
children: [
|
||||
RouteConfig(
|
||||
LibraryRouter.name,
|
||||
path: 'library',
|
||||
parent: BottomNavTabsRoute.name,
|
||||
children: [
|
||||
RouteConfig(
|
||||
LibraryTabsRoute.name,
|
||||
path: '',
|
||||
parent: LibraryRouter.name,
|
||||
children: [
|
||||
RouteConfig(
|
||||
LibraryAlbumsRoute.name,
|
||||
path: 'albums',
|
||||
parent: LibraryTabsRoute.name,
|
||||
),
|
||||
RouteConfig(
|
||||
LibraryArtistsRoute.name,
|
||||
path: 'artists',
|
||||
parent: LibraryTabsRoute.name,
|
||||
),
|
||||
RouteConfig(
|
||||
LibraryPlaylistsRoute.name,
|
||||
path: 'playlists',
|
||||
parent: LibraryTabsRoute.name,
|
||||
),
|
||||
RouteConfig(
|
||||
LibrarySongsRoute.name,
|
||||
path: 'songs',
|
||||
parent: LibraryTabsRoute.name,
|
||||
),
|
||||
],
|
||||
),
|
||||
RouteConfig(
|
||||
AlbumSongsRoute.name,
|
||||
path: 'album/:id',
|
||||
parent: LibraryRouter.name,
|
||||
),
|
||||
RouteConfig(
|
||||
ArtistRoute.name,
|
||||
path: 'artist/:id',
|
||||
parent: LibraryRouter.name,
|
||||
),
|
||||
RouteConfig(
|
||||
PlaylistSongsRoute.name,
|
||||
path: 'playlist/:id',
|
||||
parent: LibraryRouter.name,
|
||||
),
|
||||
RouteConfig(
|
||||
GenreSongsRoute.name,
|
||||
path: 'genre/:genre',
|
||||
parent: LibraryRouter.name,
|
||||
),
|
||||
],
|
||||
),
|
||||
RouteConfig(
|
||||
BrowseRouter.name,
|
||||
path: 'browse',
|
||||
parent: BottomNavTabsRoute.name,
|
||||
children: [
|
||||
RouteConfig(
|
||||
BrowseRoute.name,
|
||||
path: '',
|
||||
parent: BrowseRouter.name,
|
||||
),
|
||||
RouteConfig(
|
||||
AlbumSongsRoute.name,
|
||||
path: 'album/:id',
|
||||
parent: BrowseRouter.name,
|
||||
),
|
||||
RouteConfig(
|
||||
ArtistRoute.name,
|
||||
path: 'artist/:id',
|
||||
parent: BrowseRouter.name,
|
||||
),
|
||||
RouteConfig(
|
||||
PlaylistSongsRoute.name,
|
||||
path: 'playlist/:id',
|
||||
parent: BrowseRouter.name,
|
||||
),
|
||||
RouteConfig(
|
||||
GenreSongsRoute.name,
|
||||
path: 'genre/:genre',
|
||||
parent: BrowseRouter.name,
|
||||
),
|
||||
],
|
||||
),
|
||||
RouteConfig(
|
||||
SearchRouter.name,
|
||||
path: 'search',
|
||||
parent: BottomNavTabsRoute.name,
|
||||
children: [
|
||||
RouteConfig(
|
||||
SearchRoute.name,
|
||||
path: '',
|
||||
parent: SearchRouter.name,
|
||||
),
|
||||
RouteConfig(
|
||||
AlbumSongsRoute.name,
|
||||
path: 'album/:id',
|
||||
parent: SearchRouter.name,
|
||||
),
|
||||
RouteConfig(
|
||||
ArtistRoute.name,
|
||||
path: 'artist/:id',
|
||||
parent: SearchRouter.name,
|
||||
),
|
||||
RouteConfig(
|
||||
PlaylistSongsRoute.name,
|
||||
path: 'playlist/:id',
|
||||
parent: SearchRouter.name,
|
||||
),
|
||||
RouteConfig(
|
||||
GenreSongsRoute.name,
|
||||
path: 'genre/:genre',
|
||||
parent: SearchRouter.name,
|
||||
),
|
||||
],
|
||||
),
|
||||
RouteConfig(
|
||||
SettingsRouter.name,
|
||||
path: 'settings',
|
||||
parent: BottomNavTabsRoute.name,
|
||||
children: [
|
||||
RouteConfig(
|
||||
SettingsRoute.name,
|
||||
path: '',
|
||||
parent: SettingsRouter.name,
|
||||
),
|
||||
RouteConfig(
|
||||
SourceRoute.name,
|
||||
path: 'source/:id',
|
||||
parent: SettingsRouter.name,
|
||||
),
|
||||
],
|
||||
),
|
||||
],
|
||||
)
|
||||
],
|
||||
),
|
||||
RouteConfig(
|
||||
NowPlayingRoute.name,
|
||||
path: '/now-playing',
|
||||
),
|
||||
];
|
||||
}
|
||||
|
||||
/// generated route for
|
||||
/// [EmptyRouterPage]
|
||||
class RootRouter extends PageRouteInfo<void> {
|
||||
const RootRouter({List<PageRouteInfo>? children})
|
||||
: super(
|
||||
RootRouter.name,
|
||||
path: '/',
|
||||
initialChildren: children,
|
||||
);
|
||||
|
||||
static const String name = 'RootRouter';
|
||||
}
|
||||
|
||||
/// generated route for
|
||||
/// [NowPlayingPage]
|
||||
class NowPlayingRoute extends PageRouteInfo<void> {
|
||||
const NowPlayingRoute()
|
||||
: super(
|
||||
NowPlayingRoute.name,
|
||||
path: '/now-playing',
|
||||
);
|
||||
|
||||
static const String name = 'NowPlayingRoute';
|
||||
}
|
||||
|
||||
/// generated route for
|
||||
/// [BottomNavTabsPage]
|
||||
class BottomNavTabsRoute extends PageRouteInfo<void> {
|
||||
const BottomNavTabsRoute({List<PageRouteInfo>? children})
|
||||
: super(
|
||||
BottomNavTabsRoute.name,
|
||||
path: '',
|
||||
initialChildren: children,
|
||||
);
|
||||
|
||||
static const String name = 'BottomNavTabsRoute';
|
||||
}
|
||||
|
||||
/// generated route for
|
||||
/// [EmptyRouterPage]
|
||||
class LibraryRouter extends PageRouteInfo<void> {
|
||||
const LibraryRouter({List<PageRouteInfo>? children})
|
||||
: super(
|
||||
LibraryRouter.name,
|
||||
path: 'library',
|
||||
initialChildren: children,
|
||||
);
|
||||
|
||||
static const String name = 'LibraryRouter';
|
||||
}
|
||||
|
||||
/// generated route for
|
||||
/// [EmptyRouterPage]
|
||||
class BrowseRouter extends PageRouteInfo<void> {
|
||||
const BrowseRouter({List<PageRouteInfo>? children})
|
||||
: super(
|
||||
BrowseRouter.name,
|
||||
path: 'browse',
|
||||
initialChildren: children,
|
||||
);
|
||||
|
||||
static const String name = 'BrowseRouter';
|
||||
}
|
||||
|
||||
/// generated route for
|
||||
/// [EmptyRouterPage]
|
||||
class SearchRouter extends PageRouteInfo<void> {
|
||||
const SearchRouter({List<PageRouteInfo>? children})
|
||||
: super(
|
||||
SearchRouter.name,
|
||||
path: 'search',
|
||||
initialChildren: children,
|
||||
);
|
||||
|
||||
static const String name = 'SearchRouter';
|
||||
}
|
||||
|
||||
/// generated route for
|
||||
/// [EmptyRouterPage]
|
||||
class SettingsRouter extends PageRouteInfo<void> {
|
||||
const SettingsRouter({List<PageRouteInfo>? children})
|
||||
: super(
|
||||
SettingsRouter.name,
|
||||
path: 'settings',
|
||||
initialChildren: children,
|
||||
);
|
||||
|
||||
static const String name = 'SettingsRouter';
|
||||
}
|
||||
|
||||
/// generated route for
|
||||
/// [LibraryTabsPage]
|
||||
class LibraryTabsRoute extends PageRouteInfo<void> {
|
||||
const LibraryTabsRoute({List<PageRouteInfo>? children})
|
||||
: super(
|
||||
LibraryTabsRoute.name,
|
||||
path: '',
|
||||
initialChildren: children,
|
||||
);
|
||||
|
||||
static const String name = 'LibraryTabsRoute';
|
||||
}
|
||||
|
||||
/// generated route for
|
||||
/// [AlbumSongsPage]
|
||||
class AlbumSongsRoute extends PageRouteInfo<AlbumSongsRouteArgs> {
|
||||
AlbumSongsRoute({
|
||||
Key? key,
|
||||
required String id,
|
||||
}) : super(
|
||||
AlbumSongsRoute.name,
|
||||
path: 'album/:id',
|
||||
args: AlbumSongsRouteArgs(
|
||||
key: key,
|
||||
id: id,
|
||||
),
|
||||
rawPathParams: {'id': id},
|
||||
);
|
||||
|
||||
static const String name = 'AlbumSongsRoute';
|
||||
}
|
||||
|
||||
class AlbumSongsRouteArgs {
|
||||
const AlbumSongsRouteArgs({
|
||||
this.key,
|
||||
required this.id,
|
||||
});
|
||||
|
||||
final Key? key;
|
||||
|
||||
final String id;
|
||||
|
||||
@override
|
||||
String toString() {
|
||||
return 'AlbumSongsRouteArgs{key: $key, id: $id}';
|
||||
}
|
||||
}
|
||||
|
||||
/// generated route for
|
||||
/// [ArtistPage]
|
||||
class ArtistRoute extends PageRouteInfo<ArtistRouteArgs> {
|
||||
ArtistRoute({
|
||||
Key? key,
|
||||
required String id,
|
||||
}) : super(
|
||||
ArtistRoute.name,
|
||||
path: 'artist/:id',
|
||||
args: ArtistRouteArgs(
|
||||
key: key,
|
||||
id: id,
|
||||
),
|
||||
rawPathParams: {'id': id},
|
||||
);
|
||||
|
||||
static const String name = 'ArtistRoute';
|
||||
}
|
||||
|
||||
class ArtistRouteArgs {
|
||||
const ArtistRouteArgs({
|
||||
this.key,
|
||||
required this.id,
|
||||
});
|
||||
|
||||
final Key? key;
|
||||
|
||||
final String id;
|
||||
|
||||
@override
|
||||
String toString() {
|
||||
return 'ArtistRouteArgs{key: $key, id: $id}';
|
||||
}
|
||||
}
|
||||
|
||||
/// generated route for
|
||||
/// [PlaylistSongsPage]
|
||||
class PlaylistSongsRoute extends PageRouteInfo<PlaylistSongsRouteArgs> {
|
||||
PlaylistSongsRoute({
|
||||
Key? key,
|
||||
required String id,
|
||||
}) : super(
|
||||
PlaylistSongsRoute.name,
|
||||
path: 'playlist/:id',
|
||||
args: PlaylistSongsRouteArgs(
|
||||
key: key,
|
||||
id: id,
|
||||
),
|
||||
rawPathParams: {'id': id},
|
||||
);
|
||||
|
||||
static const String name = 'PlaylistSongsRoute';
|
||||
}
|
||||
|
||||
class PlaylistSongsRouteArgs {
|
||||
const PlaylistSongsRouteArgs({
|
||||
this.key,
|
||||
required this.id,
|
||||
});
|
||||
|
||||
final Key? key;
|
||||
|
||||
final String id;
|
||||
|
||||
@override
|
||||
String toString() {
|
||||
return 'PlaylistSongsRouteArgs{key: $key, id: $id}';
|
||||
}
|
||||
}
|
||||
|
||||
/// generated route for
|
||||
/// [GenreSongsPage]
|
||||
class GenreSongsRoute extends PageRouteInfo<GenreSongsRouteArgs> {
|
||||
GenreSongsRoute({
|
||||
Key? key,
|
||||
required String genre,
|
||||
}) : super(
|
||||
GenreSongsRoute.name,
|
||||
path: 'genre/:genre',
|
||||
args: GenreSongsRouteArgs(
|
||||
key: key,
|
||||
genre: genre,
|
||||
),
|
||||
rawPathParams: {'genre': genre},
|
||||
);
|
||||
|
||||
static const String name = 'GenreSongsRoute';
|
||||
}
|
||||
|
||||
class GenreSongsRouteArgs {
|
||||
const GenreSongsRouteArgs({
|
||||
this.key,
|
||||
required this.genre,
|
||||
});
|
||||
|
||||
final Key? key;
|
||||
|
||||
final String genre;
|
||||
|
||||
@override
|
||||
String toString() {
|
||||
return 'GenreSongsRouteArgs{key: $key, genre: $genre}';
|
||||
}
|
||||
}
|
||||
|
||||
/// generated route for
|
||||
/// [LibraryAlbumsPage]
|
||||
class LibraryAlbumsRoute extends PageRouteInfo<void> {
|
||||
const LibraryAlbumsRoute()
|
||||
: super(
|
||||
LibraryAlbumsRoute.name,
|
||||
path: 'albums',
|
||||
);
|
||||
|
||||
static const String name = 'LibraryAlbumsRoute';
|
||||
}
|
||||
|
||||
/// generated route for
|
||||
/// [LibraryArtistsPage]
|
||||
class LibraryArtistsRoute extends PageRouteInfo<void> {
|
||||
const LibraryArtistsRoute()
|
||||
: super(
|
||||
LibraryArtistsRoute.name,
|
||||
path: 'artists',
|
||||
);
|
||||
|
||||
static const String name = 'LibraryArtistsRoute';
|
||||
}
|
||||
|
||||
/// generated route for
|
||||
/// [LibraryPlaylistsPage]
|
||||
class LibraryPlaylistsRoute extends PageRouteInfo<void> {
|
||||
const LibraryPlaylistsRoute()
|
||||
: super(
|
||||
LibraryPlaylistsRoute.name,
|
||||
path: 'playlists',
|
||||
);
|
||||
|
||||
static const String name = 'LibraryPlaylistsRoute';
|
||||
}
|
||||
|
||||
/// generated route for
|
||||
/// [LibrarySongsPage]
|
||||
class LibrarySongsRoute extends PageRouteInfo<void> {
|
||||
const LibrarySongsRoute()
|
||||
: super(
|
||||
LibrarySongsRoute.name,
|
||||
path: 'songs',
|
||||
);
|
||||
|
||||
static const String name = 'LibrarySongsRoute';
|
||||
}
|
||||
|
||||
/// generated route for
|
||||
/// [BrowsePage]
|
||||
class BrowseRoute extends PageRouteInfo<void> {
|
||||
const BrowseRoute()
|
||||
: super(
|
||||
BrowseRoute.name,
|
||||
path: '',
|
||||
);
|
||||
|
||||
static const String name = 'BrowseRoute';
|
||||
}
|
||||
|
||||
/// generated route for
|
||||
/// [SearchPage]
|
||||
class SearchRoute extends PageRouteInfo<void> {
|
||||
const SearchRoute()
|
||||
: super(
|
||||
SearchRoute.name,
|
||||
path: '',
|
||||
);
|
||||
|
||||
static const String name = 'SearchRoute';
|
||||
}
|
||||
|
||||
/// generated route for
|
||||
/// [SettingsPage]
|
||||
class SettingsRoute extends PageRouteInfo<void> {
|
||||
const SettingsRoute()
|
||||
: super(
|
||||
SettingsRoute.name,
|
||||
path: '',
|
||||
);
|
||||
|
||||
static const String name = 'SettingsRoute';
|
||||
}
|
||||
|
||||
/// generated route for
|
||||
/// [SourcePage]
|
||||
class SourceRoute extends PageRouteInfo<SourceRouteArgs> {
|
||||
SourceRoute({
|
||||
Key? key,
|
||||
int? id,
|
||||
}) : super(
|
||||
SourceRoute.name,
|
||||
path: 'source/:id',
|
||||
args: SourceRouteArgs(
|
||||
key: key,
|
||||
id: id,
|
||||
),
|
||||
rawPathParams: {'id': id},
|
||||
);
|
||||
|
||||
static const String name = 'SourceRoute';
|
||||
}
|
||||
|
||||
class SourceRouteArgs {
|
||||
const SourceRouteArgs({
|
||||
this.key,
|
||||
this.id,
|
||||
});
|
||||
|
||||
final Key? key;
|
||||
|
||||
final int? id;
|
||||
|
||||
@override
|
||||
String toString() {
|
||||
return 'SourceRouteArgs{key: $key, id: $id}';
|
||||
}
|
||||
}
|
||||
@@ -1,63 +0,0 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_gen/gen_l10n/app_localizations.dart';
|
||||
|
||||
class ShuffleFab extends StatelessWidget {
|
||||
final void Function()? onPressed;
|
||||
|
||||
const ShuffleFab({
|
||||
super.key,
|
||||
this.onPressed,
|
||||
});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final l = AppLocalizations.of(context);
|
||||
|
||||
return FloatingActionButton(
|
||||
heroTag: null,
|
||||
onPressed: onPressed,
|
||||
tooltip: l.actionsCancel,
|
||||
child: const Icon(Icons.shuffle_rounded),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class RadioPlayFab extends StatelessWidget {
|
||||
final void Function()? onPressed;
|
||||
|
||||
const RadioPlayFab({
|
||||
super.key,
|
||||
this.onPressed,
|
||||
});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return FloatingActionButton(
|
||||
heroTag: null,
|
||||
onPressed: onPressed,
|
||||
child: Stack(
|
||||
clipBehavior: Clip.none,
|
||||
children: [
|
||||
const Icon(Icons.radio_rounded),
|
||||
Positioned(
|
||||
bottom: -11,
|
||||
right: -10,
|
||||
child: Icon(
|
||||
Icons.play_arrow_rounded,
|
||||
color: Theme.of(context).colorScheme.primaryContainer,
|
||||
size: 26,
|
||||
),
|
||||
),
|
||||
const Positioned(
|
||||
bottom: -6,
|
||||
right: -5,
|
||||
child: Icon(
|
||||
Icons.play_arrow_rounded,
|
||||
size: 16,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -1,414 +0,0 @@
|
||||
// ignore_for_file: use_build_context_synchronously
|
||||
|
||||
import 'dart:async';
|
||||
|
||||
import 'package:auto_route/auto_route.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_gen/gen_l10n/app_localizations.dart';
|
||||
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
||||
|
||||
import '../models/music.dart';
|
||||
import '../services/cache_service.dart';
|
||||
import '../state/theme.dart';
|
||||
import 'app_router.dart';
|
||||
import 'hooks/use_download_actions.dart';
|
||||
import 'images.dart';
|
||||
|
||||
enum MenuSize {
|
||||
small,
|
||||
medium,
|
||||
}
|
||||
|
||||
Future<T?> showContextMenu<T>({
|
||||
required BuildContext context,
|
||||
required WidgetRef ref,
|
||||
required WidgetBuilder builder,
|
||||
}) {
|
||||
return showModalBottomSheet<T>(
|
||||
backgroundColor: ref.read(baseThemeProvider).theme.colorScheme.background,
|
||||
useRootNavigator: true,
|
||||
isScrollControlled: true,
|
||||
context: context,
|
||||
builder: builder,
|
||||
);
|
||||
}
|
||||
|
||||
class BottomSheetMenu extends HookConsumerWidget {
|
||||
final Widget child;
|
||||
final MenuSize size;
|
||||
|
||||
const BottomSheetMenu({
|
||||
super.key,
|
||||
required this.child,
|
||||
this.size = MenuSize.medium,
|
||||
});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context, WidgetRef ref) {
|
||||
final theme = ref.watch(baseThemeProvider);
|
||||
final height = size == MenuSize.medium ? 0.4 : 0.25;
|
||||
|
||||
return Theme(
|
||||
data: theme.theme,
|
||||
child: DraggableScrollableSheet(
|
||||
expand: false,
|
||||
initialChildSize: height,
|
||||
maxChildSize: height,
|
||||
minChildSize: height - 0.05,
|
||||
snap: true,
|
||||
snapSizes: [height - 0.05, height],
|
||||
builder: (context, scrollController) {
|
||||
return PrimaryScrollController(
|
||||
controller: scrollController,
|
||||
child: SizedBox(
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.only(top: 8),
|
||||
child: child,
|
||||
),
|
||||
),
|
||||
);
|
||||
},
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class AlbumContextMenu extends HookConsumerWidget {
|
||||
final Album album;
|
||||
|
||||
const AlbumContextMenu({
|
||||
super.key,
|
||||
required this.album,
|
||||
});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context, WidgetRef ref) {
|
||||
final downloadActions = useAlbumDownloadActions(
|
||||
context: context,
|
||||
ref: ref,
|
||||
album: album,
|
||||
);
|
||||
|
||||
return ListView(
|
||||
children: [
|
||||
_AlbumHeader(album: album),
|
||||
const SizedBox(height: 8),
|
||||
const _Star(),
|
||||
if (album.artistId != null) _ViewArtist(id: album.artistId!),
|
||||
for (var action in downloadActions)
|
||||
_DownloadAction(key: ValueKey(action.type), downloadAction: action),
|
||||
],
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class SongContextMenu extends HookConsumerWidget {
|
||||
final Song song;
|
||||
|
||||
const SongContextMenu({
|
||||
super.key,
|
||||
required this.song,
|
||||
});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context, WidgetRef ref) {
|
||||
return ListView(
|
||||
children: [
|
||||
_SongHeader(song: song),
|
||||
const SizedBox(height: 8),
|
||||
const _Star(),
|
||||
if (song.artistId != null) _ViewArtist(id: song.artistId!),
|
||||
if (song.albumId != null) _ViewAlbum(id: song.albumId!),
|
||||
// const _DownloadAction(),
|
||||
],
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class ArtistContextMenu extends HookConsumerWidget {
|
||||
final Artist artist;
|
||||
|
||||
const ArtistContextMenu({
|
||||
super.key,
|
||||
required this.artist,
|
||||
});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context, WidgetRef ref) {
|
||||
return ListView(
|
||||
children: [
|
||||
_ArtistHeader(artist: artist),
|
||||
const SizedBox(height: 8),
|
||||
const _Star(),
|
||||
// const _Download(),
|
||||
],
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class PlaylistContextMenu extends HookConsumerWidget {
|
||||
final Playlist playlist;
|
||||
|
||||
const PlaylistContextMenu({
|
||||
super.key,
|
||||
required this.playlist,
|
||||
});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context, WidgetRef ref) {
|
||||
final downloadActions = usePlaylistDownloadActions(
|
||||
context: context,
|
||||
ref: ref,
|
||||
playlist: playlist,
|
||||
);
|
||||
|
||||
return ListView(
|
||||
children: [
|
||||
_PlaylistHeader(playlist: playlist),
|
||||
const SizedBox(height: 8),
|
||||
for (var action in downloadActions)
|
||||
_DownloadAction(key: ValueKey(action.type), downloadAction: action),
|
||||
],
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class _AlbumHeader extends HookConsumerWidget {
|
||||
final Album album;
|
||||
|
||||
const _AlbumHeader({
|
||||
required this.album,
|
||||
});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context, WidgetRef ref) {
|
||||
final cache = ref.watch(cacheServiceProvider);
|
||||
|
||||
return _Header(
|
||||
title: album.name,
|
||||
subtitle: album.albumArtist,
|
||||
image: CardClip(
|
||||
child: UriCacheInfoImage(
|
||||
cache: cache.albumArt(album, thumbnail: true),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class _SongHeader extends HookConsumerWidget {
|
||||
final Song song;
|
||||
|
||||
const _SongHeader({
|
||||
required this.song,
|
||||
});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context, WidgetRef ref) {
|
||||
return _Header(
|
||||
title: song.title,
|
||||
subtitle: song.artist,
|
||||
image: SongAlbumArt(song: song, square: false),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class _ArtistHeader extends HookConsumerWidget {
|
||||
final Artist artist;
|
||||
|
||||
const _ArtistHeader({
|
||||
required this.artist,
|
||||
});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context, WidgetRef ref) {
|
||||
final l = AppLocalizations.of(context);
|
||||
return _Header(
|
||||
title: artist.name,
|
||||
subtitle: l.resourcesAlbumCount(artist.albumCount),
|
||||
image: CircleClip(child: ArtistArtImage(artistId: artist.id)),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class _PlaylistHeader extends HookConsumerWidget {
|
||||
final Playlist playlist;
|
||||
|
||||
const _PlaylistHeader({
|
||||
required this.playlist,
|
||||
});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context, WidgetRef ref) {
|
||||
final cache = ref.watch(cacheServiceProvider);
|
||||
final l = AppLocalizations.of(context);
|
||||
return _Header(
|
||||
title: playlist.name,
|
||||
subtitle: l.resourcesSongCount(playlist.songCount),
|
||||
image: CardClip(
|
||||
child: UriCacheInfoImage(
|
||||
cache: cache.playlistArt(playlist, thumbnail: true),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class _Header extends HookConsumerWidget {
|
||||
final String title;
|
||||
final String? subtitle;
|
||||
final Widget image;
|
||||
|
||||
const _Header({
|
||||
required this.title,
|
||||
this.subtitle,
|
||||
required this.image,
|
||||
});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context, WidgetRef ref) {
|
||||
final theme = Theme.of(context);
|
||||
|
||||
return Padding(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 24, vertical: 16),
|
||||
child: Row(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
SizedBox(height: 80, width: 80, child: image),
|
||||
const SizedBox(width: 16),
|
||||
Expanded(
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Text(title, style: theme.textTheme.titleLarge),
|
||||
if (subtitle != null)
|
||||
Text(subtitle!, style: theme.textTheme.titleSmall),
|
||||
],
|
||||
),
|
||||
)
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class _Star extends HookConsumerWidget {
|
||||
const _Star();
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context, WidgetRef ref) {
|
||||
final l = AppLocalizations.of(context);
|
||||
return _MenuItem(
|
||||
title: l.actionsStar,
|
||||
icon: const Icon(Icons.star_outline_rounded),
|
||||
onTap: () {},
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class _DownloadAction extends HookConsumerWidget {
|
||||
final DownloadAction downloadAction;
|
||||
|
||||
const _DownloadAction({
|
||||
super.key,
|
||||
required this.downloadAction,
|
||||
});
|
||||
|
||||
String _actionText(AppLocalizations l) {
|
||||
switch (downloadAction.type) {
|
||||
case DownloadActionType.download:
|
||||
return l.actionsDownload;
|
||||
case DownloadActionType.cancel:
|
||||
return l.actionsDownloadCancel;
|
||||
case DownloadActionType.delete:
|
||||
return l.actionsDownloadDelete;
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context, WidgetRef ref) {
|
||||
return _MenuItem(
|
||||
title: _actionText(AppLocalizations.of(context)),
|
||||
icon: downloadAction.iconBuilder(context),
|
||||
onTap: downloadAction.action,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class _ViewArtist extends HookConsumerWidget {
|
||||
final String id;
|
||||
|
||||
const _ViewArtist({
|
||||
required this.id,
|
||||
});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context, WidgetRef ref) {
|
||||
final l = AppLocalizations.of(context);
|
||||
return _MenuItem(
|
||||
title: l.resourcesArtistActionsView,
|
||||
icon: const Icon(Icons.person_rounded),
|
||||
onTap: () async {
|
||||
final router = context.router;
|
||||
|
||||
await router.pop();
|
||||
if (router.currentPath == '/now-playing') {
|
||||
await router.pop();
|
||||
await router.navigate(const LibraryRouter());
|
||||
}
|
||||
await router.navigate(ArtistRoute(id: id));
|
||||
},
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class _ViewAlbum extends HookConsumerWidget {
|
||||
final String id;
|
||||
|
||||
const _ViewAlbum({
|
||||
required this.id,
|
||||
});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context, WidgetRef ref) {
|
||||
final l = AppLocalizations.of(context);
|
||||
return _MenuItem(
|
||||
title: l.resourcesAlbumActionsView,
|
||||
icon: const Icon(Icons.album_rounded),
|
||||
onTap: () async {
|
||||
final router = context.router;
|
||||
|
||||
await router.pop();
|
||||
if (router.currentPath == '/now-playing') {
|
||||
await router.pop();
|
||||
await router.navigate(const LibraryRouter());
|
||||
}
|
||||
await router.navigate(AlbumSongsRoute(id: id));
|
||||
},
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class _MenuItem extends StatelessWidget {
|
||||
final String title;
|
||||
final Widget icon;
|
||||
final FutureOr<void> Function()? onTap;
|
||||
|
||||
const _MenuItem({
|
||||
required this.title,
|
||||
required this.icon,
|
||||
this.onTap,
|
||||
});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return ListTile(
|
||||
title: Text(title),
|
||||
leading: Padding(
|
||||
padding: const EdgeInsetsDirectional.only(start: 8),
|
||||
child: icon,
|
||||
),
|
||||
onTap: onTap,
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -1,96 +0,0 @@
|
||||
import 'package:fast_immutable_collections/fast_immutable_collections.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_gen/gen_l10n/app_localizations.dart';
|
||||
import 'package:flutter_hooks/flutter_hooks.dart';
|
||||
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
||||
|
||||
import '../models/support.dart';
|
||||
import '../state/theme.dart';
|
||||
|
||||
class DeleteDialog extends HookConsumerWidget {
|
||||
const DeleteDialog({
|
||||
super.key,
|
||||
});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context, WidgetRef ref) {
|
||||
final theme = ref.watch(baseThemeProvider);
|
||||
|
||||
final l = AppLocalizations.of(context);
|
||||
|
||||
return Theme(
|
||||
data: theme.theme,
|
||||
child: AlertDialog(
|
||||
title: Text(l.resourcesSongListDeleteAllTitle),
|
||||
content: Text(l.resourcesSongListDeleteAllContent),
|
||||
actions: [
|
||||
FilledButton.tonal(
|
||||
onPressed: () => Navigator.pop(context, false),
|
||||
child: Text(l.actionsCancel),
|
||||
),
|
||||
FilledButton.icon(
|
||||
onPressed: () => Navigator.pop(context, true),
|
||||
label: Text(l.actionsDelete),
|
||||
icon: const Icon(Icons.delete_forever_rounded),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class MultipleChoiceDialog<T> extends HookConsumerWidget {
|
||||
final String title;
|
||||
final T current;
|
||||
final IList<MultiChoiceOption> options;
|
||||
|
||||
const MultipleChoiceDialog({
|
||||
super.key,
|
||||
required this.title,
|
||||
required this.current,
|
||||
required this.options,
|
||||
});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context, WidgetRef ref) {
|
||||
final l = AppLocalizations.of(context);
|
||||
final state = useState<T>(current);
|
||||
|
||||
List<Widget> choices = [];
|
||||
for (var opt in options) {
|
||||
final value = opt.map(
|
||||
(value) => null,
|
||||
int: (value) => value.option,
|
||||
string: (value) => value.option,
|
||||
) as T;
|
||||
choices.add(RadioListTile<T>(
|
||||
value: value,
|
||||
groupValue: state.value,
|
||||
title: Text(opt.title),
|
||||
onChanged: (value) => state.value = value as T,
|
||||
));
|
||||
}
|
||||
|
||||
return AlertDialog(
|
||||
title: Text(title),
|
||||
contentPadding: const EdgeInsets.symmetric(vertical: 20),
|
||||
content: Material(
|
||||
type: MaterialType.transparency,
|
||||
child: SingleChildScrollView(
|
||||
child: Column(children: choices),
|
||||
),
|
||||
),
|
||||
actions: [
|
||||
FilledButton.tonal(
|
||||
onPressed: () => Navigator.pop(context, null),
|
||||
child: Text(l.actionsCancel),
|
||||
),
|
||||
FilledButton.icon(
|
||||
onPressed: () => Navigator.pop(context, state.value),
|
||||
label: Text(l.actionsOk),
|
||||
icon: const Icon(Icons.check_rounded),
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -1,76 +0,0 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
||||
|
||||
import '../models/support.dart';
|
||||
import '../state/theme.dart';
|
||||
|
||||
class MediaItemGradient extends ConsumerWidget {
|
||||
const MediaItemGradient({super.key});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context, WidgetRef ref) {
|
||||
final colors = ref.watch(mediaItemThemeProvider).valueOrNull;
|
||||
return BackgroundGradient(colors: colors);
|
||||
}
|
||||
}
|
||||
|
||||
class AlbumArtGradient extends ConsumerWidget {
|
||||
final String id;
|
||||
|
||||
const AlbumArtGradient({
|
||||
super.key,
|
||||
required this.id,
|
||||
});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context, WidgetRef ref) {
|
||||
final colors = ref.watch(albumArtThemeProvider(id)).valueOrNull;
|
||||
return BackgroundGradient(colors: colors);
|
||||
}
|
||||
}
|
||||
|
||||
class PlaylistArtGradient extends ConsumerWidget {
|
||||
final String id;
|
||||
|
||||
const PlaylistArtGradient({
|
||||
super.key,
|
||||
required this.id,
|
||||
});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context, WidgetRef ref) {
|
||||
final colors = ref.watch(playlistArtThemeProvider(id)).valueOrNull;
|
||||
return BackgroundGradient(colors: colors);
|
||||
}
|
||||
}
|
||||
|
||||
class BackgroundGradient extends HookConsumerWidget {
|
||||
final ColorTheme? colors;
|
||||
|
||||
const BackgroundGradient({
|
||||
super.key,
|
||||
this.colors,
|
||||
});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context, WidgetRef ref) {
|
||||
final base = ref.watch(baseThemeProvider);
|
||||
|
||||
return SizedBox(
|
||||
width: double.infinity,
|
||||
height: MediaQuery.of(context).size.height,
|
||||
child: Container(
|
||||
decoration: BoxDecoration(
|
||||
gradient: LinearGradient(
|
||||
begin: Alignment.topCenter,
|
||||
end: Alignment.bottomCenter,
|
||||
colors: [
|
||||
colors?.gradientHigh ?? base.gradientHigh,
|
||||
colors?.gradientLow ?? base.gradientLow,
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -1,149 +0,0 @@
|
||||
import 'dart:async';
|
||||
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
||||
|
||||
import '../../models/music.dart';
|
||||
import '../../models/support.dart';
|
||||
import '../../services/download_service.dart';
|
||||
import '../../state/music.dart';
|
||||
import '../../state/settings.dart';
|
||||
import '../dialogs.dart';
|
||||
|
||||
enum DownloadActionType {
|
||||
download,
|
||||
cancel,
|
||||
delete,
|
||||
}
|
||||
|
||||
class DownloadAction {
|
||||
final DownloadActionType type;
|
||||
final WidgetBuilder iconBuilder;
|
||||
final FutureOr<void> Function()? action;
|
||||
|
||||
const DownloadAction({
|
||||
required this.type,
|
||||
required this.iconBuilder,
|
||||
this.action,
|
||||
});
|
||||
}
|
||||
|
||||
List<DownloadAction> useAlbumDownloadActions({
|
||||
required BuildContext context,
|
||||
required WidgetRef ref,
|
||||
required Album album,
|
||||
}) {
|
||||
final status = ref.watch(albumDownloadStatusProvider(album.id)).valueOrNull;
|
||||
|
||||
return useListDownloadActions(
|
||||
context: context,
|
||||
ref: ref,
|
||||
list: album,
|
||||
status: status,
|
||||
onDownload: () =>
|
||||
ref.read(downloadServiceProvider.notifier).downloadAlbum(album),
|
||||
onDelete: () =>
|
||||
ref.read(downloadServiceProvider.notifier).deleteAlbum(album),
|
||||
onCancel: () =>
|
||||
ref.read(downloadServiceProvider.notifier).cancelAlbum(album),
|
||||
);
|
||||
}
|
||||
|
||||
List<DownloadAction> usePlaylistDownloadActions({
|
||||
required BuildContext context,
|
||||
required WidgetRef ref,
|
||||
required Playlist playlist,
|
||||
}) {
|
||||
final status =
|
||||
ref.watch(playlistDownloadStatusProvider(playlist.id)).valueOrNull;
|
||||
|
||||
return useListDownloadActions(
|
||||
context: context,
|
||||
ref: ref,
|
||||
list: playlist,
|
||||
status: status,
|
||||
onDownload: () =>
|
||||
ref.read(downloadServiceProvider.notifier).downloadPlaylist(playlist),
|
||||
onDelete: () =>
|
||||
ref.read(downloadServiceProvider.notifier).deletePlaylist(playlist),
|
||||
onCancel: () =>
|
||||
ref.read(downloadServiceProvider.notifier).cancelPlaylist(playlist),
|
||||
);
|
||||
}
|
||||
|
||||
List<DownloadAction> useListDownloadActions({
|
||||
required BuildContext context,
|
||||
required WidgetRef ref,
|
||||
required SourceIdentifiable list,
|
||||
required ListDownloadStatus? status,
|
||||
required FutureOr<void> Function() onDelete,
|
||||
required FutureOr<void> Function() onCancel,
|
||||
required FutureOr<void> Function() onDownload,
|
||||
}) {
|
||||
status ??= const ListDownloadStatus(total: 0, downloaded: 0, downloading: 0);
|
||||
|
||||
final sourceId = SourceId.from(list);
|
||||
final offline = ref.watch(offlineModeProvider);
|
||||
final listDownloadInProgress = ref.watch(downloadServiceProvider
|
||||
.select((value) => value.listDownloads.contains(sourceId)));
|
||||
final listDeleteInProgress = ref.watch(downloadServiceProvider
|
||||
.select((value) => value.deletes.contains(sourceId)));
|
||||
final listCancelInProgress = ref.watch(downloadServiceProvider
|
||||
.select((value) => value.listCancels.contains(sourceId)));
|
||||
|
||||
DownloadAction delete() {
|
||||
return DownloadAction(
|
||||
type: DownloadActionType.delete,
|
||||
iconBuilder: (context) => const Icon(Icons.delete_forever_rounded),
|
||||
action: listDeleteInProgress
|
||||
? null
|
||||
: () async {
|
||||
final ok = await showDialog<bool>(
|
||||
context: context,
|
||||
builder: (context) => const DeleteDialog(),
|
||||
);
|
||||
if (ok == true) {
|
||||
await onDelete();
|
||||
}
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
DownloadAction cancel() {
|
||||
return DownloadAction(
|
||||
type: DownloadActionType.cancel,
|
||||
iconBuilder: (context) => Stack(
|
||||
alignment: Alignment.center,
|
||||
children: const [
|
||||
Icon(Icons.cancel_rounded),
|
||||
SizedBox(
|
||||
height: 32,
|
||||
width: 32,
|
||||
child: CircularProgressIndicator(
|
||||
strokeWidth: 3,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
action: listCancelInProgress ? null : onCancel,
|
||||
);
|
||||
}
|
||||
|
||||
DownloadAction download() {
|
||||
return DownloadAction(
|
||||
type: DownloadActionType.download,
|
||||
iconBuilder: (context) => const Icon(Icons.download_rounded),
|
||||
action: !offline ? onDownload : null,
|
||||
);
|
||||
}
|
||||
|
||||
if (status.total == status.downloaded) {
|
||||
return [delete()];
|
||||
} else if (status.downloading == 0 && status.downloaded > 0) {
|
||||
return [download(), delete()];
|
||||
} else if (listDownloadInProgress || status.downloading > 0) {
|
||||
return [cancel()];
|
||||
} else {
|
||||
return [download()];
|
||||
}
|
||||
}
|
||||
@@ -1,91 +0,0 @@
|
||||
import 'dart:async';
|
||||
|
||||
import 'package:flutter_hooks/flutter_hooks.dart';
|
||||
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
||||
import 'package:infinite_scroll_pagination/infinite_scroll_pagination.dart';
|
||||
|
||||
import '../../models/query.dart';
|
||||
import '../../services/sync_service.dart';
|
||||
import '../../state/settings.dart';
|
||||
import '../pages/library_page.dart';
|
||||
import 'use_paging_controller.dart';
|
||||
|
||||
PagingController<int, T> useLibraryPagingController<T>(
|
||||
WidgetRef ref, {
|
||||
required int libraryTabIndex,
|
||||
required FutureOr<List<T>> Function(ListQuery query) getItems,
|
||||
}) {
|
||||
final queryProvider = libraryListQueryProvider(libraryTabIndex).select(
|
||||
(value) => value.query,
|
||||
);
|
||||
final query = useState(ref.read(queryProvider));
|
||||
|
||||
final onPageRequest = useCallback(
|
||||
(int pageKey, PagingController<int, T> pagingController) =>
|
||||
_pageRequest(getItems, query.value, pageKey, pagingController),
|
||||
[query.value],
|
||||
);
|
||||
|
||||
final pagingController = usePagingController<int, T>(
|
||||
firstPageKey: query.value.page.offset,
|
||||
onPageRequest: onPageRequest,
|
||||
);
|
||||
|
||||
ref.listen(queryProvider, (_, next) {
|
||||
query.value = next;
|
||||
pagingController.refresh();
|
||||
});
|
||||
|
||||
ref.listen(syncServiceProvider, (_, __) => pagingController.refresh());
|
||||
ref.listen(sourceIdProvider, (_, __) => pagingController.refresh());
|
||||
ref.listen(offlineModeProvider, (_, __) => pagingController.refresh());
|
||||
|
||||
return pagingController;
|
||||
}
|
||||
|
||||
PagingController<int, T> useListQueryPagingController<T>(
|
||||
WidgetRef ref, {
|
||||
required ListQuery query,
|
||||
required FutureOr<List<T>> Function(ListQuery query) getItems,
|
||||
}) {
|
||||
final onPageRequest = useCallback(
|
||||
(int pageKey, PagingController<int, T> pagingController) =>
|
||||
_pageRequest(getItems, query, pageKey, pagingController),
|
||||
[query],
|
||||
);
|
||||
|
||||
final pagingController = usePagingController<int, T>(
|
||||
firstPageKey: query.page.offset,
|
||||
onPageRequest: onPageRequest,
|
||||
);
|
||||
|
||||
return pagingController;
|
||||
}
|
||||
|
||||
Future<void> _pageRequest<T>(
|
||||
FutureOr<List<T>> Function(ListQuery query) getItems,
|
||||
ListQuery query,
|
||||
int pageKey,
|
||||
PagingController<int, dynamic> pagingController,
|
||||
) async {
|
||||
try {
|
||||
final newItems = await getItems(query.copyWith.page(offset: pageKey));
|
||||
|
||||
final isFirstPage = newItems.isNotEmpty && pageKey == 0;
|
||||
final alreadyHasItems = pagingController.itemList != null &&
|
||||
pagingController.itemList!.isNotEmpty;
|
||||
if (isFirstPage && alreadyHasItems) {
|
||||
return;
|
||||
}
|
||||
|
||||
final isLastPage = newItems.length < query.page.limit;
|
||||
if (isLastPage) {
|
||||
pagingController.appendLastPage(newItems);
|
||||
} else {
|
||||
final nextPageKey = pageKey + newItems.length;
|
||||
pagingController.appendPage(newItems, nextPageKey);
|
||||
}
|
||||
} catch (error) {
|
||||
pagingController.error = error;
|
||||
}
|
||||
}
|
||||
@@ -1,66 +0,0 @@
|
||||
import 'dart:async';
|
||||
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_hooks/flutter_hooks.dart';
|
||||
import 'package:infinite_scroll_pagination/infinite_scroll_pagination.dart';
|
||||
|
||||
PagingController<PageKeyType, ItemType>
|
||||
usePagingController<PageKeyType, ItemType>({
|
||||
required final PageKeyType firstPageKey,
|
||||
final int? invisibleItemsThreshold,
|
||||
List<Object?>? keys,
|
||||
FutureOr<void> Function(PageKeyType pageKey,
|
||||
PagingController<PageKeyType, ItemType> pagingController)?
|
||||
onPageRequest,
|
||||
}) {
|
||||
final controller = use(
|
||||
_PagingControllerHook<PageKeyType, ItemType>(
|
||||
firstPageKey: firstPageKey,
|
||||
invisibleItemsThreshold: invisibleItemsThreshold,
|
||||
keys: keys,
|
||||
),
|
||||
);
|
||||
|
||||
useEffect(() {
|
||||
listener(PageKeyType pageKey) => onPageRequest?.call(pageKey, controller);
|
||||
controller.addPageRequestListener(listener);
|
||||
return () => controller.removePageRequestListener(listener);
|
||||
}, [onPageRequest]);
|
||||
|
||||
return controller;
|
||||
}
|
||||
|
||||
class _PagingControllerHook<PageKeyType, ItemType>
|
||||
extends Hook<PagingController<PageKeyType, ItemType>> {
|
||||
const _PagingControllerHook({
|
||||
required this.firstPageKey,
|
||||
this.invisibleItemsThreshold,
|
||||
List<Object?>? keys,
|
||||
}) : super(keys: keys);
|
||||
|
||||
final PageKeyType firstPageKey;
|
||||
final int? invisibleItemsThreshold;
|
||||
|
||||
@override
|
||||
HookState<PagingController<PageKeyType, ItemType>,
|
||||
Hook<PagingController<PageKeyType, ItemType>>>
|
||||
createState() => _PagingControllerHookState<PageKeyType, ItemType>();
|
||||
}
|
||||
|
||||
class _PagingControllerHookState<PageKeyType, ItemType> extends HookState<
|
||||
PagingController<PageKeyType, ItemType>,
|
||||
_PagingControllerHook<PageKeyType, ItemType>> {
|
||||
late final controller = PagingController<PageKeyType, ItemType>(
|
||||
firstPageKey: hook.firstPageKey,
|
||||
invisibleItemsThreshold: hook.invisibleItemsThreshold);
|
||||
|
||||
@override
|
||||
PagingController<PageKeyType, ItemType> build(BuildContext context) =>
|
||||
controller;
|
||||
|
||||
@override
|
||||
void dispose() => controller.dispose();
|
||||
|
||||
@override
|
||||
String get debugLabel => 'usePagingController';
|
||||
}
|
||||
@@ -1,368 +0,0 @@
|
||||
import 'package:cached_network_image/cached_network_image.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
||||
import 'package:riverpod_annotation/riverpod_annotation.dart';
|
||||
|
||||
import '../models/music.dart';
|
||||
import '../models/support.dart';
|
||||
import '../services/cache_service.dart';
|
||||
import '../state/music.dart';
|
||||
import '../state/settings.dart';
|
||||
import '../state/theme.dart';
|
||||
|
||||
part 'images.g.dart';
|
||||
|
||||
@riverpod
|
||||
CacheInfo _artistArtCacheInfo(
|
||||
_ArtistArtCacheInfoRef ref, {
|
||||
required String artistId,
|
||||
bool thumbnail = true,
|
||||
}) {
|
||||
final cache = ref.watch(cacheServiceProvider);
|
||||
return cache.artistArtCacheInfo(artistId, thumbnail: thumbnail);
|
||||
}
|
||||
|
||||
@riverpod
|
||||
FutureOr<String?> _artistArtCachedUrl(
|
||||
_ArtistArtCachedUrlRef ref, {
|
||||
required String artistId,
|
||||
bool thumbnail = true,
|
||||
}) async {
|
||||
final cache = ref.watch(_artistArtCacheInfoProvider(
|
||||
artistId: artistId,
|
||||
thumbnail: thumbnail,
|
||||
));
|
||||
final file = await cache.cacheManager.getFileFromCache(cache.cacheKey);
|
||||
return file?.originalUrl;
|
||||
}
|
||||
|
||||
@riverpod
|
||||
FutureOr<UriCacheInfo> _artistArtUriCacheInfo(
|
||||
_ArtistArtUriCacheInfoRef ref, {
|
||||
required String artistId,
|
||||
bool thumbnail = true,
|
||||
}) async {
|
||||
final cache = ref.watch(cacheServiceProvider);
|
||||
final info = ref.watch(_artistArtCacheInfoProvider(
|
||||
artistId: artistId,
|
||||
thumbnail: thumbnail,
|
||||
));
|
||||
final cachedUrl = await ref.watch(_artistArtCachedUrlProvider(
|
||||
artistId: artistId,
|
||||
thumbnail: thumbnail,
|
||||
).future);
|
||||
final offline = ref.watch(offlineModeProvider);
|
||||
|
||||
// already cached, don't try to get the real url again
|
||||
if (cachedUrl != null) {
|
||||
return UriCacheInfo(
|
||||
uri: Uri.parse(cachedUrl),
|
||||
cacheKey: info.cacheKey,
|
||||
cacheManager: info.cacheManager,
|
||||
);
|
||||
}
|
||||
|
||||
if (offline) {
|
||||
final file = await cache.imageCache.getFileFromCache(info.cacheKey);
|
||||
if (file != null) {
|
||||
return UriCacheInfo(
|
||||
uri: Uri.parse(file.originalUrl),
|
||||
cacheKey: info.cacheKey,
|
||||
cacheManager: info.cacheManager,
|
||||
);
|
||||
} else {
|
||||
return cache.placeholder(thumbnail: thumbnail);
|
||||
}
|
||||
}
|
||||
|
||||
// assume the url is good or let this fail
|
||||
return UriCacheInfo(
|
||||
uri: (await cache.artistArtUri(artistId, thumbnail: thumbnail))!,
|
||||
cacheKey: info.cacheKey,
|
||||
cacheManager: info.cacheManager,
|
||||
);
|
||||
}
|
||||
|
||||
class ArtistArtImage extends HookConsumerWidget {
|
||||
final String artistId;
|
||||
final bool thumbnail;
|
||||
final BoxFit fit;
|
||||
final PlaceholderStyle placeholderStyle;
|
||||
final double? height;
|
||||
final double? width;
|
||||
|
||||
const ArtistArtImage({
|
||||
super.key,
|
||||
required this.artistId,
|
||||
this.thumbnail = true,
|
||||
this.fit = BoxFit.cover,
|
||||
this.placeholderStyle = PlaceholderStyle.color,
|
||||
this.height,
|
||||
this.width,
|
||||
});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context, WidgetRef ref) {
|
||||
final cache = ref.watch(_artistArtUriCacheInfoProvider(
|
||||
artistId: artistId,
|
||||
thumbnail: thumbnail,
|
||||
));
|
||||
|
||||
// TODO: figure out how to animate this without messing up with boxfit/ratio
|
||||
return cache.when(
|
||||
data: (data) => UriCacheInfoImage(
|
||||
cache: data,
|
||||
fit: fit,
|
||||
placeholderStyle: placeholderStyle,
|
||||
height: height,
|
||||
width: width,
|
||||
),
|
||||
error: (_, __) => Container(
|
||||
color: Colors.red,
|
||||
height: height,
|
||||
width: width,
|
||||
),
|
||||
loading: () => Container(
|
||||
color: Theme.of(context).colorScheme.secondaryContainer,
|
||||
height: height,
|
||||
width: width,
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class SongAlbumArt extends HookConsumerWidget {
|
||||
final Song song;
|
||||
final bool square;
|
||||
|
||||
const SongAlbumArt({
|
||||
super.key,
|
||||
required this.song,
|
||||
this.square = true,
|
||||
});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context, WidgetRef ref) {
|
||||
final album = ref.watch(albumProvider(song.albumId!)).valueOrNull;
|
||||
|
||||
return AnimatedSwitcher(
|
||||
duration: const Duration(milliseconds: 150),
|
||||
child: album != null ? AlbumArt(album: album) : const PlaceholderImage(),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class AlbumArt extends HookConsumerWidget {
|
||||
final Album album;
|
||||
final bool square;
|
||||
|
||||
const AlbumArt({
|
||||
super.key,
|
||||
required this.album,
|
||||
this.square = true,
|
||||
});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context, WidgetRef ref) {
|
||||
// generate the palette used in other views ahead of time
|
||||
ref.watch(albumArtPaletteProvider(album.id));
|
||||
final cache = ref.watch(cacheServiceProvider);
|
||||
|
||||
Widget image = UriCacheInfoImage(cache: cache.albumArt(album));
|
||||
|
||||
if (square) {
|
||||
image = AspectRatio(aspectRatio: 1.0, child: image);
|
||||
}
|
||||
|
||||
return CardClip(child: image);
|
||||
}
|
||||
}
|
||||
|
||||
class CircleClip extends StatelessWidget {
|
||||
final Widget child;
|
||||
|
||||
const CircleClip({
|
||||
super.key,
|
||||
required this.child,
|
||||
});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return ClipOval(
|
||||
clipBehavior: Clip.antiAlias,
|
||||
child: AspectRatio(
|
||||
aspectRatio: 1.0,
|
||||
child: child,
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class CardClip extends StatelessWidget {
|
||||
final Widget child;
|
||||
final bool square;
|
||||
|
||||
const CardClip({
|
||||
super.key,
|
||||
required this.child,
|
||||
this.square = true,
|
||||
});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final cardShape = Theme.of(context).cardTheme.shape;
|
||||
return ClipRRect(
|
||||
borderRadius:
|
||||
cardShape is RoundedRectangleBorder ? cardShape.borderRadius : null,
|
||||
child: !square
|
||||
? child
|
||||
: AspectRatio(
|
||||
aspectRatio: 1.0,
|
||||
child: child,
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
enum PlaceholderStyle {
|
||||
color,
|
||||
spinner,
|
||||
}
|
||||
|
||||
class UriCacheInfoImage extends StatelessWidget {
|
||||
final UriCacheInfo cache;
|
||||
final BoxFit fit;
|
||||
final PlaceholderStyle placeholderStyle;
|
||||
final double? height;
|
||||
final double? width;
|
||||
|
||||
const UriCacheInfoImage({
|
||||
super.key,
|
||||
required this.cache,
|
||||
this.fit = BoxFit.cover,
|
||||
this.placeholderStyle = PlaceholderStyle.color,
|
||||
this.height,
|
||||
this.width,
|
||||
});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return CachedNetworkImage(
|
||||
imageUrl: cache.uri.toString(),
|
||||
cacheKey: cache.cacheKey,
|
||||
cacheManager: cache.cacheManager,
|
||||
fit: fit,
|
||||
height: height,
|
||||
width: width,
|
||||
fadeInDuration: const Duration(milliseconds: 300),
|
||||
fadeOutDuration: const Duration(milliseconds: 500),
|
||||
placeholder: (context, url) =>
|
||||
placeholderStyle == PlaceholderStyle.spinner
|
||||
? Container()
|
||||
: Container(
|
||||
color: Theme.of(context).colorScheme.secondaryContainer,
|
||||
),
|
||||
errorWidget: (context, url, error) => PlaceholderImage(
|
||||
fit: fit,
|
||||
height: height,
|
||||
width: width,
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class PlaceholderImage extends HookConsumerWidget {
|
||||
final BoxFit fit;
|
||||
final double? height;
|
||||
final double? width;
|
||||
final bool thumbnail;
|
||||
|
||||
const PlaceholderImage({
|
||||
super.key,
|
||||
this.fit = BoxFit.cover,
|
||||
this.height,
|
||||
this.width,
|
||||
this.thumbnail = true,
|
||||
});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context, WidgetRef ref) {
|
||||
return Image.asset(
|
||||
thumbnail ? 'assets/placeholder_thumb.png' : 'assets/placeholder.png',
|
||||
fit: fit,
|
||||
height: height,
|
||||
width: width,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class _ExpandedRatio extends StatelessWidget {
|
||||
final Widget child;
|
||||
final double aspectRatio;
|
||||
|
||||
const _ExpandedRatio({
|
||||
required this.child,
|
||||
this.aspectRatio = 1.0,
|
||||
});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Expanded(
|
||||
child: AspectRatio(
|
||||
aspectRatio: aspectRatio,
|
||||
child: child,
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class MultiImage extends HookConsumerWidget {
|
||||
final Iterable<UriCacheInfo> cacheInfo;
|
||||
|
||||
const MultiImage({
|
||||
super.key,
|
||||
required this.cacheInfo,
|
||||
});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context, WidgetRef ref) {
|
||||
final images = cacheInfo.map((cache) => UriCacheInfoImage(cache: cache));
|
||||
|
||||
final row1 = <Widget>[];
|
||||
final row2 = <Widget>[];
|
||||
|
||||
if (images.length >= 4) {
|
||||
row1.addAll([
|
||||
_ExpandedRatio(child: images.elementAt(0)),
|
||||
_ExpandedRatio(child: images.elementAt(1)),
|
||||
]);
|
||||
row2.addAll([
|
||||
_ExpandedRatio(child: images.elementAt(2)),
|
||||
_ExpandedRatio(child: images.elementAt(3)),
|
||||
]);
|
||||
}
|
||||
if (images.length == 3) {
|
||||
row1.addAll([
|
||||
_ExpandedRatio(child: images.elementAt(0)),
|
||||
_ExpandedRatio(child: images.elementAt(1)),
|
||||
]);
|
||||
row2.addAll([
|
||||
_ExpandedRatio(aspectRatio: 2.0, child: images.elementAt(2)),
|
||||
]);
|
||||
}
|
||||
if (images.length == 2) {
|
||||
row1.add(_ExpandedRatio(aspectRatio: 2.0, child: images.elementAt(0)));
|
||||
row2.add(_ExpandedRatio(aspectRatio: 2.0, child: images.elementAt(1)));
|
||||
}
|
||||
if (images.length == 1) {
|
||||
row1.addAll([_ExpandedRatio(child: images.elementAt(0))]);
|
||||
}
|
||||
|
||||
return Column(
|
||||
children: [
|
||||
Row(children: row1),
|
||||
Row(children: row2),
|
||||
],
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -1,307 +0,0 @@
|
||||
// GENERATED CODE - DO NOT MODIFY BY HAND
|
||||
|
||||
part of 'images.dart';
|
||||
|
||||
// **************************************************************************
|
||||
// RiverpodGenerator
|
||||
// **************************************************************************
|
||||
|
||||
String _$artistArtCacheInfoHash() =>
|
||||
r'f82d3e91aa1596939e376c6a7ea7d3e974c6f0fc';
|
||||
|
||||
/// Copied from Dart SDK
|
||||
class _SystemHash {
|
||||
_SystemHash._();
|
||||
|
||||
static int combine(int hash, int value) {
|
||||
// ignore: parameter_assignments
|
||||
hash = 0x1fffffff & (hash + value);
|
||||
// ignore: parameter_assignments
|
||||
hash = 0x1fffffff & (hash + ((0x0007ffff & hash) << 10));
|
||||
return hash ^ (hash >> 6);
|
||||
}
|
||||
|
||||
static int finish(int hash) {
|
||||
// ignore: parameter_assignments
|
||||
hash = 0x1fffffff & (hash + ((0x03ffffff & hash) << 3));
|
||||
// ignore: parameter_assignments
|
||||
hash = hash ^ (hash >> 11);
|
||||
return 0x1fffffff & (hash + ((0x00003fff & hash) << 15));
|
||||
}
|
||||
}
|
||||
|
||||
typedef _ArtistArtCacheInfoRef = AutoDisposeProviderRef<CacheInfo>;
|
||||
|
||||
/// See also [_artistArtCacheInfo].
|
||||
@ProviderFor(_artistArtCacheInfo)
|
||||
const _artistArtCacheInfoProvider = _ArtistArtCacheInfoFamily();
|
||||
|
||||
/// See also [_artistArtCacheInfo].
|
||||
class _ArtistArtCacheInfoFamily extends Family<CacheInfo> {
|
||||
/// See also [_artistArtCacheInfo].
|
||||
const _ArtistArtCacheInfoFamily();
|
||||
|
||||
/// See also [_artistArtCacheInfo].
|
||||
_ArtistArtCacheInfoProvider call({
|
||||
required String artistId,
|
||||
bool thumbnail = true,
|
||||
}) {
|
||||
return _ArtistArtCacheInfoProvider(
|
||||
artistId: artistId,
|
||||
thumbnail: thumbnail,
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
_ArtistArtCacheInfoProvider getProviderOverride(
|
||||
covariant _ArtistArtCacheInfoProvider provider,
|
||||
) {
|
||||
return call(
|
||||
artistId: provider.artistId,
|
||||
thumbnail: provider.thumbnail,
|
||||
);
|
||||
}
|
||||
|
||||
static const Iterable<ProviderOrFamily>? _dependencies = null;
|
||||
|
||||
@override
|
||||
Iterable<ProviderOrFamily>? get dependencies => _dependencies;
|
||||
|
||||
static const Iterable<ProviderOrFamily>? _allTransitiveDependencies = null;
|
||||
|
||||
@override
|
||||
Iterable<ProviderOrFamily>? get allTransitiveDependencies =>
|
||||
_allTransitiveDependencies;
|
||||
|
||||
@override
|
||||
String? get name => r'_artistArtCacheInfoProvider';
|
||||
}
|
||||
|
||||
/// See also [_artistArtCacheInfo].
|
||||
class _ArtistArtCacheInfoProvider extends AutoDisposeProvider<CacheInfo> {
|
||||
/// See also [_artistArtCacheInfo].
|
||||
_ArtistArtCacheInfoProvider({
|
||||
required this.artistId,
|
||||
this.thumbnail = true,
|
||||
}) : super.internal(
|
||||
(ref) => _artistArtCacheInfo(
|
||||
ref,
|
||||
artistId: artistId,
|
||||
thumbnail: thumbnail,
|
||||
),
|
||||
from: _artistArtCacheInfoProvider,
|
||||
name: r'_artistArtCacheInfoProvider',
|
||||
debugGetCreateSourceHash:
|
||||
const bool.fromEnvironment('dart.vm.product')
|
||||
? null
|
||||
: _$artistArtCacheInfoHash,
|
||||
dependencies: _ArtistArtCacheInfoFamily._dependencies,
|
||||
allTransitiveDependencies:
|
||||
_ArtistArtCacheInfoFamily._allTransitiveDependencies,
|
||||
);
|
||||
|
||||
final String artistId;
|
||||
final bool thumbnail;
|
||||
|
||||
@override
|
||||
bool operator ==(Object other) {
|
||||
return other is _ArtistArtCacheInfoProvider &&
|
||||
other.artistId == artistId &&
|
||||
other.thumbnail == thumbnail;
|
||||
}
|
||||
|
||||
@override
|
||||
int get hashCode {
|
||||
var hash = _SystemHash.combine(0, runtimeType.hashCode);
|
||||
hash = _SystemHash.combine(hash, artistId.hashCode);
|
||||
hash = _SystemHash.combine(hash, thumbnail.hashCode);
|
||||
|
||||
return _SystemHash.finish(hash);
|
||||
}
|
||||
}
|
||||
|
||||
String _$artistArtCachedUrlHash() =>
|
||||
r'2a5e0fea614ff12a1d562faccec6cfe98394af42';
|
||||
typedef _ArtistArtCachedUrlRef = AutoDisposeFutureProviderRef<String?>;
|
||||
|
||||
/// See also [_artistArtCachedUrl].
|
||||
@ProviderFor(_artistArtCachedUrl)
|
||||
const _artistArtCachedUrlProvider = _ArtistArtCachedUrlFamily();
|
||||
|
||||
/// See also [_artistArtCachedUrl].
|
||||
class _ArtistArtCachedUrlFamily extends Family<AsyncValue<String?>> {
|
||||
/// See also [_artistArtCachedUrl].
|
||||
const _ArtistArtCachedUrlFamily();
|
||||
|
||||
/// See also [_artistArtCachedUrl].
|
||||
_ArtistArtCachedUrlProvider call({
|
||||
required String artistId,
|
||||
bool thumbnail = true,
|
||||
}) {
|
||||
return _ArtistArtCachedUrlProvider(
|
||||
artistId: artistId,
|
||||
thumbnail: thumbnail,
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
_ArtistArtCachedUrlProvider getProviderOverride(
|
||||
covariant _ArtistArtCachedUrlProvider provider,
|
||||
) {
|
||||
return call(
|
||||
artistId: provider.artistId,
|
||||
thumbnail: provider.thumbnail,
|
||||
);
|
||||
}
|
||||
|
||||
static const Iterable<ProviderOrFamily>? _dependencies = null;
|
||||
|
||||
@override
|
||||
Iterable<ProviderOrFamily>? get dependencies => _dependencies;
|
||||
|
||||
static const Iterable<ProviderOrFamily>? _allTransitiveDependencies = null;
|
||||
|
||||
@override
|
||||
Iterable<ProviderOrFamily>? get allTransitiveDependencies =>
|
||||
_allTransitiveDependencies;
|
||||
|
||||
@override
|
||||
String? get name => r'_artistArtCachedUrlProvider';
|
||||
}
|
||||
|
||||
/// See also [_artistArtCachedUrl].
|
||||
class _ArtistArtCachedUrlProvider extends AutoDisposeFutureProvider<String?> {
|
||||
/// See also [_artistArtCachedUrl].
|
||||
_ArtistArtCachedUrlProvider({
|
||||
required this.artistId,
|
||||
this.thumbnail = true,
|
||||
}) : super.internal(
|
||||
(ref) => _artistArtCachedUrl(
|
||||
ref,
|
||||
artistId: artistId,
|
||||
thumbnail: thumbnail,
|
||||
),
|
||||
from: _artistArtCachedUrlProvider,
|
||||
name: r'_artistArtCachedUrlProvider',
|
||||
debugGetCreateSourceHash:
|
||||
const bool.fromEnvironment('dart.vm.product')
|
||||
? null
|
||||
: _$artistArtCachedUrlHash,
|
||||
dependencies: _ArtistArtCachedUrlFamily._dependencies,
|
||||
allTransitiveDependencies:
|
||||
_ArtistArtCachedUrlFamily._allTransitiveDependencies,
|
||||
);
|
||||
|
||||
final String artistId;
|
||||
final bool thumbnail;
|
||||
|
||||
@override
|
||||
bool operator ==(Object other) {
|
||||
return other is _ArtistArtCachedUrlProvider &&
|
||||
other.artistId == artistId &&
|
||||
other.thumbnail == thumbnail;
|
||||
}
|
||||
|
||||
@override
|
||||
int get hashCode {
|
||||
var hash = _SystemHash.combine(0, runtimeType.hashCode);
|
||||
hash = _SystemHash.combine(hash, artistId.hashCode);
|
||||
hash = _SystemHash.combine(hash, thumbnail.hashCode);
|
||||
|
||||
return _SystemHash.finish(hash);
|
||||
}
|
||||
}
|
||||
|
||||
String _$artistArtUriCacheInfoHash() =>
|
||||
r'9bdc0f5654882265236ef746ea697a6d107a4b6f';
|
||||
typedef _ArtistArtUriCacheInfoRef = AutoDisposeFutureProviderRef<UriCacheInfo>;
|
||||
|
||||
/// See also [_artistArtUriCacheInfo].
|
||||
@ProviderFor(_artistArtUriCacheInfo)
|
||||
const _artistArtUriCacheInfoProvider = _ArtistArtUriCacheInfoFamily();
|
||||
|
||||
/// See also [_artistArtUriCacheInfo].
|
||||
class _ArtistArtUriCacheInfoFamily extends Family<AsyncValue<UriCacheInfo>> {
|
||||
/// See also [_artistArtUriCacheInfo].
|
||||
const _ArtistArtUriCacheInfoFamily();
|
||||
|
||||
/// See also [_artistArtUriCacheInfo].
|
||||
_ArtistArtUriCacheInfoProvider call({
|
||||
required String artistId,
|
||||
bool thumbnail = true,
|
||||
}) {
|
||||
return _ArtistArtUriCacheInfoProvider(
|
||||
artistId: artistId,
|
||||
thumbnail: thumbnail,
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
_ArtistArtUriCacheInfoProvider getProviderOverride(
|
||||
covariant _ArtistArtUriCacheInfoProvider provider,
|
||||
) {
|
||||
return call(
|
||||
artistId: provider.artistId,
|
||||
thumbnail: provider.thumbnail,
|
||||
);
|
||||
}
|
||||
|
||||
static const Iterable<ProviderOrFamily>? _dependencies = null;
|
||||
|
||||
@override
|
||||
Iterable<ProviderOrFamily>? get dependencies => _dependencies;
|
||||
|
||||
static const Iterable<ProviderOrFamily>? _allTransitiveDependencies = null;
|
||||
|
||||
@override
|
||||
Iterable<ProviderOrFamily>? get allTransitiveDependencies =>
|
||||
_allTransitiveDependencies;
|
||||
|
||||
@override
|
||||
String? get name => r'_artistArtUriCacheInfoProvider';
|
||||
}
|
||||
|
||||
/// See also [_artistArtUriCacheInfo].
|
||||
class _ArtistArtUriCacheInfoProvider
|
||||
extends AutoDisposeFutureProvider<UriCacheInfo> {
|
||||
/// See also [_artistArtUriCacheInfo].
|
||||
_ArtistArtUriCacheInfoProvider({
|
||||
required this.artistId,
|
||||
this.thumbnail = true,
|
||||
}) : super.internal(
|
||||
(ref) => _artistArtUriCacheInfo(
|
||||
ref,
|
||||
artistId: artistId,
|
||||
thumbnail: thumbnail,
|
||||
),
|
||||
from: _artistArtUriCacheInfoProvider,
|
||||
name: r'_artistArtUriCacheInfoProvider',
|
||||
debugGetCreateSourceHash:
|
||||
const bool.fromEnvironment('dart.vm.product')
|
||||
? null
|
||||
: _$artistArtUriCacheInfoHash,
|
||||
dependencies: _ArtistArtUriCacheInfoFamily._dependencies,
|
||||
allTransitiveDependencies:
|
||||
_ArtistArtUriCacheInfoFamily._allTransitiveDependencies,
|
||||
);
|
||||
|
||||
final String artistId;
|
||||
final bool thumbnail;
|
||||
|
||||
@override
|
||||
bool operator ==(Object other) {
|
||||
return other is _ArtistArtUriCacheInfoProvider &&
|
||||
other.artistId == artistId &&
|
||||
other.thumbnail == thumbnail;
|
||||
}
|
||||
|
||||
@override
|
||||
int get hashCode {
|
||||
var hash = _SystemHash.combine(0, runtimeType.hashCode);
|
||||
hash = _SystemHash.combine(hash, artistId.hashCode);
|
||||
hash = _SystemHash.combine(hash, thumbnail.hashCode);
|
||||
|
||||
return _SystemHash.finish(hash);
|
||||
}
|
||||
}
|
||||
// ignore_for_file: unnecessary_raw_strings, subtype_of_sealed_class, invalid_use_of_internal_member, do_not_use_environment, prefer_const_constructors, public_member_api_docs, avoid_private_typedef_functions
|
||||
@@ -1,433 +0,0 @@
|
||||
import 'package:collection/collection.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_gen/gen_l10n/app_localizations.dart';
|
||||
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
||||
|
||||
import '../models/music.dart';
|
||||
import '../services/cache_service.dart';
|
||||
import '../services/download_service.dart';
|
||||
import '../state/audio.dart';
|
||||
import '../state/music.dart';
|
||||
import '../state/theme.dart';
|
||||
import 'context_menus.dart';
|
||||
import 'images.dart';
|
||||
import 'pages/songs_page.dart';
|
||||
|
||||
enum CardStyle {
|
||||
imageOnly,
|
||||
withText,
|
||||
}
|
||||
|
||||
enum AlbumSubtitle {
|
||||
artist,
|
||||
year,
|
||||
}
|
||||
|
||||
class AlbumCard extends HookConsumerWidget {
|
||||
final Album album;
|
||||
final void Function()? onTap;
|
||||
final CardStyle style;
|
||||
final AlbumSubtitle subtitle;
|
||||
|
||||
const AlbumCard({
|
||||
super.key,
|
||||
required this.album,
|
||||
this.onTap,
|
||||
this.style = CardStyle.withText,
|
||||
this.subtitle = AlbumSubtitle.artist,
|
||||
});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context, WidgetRef ref) {
|
||||
// generate the palette used in other views ahead of time
|
||||
ref.watch(albumArtPaletteProvider(album.id));
|
||||
final cache = ref.watch(cacheServiceProvider);
|
||||
final info = cache.albumArt(album);
|
||||
|
||||
final image = CardClip(child: UriCacheInfoImage(cache: info));
|
||||
Widget content;
|
||||
if (style == CardStyle.imageOnly) {
|
||||
content = image;
|
||||
} else {
|
||||
content = Column(
|
||||
children: [
|
||||
image,
|
||||
_AlbumCardText(album: album, subtitle: subtitle),
|
||||
],
|
||||
);
|
||||
}
|
||||
|
||||
return ImageCard(
|
||||
onTap: onTap,
|
||||
onLongPress: () {
|
||||
showContextMenu(
|
||||
context: context,
|
||||
ref: ref,
|
||||
builder: (context) => BottomSheetMenu(
|
||||
child: AlbumContextMenu(album: album),
|
||||
),
|
||||
);
|
||||
},
|
||||
child: content,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class ImageCard extends StatelessWidget {
|
||||
final Widget child;
|
||||
final void Function()? onTap;
|
||||
final void Function()? onLongPress;
|
||||
|
||||
const ImageCard({
|
||||
super.key,
|
||||
required this.child,
|
||||
this.onTap,
|
||||
this.onLongPress,
|
||||
});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Card(
|
||||
surfaceTintColor: Colors.transparent,
|
||||
margin: const EdgeInsets.all(0),
|
||||
child: Stack(
|
||||
fit: StackFit.passthrough,
|
||||
alignment: Alignment.bottomCenter,
|
||||
children: [
|
||||
child,
|
||||
Positioned.fill(
|
||||
child: Material(
|
||||
color: Colors.transparent,
|
||||
child: InkWell(
|
||||
onTap: onTap,
|
||||
onLongPress: onLongPress,
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class _AlbumCardText extends StatelessWidget {
|
||||
final Album album;
|
||||
final AlbumSubtitle subtitle;
|
||||
|
||||
const _AlbumCardText({
|
||||
required this.album,
|
||||
required this.subtitle,
|
||||
});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final textTheme = Theme.of(context).textTheme;
|
||||
|
||||
return Container(
|
||||
padding: const EdgeInsets.only(top: 4, bottom: 8),
|
||||
width: double.infinity,
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
SizedBox(
|
||||
width: double.infinity,
|
||||
child: Text(
|
||||
album.name,
|
||||
maxLines: 1,
|
||||
softWrap: false,
|
||||
overflow: TextOverflow.fade,
|
||||
textAlign: TextAlign.start,
|
||||
style: textTheme.bodyMedium!.copyWith(
|
||||
fontWeight: FontWeight.bold,
|
||||
),
|
||||
),
|
||||
),
|
||||
Text(
|
||||
(subtitle == AlbumSubtitle.artist
|
||||
? album.albumArtist
|
||||
: album.year?.toString()) ??
|
||||
'',
|
||||
maxLines: 1,
|
||||
softWrap: false,
|
||||
overflow: TextOverflow.fade,
|
||||
textAlign: TextAlign.start,
|
||||
style: textTheme.bodySmall,
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class AlbumListTile extends HookConsumerWidget {
|
||||
final Album album;
|
||||
final void Function()? onTap;
|
||||
|
||||
const AlbumListTile({
|
||||
super.key,
|
||||
required this.album,
|
||||
this.onTap,
|
||||
});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context, WidgetRef ref) {
|
||||
final artist = ref.watch(albumProvider(album.artistId!)).valueOrNull;
|
||||
|
||||
return ListTile(
|
||||
leading: AlbumArt(album: album),
|
||||
title: Text(album.name),
|
||||
subtitle: Text(album.albumArtist ?? artist!.name),
|
||||
onTap: onTap,
|
||||
onLongPress: () {
|
||||
showContextMenu(
|
||||
context: context,
|
||||
ref: ref,
|
||||
builder: (context) => BottomSheetMenu(
|
||||
size: MenuSize.small,
|
||||
child: AlbumContextMenu(album: album),
|
||||
),
|
||||
);
|
||||
},
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class ArtistListTile extends HookConsumerWidget {
|
||||
final Artist artist;
|
||||
final void Function()? onTap;
|
||||
|
||||
const ArtistListTile({
|
||||
super.key,
|
||||
required this.artist,
|
||||
this.onTap,
|
||||
});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context, WidgetRef ref) {
|
||||
return ListTile(
|
||||
leading: CircleClip(
|
||||
child: ArtistArtImage(artistId: artist.id),
|
||||
),
|
||||
title: Text(artist.name),
|
||||
subtitle: Text(AppLocalizations.of(context).resourcesAlbumCount(
|
||||
artist.albumCount,
|
||||
)),
|
||||
onTap: onTap,
|
||||
onLongPress: () {
|
||||
showContextMenu(
|
||||
context: context,
|
||||
ref: ref,
|
||||
builder: (context) => BottomSheetMenu(
|
||||
size: MenuSize.small,
|
||||
child: ArtistContextMenu(artist: artist),
|
||||
),
|
||||
);
|
||||
},
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class PlaylistListTile extends HookConsumerWidget {
|
||||
final Playlist playlist;
|
||||
final void Function()? onTap;
|
||||
|
||||
const PlaylistListTile({
|
||||
super.key,
|
||||
required this.playlist,
|
||||
this.onTap,
|
||||
});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context, WidgetRef ref) {
|
||||
// generate the palette used in other views ahead of time
|
||||
ref.watch(playlistArtPaletteProvider(playlist.id));
|
||||
final cache = ref.watch(cacheServiceProvider).playlistArt(playlist);
|
||||
|
||||
return ListTile(
|
||||
leading: CardClip(
|
||||
child: UriCacheInfoImage(cache: cache),
|
||||
),
|
||||
title: Text(playlist.name),
|
||||
subtitle: Text(AppLocalizations.of(context).resourcesSongCount(
|
||||
playlist.songCount,
|
||||
)),
|
||||
onTap: onTap,
|
||||
onLongPress: () {
|
||||
showContextMenu(
|
||||
context: context,
|
||||
ref: ref,
|
||||
builder: (context) => BottomSheetMenu(
|
||||
size: MenuSize.small,
|
||||
child: PlaylistContextMenu(playlist: playlist),
|
||||
),
|
||||
);
|
||||
},
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class SongListTile extends HookConsumerWidget {
|
||||
final Song song;
|
||||
final void Function()? onTap;
|
||||
final bool image;
|
||||
|
||||
const SongListTile({
|
||||
super.key,
|
||||
required this.song,
|
||||
this.onTap,
|
||||
this.image = false,
|
||||
});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context, WidgetRef ref) {
|
||||
return Material(
|
||||
type: MaterialType.transparency,
|
||||
child: ListTile(
|
||||
title: _SongTitle(song: song),
|
||||
subtitle: _SongSubtitle(song: song),
|
||||
leading: image ? SongAlbumArt(song: song) : null,
|
||||
trailing: IconButton(
|
||||
icon: const Icon(
|
||||
Icons.star_outline_rounded,
|
||||
size: 36,
|
||||
),
|
||||
onPressed: () {},
|
||||
),
|
||||
onTap: onTap,
|
||||
onLongPress: () {
|
||||
showContextMenu(
|
||||
context: context,
|
||||
ref: ref,
|
||||
builder: (context) => BottomSheetMenu(
|
||||
child: SongContextMenu(song: song),
|
||||
),
|
||||
);
|
||||
},
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class _SongSubtitle extends HookConsumerWidget {
|
||||
final Song song;
|
||||
|
||||
const _SongSubtitle({
|
||||
required this.song,
|
||||
});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context, WidgetRef ref) {
|
||||
final downloadTaskId = ref.watch(songProvider(song.id).select(
|
||||
(value) => value.valueOrNull?.downloadTaskId,
|
||||
));
|
||||
final downloadFilePath = ref.watch(songProvider(song.id).select(
|
||||
(value) => value.valueOrNull?.downloadFilePath,
|
||||
));
|
||||
final download = ref.watch(downloadServiceProvider.select(
|
||||
(value) => value.downloads.firstWhereOrNull(
|
||||
(e) => e.taskId == downloadTaskId,
|
||||
),
|
||||
));
|
||||
|
||||
final inheritedStyle = DefaultTextStyle.of(context).style;
|
||||
|
||||
Widget? downloadIndicator;
|
||||
if (downloadFilePath != null) {
|
||||
downloadIndicator = const Padding(
|
||||
padding: EdgeInsetsDirectional.only(end: 3),
|
||||
child: Icon(
|
||||
Icons.download_done_rounded,
|
||||
size: 20,
|
||||
),
|
||||
);
|
||||
} else if (downloadTaskId != null || download != null) {
|
||||
downloadIndicator = Padding(
|
||||
padding: const EdgeInsetsDirectional.only(start: 4, end: 9),
|
||||
child: SizedBox(
|
||||
height: 10,
|
||||
width: 10,
|
||||
child: CircularProgressIndicator(
|
||||
strokeWidth: 2,
|
||||
value: download != null && download.progress > 0
|
||||
? download.progress / 100
|
||||
: null,
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
return Row(
|
||||
children: [
|
||||
if (downloadIndicator != null) downloadIndicator,
|
||||
Expanded(
|
||||
child: Text(
|
||||
song.artist ?? song.album ?? '',
|
||||
maxLines: 1,
|
||||
softWrap: false,
|
||||
overflow: TextOverflow.fade,
|
||||
style: TextStyle(
|
||||
color: inheritedStyle.color,
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class _SongTitle extends HookConsumerWidget {
|
||||
final Song song;
|
||||
|
||||
const _SongTitle({
|
||||
required this.song,
|
||||
});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context, WidgetRef ref) {
|
||||
final mediaItem = ref.watch(mediaItemProvider).valueOrNull;
|
||||
final mediaItemData = ref.watch(mediaItemDataProvider);
|
||||
final inheritedStyle = DefaultTextStyle.of(context).style;
|
||||
final theme = Theme.of(context);
|
||||
final queueContext = QueueContext.maybeOf(context);
|
||||
|
||||
final playing = mediaItem != null &&
|
||||
mediaItemData != null &&
|
||||
mediaItem.id == song.id &&
|
||||
mediaItemData.contextId == queueContext?.id &&
|
||||
mediaItemData.contextType == queueContext?.type;
|
||||
|
||||
return Row(
|
||||
children: [
|
||||
if (playing)
|
||||
Padding(
|
||||
padding: const EdgeInsetsDirectional.only(end: 2),
|
||||
child: Icon(
|
||||
Icons.play_arrow_rounded,
|
||||
size: 18,
|
||||
color: theme.colorScheme.primary,
|
||||
),
|
||||
),
|
||||
Expanded(
|
||||
child: Text(
|
||||
song.title,
|
||||
maxLines: 1,
|
||||
softWrap: false,
|
||||
overflow: TextOverflow.fade,
|
||||
style: TextStyle(
|
||||
color: playing ? theme.colorScheme.primary : inheritedStyle.color,
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class FabPadding extends StatelessWidget {
|
||||
const FabPadding({super.key});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return const SizedBox(height: 86);
|
||||
}
|
||||
}
|
||||
@@ -1,136 +0,0 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
||||
import 'package:infinite_scroll_pagination/infinite_scroll_pagination.dart';
|
||||
|
||||
import '../services/sync_service.dart';
|
||||
import 'items.dart';
|
||||
import 'snackbars.dart';
|
||||
|
||||
class PagedListQueryView<T> extends HookConsumerWidget {
|
||||
final PagingController<int, T> pagingController;
|
||||
final bool refreshSyncAll;
|
||||
final bool fabPadding;
|
||||
final bool useSliver;
|
||||
final Widget Function(BuildContext context, T item, int index) itemBuilder;
|
||||
|
||||
const PagedListQueryView({
|
||||
super.key,
|
||||
required this.pagingController,
|
||||
this.refreshSyncAll = false,
|
||||
this.fabPadding = true,
|
||||
this.useSliver = false,
|
||||
required this.itemBuilder,
|
||||
});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context, WidgetRef ref) {
|
||||
final builderDelegate = PagedChildBuilderDelegate<T>(
|
||||
itemBuilder: (context, item, index) => itemBuilder(context, item, index),
|
||||
noMoreItemsIndicatorBuilder:
|
||||
fabPadding ? (context) => const FabPadding() : null,
|
||||
);
|
||||
|
||||
final listView = useSliver
|
||||
? PagedSliverList<int, T>(
|
||||
pagingController: pagingController,
|
||||
builderDelegate: builderDelegate,
|
||||
)
|
||||
: PagedListView<int, T>(
|
||||
pagingController: pagingController,
|
||||
builderDelegate: builderDelegate,
|
||||
);
|
||||
|
||||
if (refreshSyncAll) {
|
||||
return SyncAllRefresh(child: listView);
|
||||
} else {
|
||||
return listView;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
enum GridSize {
|
||||
small,
|
||||
large,
|
||||
}
|
||||
|
||||
class PagedGridQueryView<T> extends HookConsumerWidget {
|
||||
final PagingController<int, T> pagingController;
|
||||
final bool refreshSyncAll;
|
||||
final bool fabPadding;
|
||||
final GridSize size;
|
||||
final Widget Function(BuildContext context, T item, int index, GridSize size)
|
||||
itemBuilder;
|
||||
|
||||
const PagedGridQueryView({
|
||||
super.key,
|
||||
required this.pagingController,
|
||||
this.refreshSyncAll = false,
|
||||
this.fabPadding = true,
|
||||
this.size = GridSize.small,
|
||||
required this.itemBuilder,
|
||||
});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context, WidgetRef ref) {
|
||||
SliverGridDelegate gridDelegate;
|
||||
double spacing;
|
||||
|
||||
if (size == GridSize.small) {
|
||||
spacing = 4;
|
||||
gridDelegate = SliverGridDelegateWithFixedCrossAxisCount(
|
||||
crossAxisCount: 3,
|
||||
mainAxisSpacing: spacing,
|
||||
crossAxisSpacing: spacing,
|
||||
);
|
||||
} else {
|
||||
spacing = 12;
|
||||
gridDelegate = SliverGridDelegateWithFixedCrossAxisCount(
|
||||
crossAxisCount: 2,
|
||||
mainAxisSpacing: spacing,
|
||||
crossAxisSpacing: spacing,
|
||||
);
|
||||
}
|
||||
|
||||
final listView = PagedGridView<int, T>(
|
||||
padding: MediaQuery.of(context).padding + EdgeInsets.all(spacing),
|
||||
pagingController: pagingController,
|
||||
builderDelegate: PagedChildBuilderDelegate(
|
||||
itemBuilder: (context, item, index) =>
|
||||
itemBuilder(context, item, index, size),
|
||||
noMoreItemsIndicatorBuilder:
|
||||
fabPadding ? (context) => const FabPadding() : null,
|
||||
),
|
||||
gridDelegate: gridDelegate,
|
||||
showNoMoreItemsIndicatorAsGridChild: false,
|
||||
);
|
||||
|
||||
if (refreshSyncAll) {
|
||||
return SyncAllRefresh(child: listView);
|
||||
} else {
|
||||
return listView;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
class SyncAllRefresh extends HookConsumerWidget {
|
||||
final Widget child;
|
||||
|
||||
const SyncAllRefresh({
|
||||
super.key,
|
||||
required this.child,
|
||||
});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context, WidgetRef ref) {
|
||||
return RefreshIndicator(
|
||||
onRefresh: () async {
|
||||
try {
|
||||
await ref.read(syncServiceProvider.notifier).syncAll();
|
||||
} catch (e) {
|
||||
showErrorSnackbar(context, e.toString());
|
||||
}
|
||||
},
|
||||
child: child,
|
||||
);
|
||||
}
|
||||
}
|
||||