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
|
.gradle/
|
||||||
*.class
|
build/
|
||||||
*.log
|
local.properties
|
||||||
*.pyc
|
|
||||||
*.swp
|
*.apk
|
||||||
.DS_Store
|
*.aab
|
||||||
.atom/
|
captures/
|
||||||
.buildlog/
|
.cxx/
|
||||||
.history
|
|
||||||
.svn/
|
|
||||||
migrate_working_dir/
|
|
||||||
|
|
||||||
# IntelliJ related
|
|
||||||
*.iml
|
|
||||||
*.ipr
|
|
||||||
*.iws
|
|
||||||
.idea/
|
.idea/
|
||||||
|
*.iml
|
||||||
|
.vscode/
|
||||||
|
|
||||||
# The .vscode folder contains launch configuration and tasks you configure in
|
.kotlin/
|
||||||
# VS Code which you may wish to be included in version control, so this line
|
.preview/
|
||||||
# is commented out by default.
|
|
||||||
#.vscode/
|
|
||||||
|
|
||||||
# Flutter/Dart/Pub related
|
.DS_Store
|
||||||
**/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
|
|
||||||
|
|||||||
@@ -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
|
distributionBase=GRADLE_USER_HOME
|
||||||
distributionPath=wrapper/dists
|
distributionPath=wrapper/dists
|
||||||
|
distributionUrl=https\://services.gradle.org/distributions/gradle-8.11.1-bin.zip
|
||||||
|
networkTimeout=10000
|
||||||
|
validateDistributionUrl=true
|
||||||
zipStoreBase=GRADLE_USER_HOME
|
zipStoreBase=GRADLE_USER_HOME
|
||||||
zipStorePath=wrapper/dists
|
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,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||