reboot
@ -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": {}
|
|
||||||
}
|
|
||||||
11
.gitignore
vendored
@ -5,9 +5,11 @@
|
|||||||
*.swp
|
*.swp
|
||||||
.DS_Store
|
.DS_Store
|
||||||
.atom/
|
.atom/
|
||||||
|
.build/
|
||||||
.buildlog/
|
.buildlog/
|
||||||
.history
|
.history
|
||||||
.svn/
|
.svn/
|
||||||
|
.swiftpm/
|
||||||
migrate_working_dir/
|
migrate_working_dir/
|
||||||
|
|
||||||
# IntelliJ related
|
# IntelliJ related
|
||||||
@ -25,12 +27,11 @@ migrate_working_dir/
|
|||||||
**/doc/api/
|
**/doc/api/
|
||||||
**/ios/Flutter/.last_build_id
|
**/ios/Flutter/.last_build_id
|
||||||
.dart_tool/
|
.dart_tool/
|
||||||
.flutter-plugins
|
|
||||||
.flutter-plugins-dependencies
|
.flutter-plugins-dependencies
|
||||||
.packages
|
|
||||||
.pub-cache/
|
.pub-cache/
|
||||||
.pub/
|
.pub/
|
||||||
/build/
|
/build/
|
||||||
|
/coverage/
|
||||||
|
|
||||||
# Symbolication related
|
# Symbolication related
|
||||||
app.*.symbols
|
app.*.symbols
|
||||||
@ -43,7 +44,5 @@ app.*.map.json
|
|||||||
/android/app/profile
|
/android/app/profile
|
||||||
/android/app/release
|
/android/app/release
|
||||||
|
|
||||||
/.env
|
# VSCode
|
||||||
*.sqlite*
|
.vscode/settings.json
|
||||||
/.fvm/flutter_sdk
|
|
||||||
*.keystore
|
|
||||||
|
|||||||
14
.metadata
@ -1,11 +1,11 @@
|
|||||||
# This file tracks properties of this Flutter project.
|
# This file tracks properties of this Flutter project.
|
||||||
# Used by Flutter tool to assess capabilities and perform upgrades etc.
|
# Used by Flutter tool to assess capabilities and perform upgrades etc.
|
||||||
#
|
#
|
||||||
# This file should be version controlled.
|
# This file should be version controlled and should not be manually edited.
|
||||||
|
|
||||||
version:
|
version:
|
||||||
revision: 9944297138845a94256f1cf37beb88ff9a8e811a
|
revision: "9f455d2486bcb28cad87b062475f42edc959f636"
|
||||||
channel: stable
|
channel: "stable"
|
||||||
|
|
||||||
project_type: app
|
project_type: app
|
||||||
|
|
||||||
@ -13,11 +13,11 @@ project_type: app
|
|||||||
migration:
|
migration:
|
||||||
platforms:
|
platforms:
|
||||||
- platform: root
|
- platform: root
|
||||||
create_revision: 9944297138845a94256f1cf37beb88ff9a8e811a
|
create_revision: 9f455d2486bcb28cad87b062475f42edc959f636
|
||||||
base_revision: 9944297138845a94256f1cf37beb88ff9a8e811a
|
base_revision: 9f455d2486bcb28cad87b062475f42edc959f636
|
||||||
- platform: android
|
- platform: android
|
||||||
create_revision: 9944297138845a94256f1cf37beb88ff9a8e811a
|
create_revision: 9f455d2486bcb28cad87b062475f42edc959f636
|
||||||
base_revision: 9944297138845a94256f1cf37beb88ff9a8e811a
|
base_revision: 9f455d2486bcb28cad87b062475f42edc959f636
|
||||||
|
|
||||||
# User provided section
|
# User provided section
|
||||||
|
|
||||||
|
|||||||
@ -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,19 +1 @@
|
|||||||
include: package:flutter_lints/flutter.yaml
|
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
|
|
||||||
|
|||||||
3
android/.gitignore
vendored
@ -5,9 +5,10 @@ gradle-wrapper.jar
|
|||||||
/gradlew.bat
|
/gradlew.bat
|
||||||
/local.properties
|
/local.properties
|
||||||
GeneratedPluginRegistrant.java
|
GeneratedPluginRegistrant.java
|
||||||
|
.cxx/
|
||||||
|
|
||||||
# Remember to never publicly share your keystore.
|
# Remember to never publicly share your keystore.
|
||||||
# See https://flutter.dev/docs/deployment/android#reference-the-keystore-from-the-app
|
# See https://flutter.dev/to/reference-keystore
|
||||||
key.properties
|
key.properties
|
||||||
**/*.keystore
|
**/*.keystore
|
||||||
**/*.jks
|
**/*.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"
|
|
||||||
}
|
|
||||||
41
android/app/build.gradle.kts
Normal file
@ -0,0 +1,41 @@
|
|||||||
|
plugins {
|
||||||
|
id("com.android.application")
|
||||||
|
id("kotlin-android")
|
||||||
|
// The Flutter Gradle Plugin must be applied after the Android and Kotlin Gradle plugins.
|
||||||
|
id("dev.flutter.flutter-gradle-plugin")
|
||||||
|
}
|
||||||
|
|
||||||
|
android {
|
||||||
|
namespace = "com.subtracks2"
|
||||||
|
compileSdk = flutter.compileSdkVersion
|
||||||
|
ndkVersion = flutter.ndkVersion
|
||||||
|
|
||||||
|
compileOptions {
|
||||||
|
sourceCompatibility = JavaVersion.VERSION_11
|
||||||
|
targetCompatibility = JavaVersion.VERSION_11
|
||||||
|
}
|
||||||
|
|
||||||
|
kotlinOptions { jvmTarget = JavaVersion.VERSION_11.toString() }
|
||||||
|
|
||||||
|
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://flutter.dev/to/review-gradle-config.
|
||||||
|
minSdk = flutter.minSdkVersion
|
||||||
|
targetSdk = flutter.targetSdkVersion
|
||||||
|
versionCode = flutter.versionCode
|
||||||
|
versionName = flutter.versionName
|
||||||
|
}
|
||||||
|
|
||||||
|
buildTypes {
|
||||||
|
release {
|
||||||
|
// TODO: Add your own signing config for the release build.
|
||||||
|
// Signing with the debug keys for now, so `flutter run --release` works.
|
||||||
|
signingConfig = signingConfigs.getByName("debug")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
flutter { source = "../.." }
|
||||||
@ -1,5 +1,4 @@
|
|||||||
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
|
<manifest xmlns:android="http://schemas.android.com/apk/res/android">
|
||||||
package="com.subtracks2">
|
|
||||||
<!-- The INTERNET permission is required for development. Specifically,
|
<!-- The INTERNET permission is required for development. Specifically,
|
||||||
the Flutter tool needs it to communicate with the running application
|
the Flutter tool needs it to communicate with the running application
|
||||||
to allow setting breakpoints, to provide hot reload, etc.
|
to allow setting breakpoints, to provide hot reload, etc.
|
||||||
|
|||||||
@ -1,72 +1,45 @@
|
|||||||
<manifest
|
<manifest xmlns:android="http://schemas.android.com/apk/res/android">
|
||||||
xmlns:android="http://schemas.android.com/apk/res/android"
|
|
||||||
xmlns:tools="http://schemas.android.com/tools"
|
|
||||||
package="com.subtracks2">
|
|
||||||
<application
|
<application
|
||||||
android:label="subtracks"
|
android:label="subtracks2"
|
||||||
android:name="${applicationName}"
|
android:name="${applicationName}"
|
||||||
android:icon="@mipmap/ic_launcher"
|
android:icon="@mipmap/ic_launcher">
|
||||||
android:usesCleartextTraffic="true">
|
|
||||||
<activity
|
<activity
|
||||||
android:name="com.ryanheise.audioservice.AudioServiceActivity"
|
android:name=".MainActivity"
|
||||||
android:exported="true"
|
android:exported="true"
|
||||||
android:launchMode="singleTop"
|
android:launchMode="singleTop"
|
||||||
android:theme="@style/LaunchTheme"
|
android:taskAffinity=""
|
||||||
android:configChanges="orientation|keyboardHidden|keyboard|screenSize|smallestScreenSize|locale|layoutDirection|fontScale|screenLayout|density|uiMode"
|
android:theme="@style/LaunchTheme"
|
||||||
android:hardwareAccelerated="true"
|
android:configChanges="orientation|keyboardHidden|keyboard|screenSize|smallestScreenSize|locale|layoutDirection|fontScale|screenLayout|density|uiMode"
|
||||||
android:windowSoftInputMode="adjustResize">
|
android:hardwareAccelerated="true"
|
||||||
|
android:windowSoftInputMode="adjustResize">
|
||||||
<!-- Specifies an Android theme to apply to this Activity as soon as
|
<!-- Specifies an Android theme to apply to this Activity as soon as
|
||||||
the Android process has started. This theme is visible to the user
|
the Android process has started. This theme is visible to the user
|
||||||
while the Flutter UI initializes. After that, this theme continues
|
while the Flutter UI initializes. After that, this theme continues
|
||||||
to determine the Window background behind the Flutter UI. -->
|
to determine the Window background behind the Flutter UI. -->
|
||||||
<meta-data
|
<meta-data
|
||||||
android:name="io.flutter.embedding.android.NormalTheme"
|
android:name="io.flutter.embedding.android.NormalTheme"
|
||||||
android:resource="@style/NormalTheme" />
|
android:resource="@style/NormalTheme"
|
||||||
|
/>
|
||||||
<intent-filter>
|
<intent-filter>
|
||||||
<action android:name="android.intent.action.MAIN" />
|
<action android:name="android.intent.action.MAIN"/>
|
||||||
<category android:name="android.intent.category.LAUNCHER" />
|
<category android:name="android.intent.category.LAUNCHER"/>
|
||||||
</intent-filter>
|
</intent-filter>
|
||||||
</activity>
|
</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.
|
<!-- Don't delete the meta-data below.
|
||||||
This is used by the Flutter tool to generate GeneratedPluginRegistrant.java -->
|
This is used by the Flutter tool to generate GeneratedPluginRegistrant.java -->
|
||||||
<meta-data
|
<meta-data
|
||||||
android:name="flutterEmbedding"
|
android:name="flutterEmbedding"
|
||||||
android:value="2" />
|
android:value="2" />
|
||||||
<!-- <meta-data
|
|
||||||
android:name="io.flutter.embedding.android.EnableImpeller"
|
|
||||||
android:value="true" /> -->
|
|
||||||
</application>
|
</application>
|
||||||
|
<!-- Required to query activities that can process text, see:
|
||||||
|
https://developer.android.com/training/package-visibility and
|
||||||
|
https://developer.android.com/reference/android/content/Intent#ACTION_PROCESS_TEXT.
|
||||||
|
|
||||||
<uses-permission android:name="android.permission.INTERNET" />
|
In particular, this is used by the Flutter engine in io.flutter.plugin.text.ProcessTextPlugin. -->
|
||||||
|
<queries>
|
||||||
<!-- audio_service -->
|
<intent>
|
||||||
<uses-permission android:name="android.permission.WAKE_LOCK" />
|
<action android:name="android.intent.action.PROCESS_TEXT"/>
|
||||||
<uses-permission android:name="android.permission.FOREGROUND_SERVICE" />
|
<data android:mimeType="text/plain"/>
|
||||||
|
</intent>
|
||||||
|
</queries>
|
||||||
</manifest>
|
</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);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@ -2,5 +2,4 @@ package com.subtracks2
|
|||||||
|
|
||||||
import io.flutter.embedding.android.FlutterActivity
|
import io.flutter.embedding.android.FlutterActivity
|
||||||
|
|
||||||
class MainActivity: FlutterActivity() {
|
class MainActivity : FlutterActivity()
|
||||||
}
|
|
||||||
@ -1,30 +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="#FFFFFF">
|
|
||||||
<group android:scaleX="0.92"
|
|
||||||
android:scaleY="0.6735232"
|
|
||||||
android:translateX="11.9936"
|
|
||||||
android:translateY="35.83246">
|
|
||||||
<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,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>
|
|
||||||
|
Before Width: | Height: | Size: 441 B |
|
Before Width: | Height: | Size: 401 B |
|
Before Width: | Height: | Size: 318 B |
|
Before Width: | Height: | Size: 284 B |
|
Before Width: | Height: | Size: 600 B |
|
Before Width: | Height: | Size: 575 B |
|
Before Width: | Height: | Size: 925 B |
|
Before Width: | Height: | Size: 921 B |
@ -1,29 +0,0 @@
|
|||||||
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
|
||||||
android:width="108dp"
|
|
||||||
android:height="108dp"
|
|
||||||
android:viewportWidth="299.84"
|
|
||||||
android:viewportHeight="219.51">
|
|
||||||
<group android:scaleX="0.43"
|
|
||||||
android:scaleY="0.3147989"
|
|
||||||
android:translateX="85.4544"
|
|
||||||
android:translateY="75.20425">
|
|
||||||
<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="#f4d9ff"
|
|
||||||
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="#f4d9ff"
|
|
||||||
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="#f4d9ff"
|
|
||||||
android:fillType="nonZero"
|
|
||||||
android:strokeColor="#00000000"/>
|
|
||||||
</group>
|
|
||||||
</vector>
|
|
||||||
@ -1,5 +0,0 @@
|
|||||||
<?xml version="1.0" encoding="utf-8"?>
|
|
||||||
<adaptive-icon xmlns:android="http://schemas.android.com/apk/res/android">
|
|
||||||
<background android:drawable="@color/ic_launcher_background"/>
|
|
||||||
<foreground android:drawable="@drawable/ic_launcher_foreground"/>
|
|
||||||
</adaptive-icon>
|
|
||||||
@ -1,5 +0,0 @@
|
|||||||
<?xml version="1.0" encoding="utf-8"?>
|
|
||||||
<adaptive-icon xmlns:android="http://schemas.android.com/apk/res/android">
|
|
||||||
<background android:drawable="@color/ic_launcher_background"/>
|
|
||||||
<foreground android:drawable="@drawable/ic_launcher_foreground"/>
|
|
||||||
</adaptive-icon>
|
|
||||||
|
Before Width: | Height: | Size: 1.4 KiB After Width: | Height: | Size: 544 B |
|
Before Width: | Height: | Size: 4.2 KiB |
|
Before Width: | Height: | Size: 3.4 KiB |
|
Before Width: | Height: | Size: 1.1 KiB After Width: | Height: | Size: 442 B |
|
Before Width: | Height: | Size: 2.6 KiB |
|
Before Width: | Height: | Size: 2.1 KiB |
|
Before Width: | Height: | Size: 2.1 KiB After Width: | Height: | Size: 721 B |
|
Before Width: | Height: | Size: 6.2 KiB |
|
Before Width: | Height: | Size: 4.9 KiB |
|
Before Width: | Height: | Size: 3.2 KiB After Width: | Height: | Size: 1.0 KiB |
|
Before Width: | Height: | Size: 11 KiB |
|
Before Width: | Height: | Size: 7.7 KiB |
|
Before Width: | Height: | Size: 4.6 KiB After Width: | Height: | Size: 1.4 KiB |
|
Before Width: | Height: | Size: 16 KiB |
|
Before Width: | Height: | Size: 11 KiB |
@ -1,4 +0,0 @@
|
|||||||
<?xml version="1.0" encoding="utf-8"?>
|
|
||||||
<resources
|
|
||||||
xmlns:tools="http://schemas.android.com/tools"
|
|
||||||
tools:keep="@drawable/*" />
|
|
||||||
@ -1,4 +0,0 @@
|
|||||||
<?xml version="1.0" encoding="utf-8"?>
|
|
||||||
<resources>
|
|
||||||
<color name="ic_launcher_background">#6A1B9A</color>
|
|
||||||
</resources>
|
|
||||||
@ -1,5 +1,4 @@
|
|||||||
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
|
<manifest xmlns:android="http://schemas.android.com/apk/res/android">
|
||||||
package="com.subtracks2">
|
|
||||||
<!-- The INTERNET permission is required for development. Specifically,
|
<!-- The INTERNET permission is required for development. Specifically,
|
||||||
the Flutter tool needs it to communicate with the running application
|
the Flutter tool needs it to communicate with the running application
|
||||||
to allow setting breakpoints, to provide hot reload, etc.
|
to allow setting breakpoints, to provide hot reload, etc.
|
||||||
|
|||||||
@ -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
|
|
||||||
}
|
|
||||||
24
android/build.gradle.kts
Normal file
@ -0,0 +1,24 @@
|
|||||||
|
allprojects {
|
||||||
|
repositories {
|
||||||
|
google()
|
||||||
|
mavenCentral()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
val newBuildDir: Directory =
|
||||||
|
rootProject.layout.buildDirectory
|
||||||
|
.dir("../../build")
|
||||||
|
.get()
|
||||||
|
rootProject.layout.buildDirectory.value(newBuildDir)
|
||||||
|
|
||||||
|
subprojects {
|
||||||
|
val newSubprojectBuildDir: Directory = newBuildDir.dir(project.name)
|
||||||
|
project.layout.buildDirectory.value(newSubprojectBuildDir)
|
||||||
|
}
|
||||||
|
subprojects {
|
||||||
|
project.evaluationDependsOn(":app")
|
||||||
|
}
|
||||||
|
|
||||||
|
tasks.register<Delete>("clean") {
|
||||||
|
delete(rootProject.layout.buildDirectory)
|
||||||
|
}
|
||||||
@ -1,3 +1,3 @@
|
|||||||
org.gradle.jvmargs=-Xmx1536M
|
org.gradle.jvmargs=-Xmx8G -XX:MaxMetaspaceSize=4G -XX:ReservedCodeCacheSize=512m -XX:+HeapDumpOnOutOfMemoryError
|
||||||
android.useAndroidX=true
|
android.useAndroidX=true
|
||||||
android.enableJetifier=true
|
android.enableJetifier=true
|
||||||
|
|||||||
0
android/gradle/wrapper/gradle-wrapper.jar
vendored
Executable file → Normal file
@ -2,4 +2,4 @@ distributionBase=GRADLE_USER_HOME
|
|||||||
distributionPath=wrapper/dists
|
distributionPath=wrapper/dists
|
||||||
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
|
distributionUrl=https\://services.gradle.org/distributions/gradle-8.12-all.zip
|
||||||
|
|||||||
0
android/gradlew.bat
vendored
Executable file → Normal file
@ -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"
|
|
||||||
26
android/settings.gradle.kts
Normal file
@ -0,0 +1,26 @@
|
|||||||
|
pluginManagement {
|
||||||
|
val flutterSdkPath =
|
||||||
|
run {
|
||||||
|
val properties = java.util.Properties()
|
||||||
|
file("local.properties").inputStream().use { properties.load(it) }
|
||||||
|
val flutterSdkPath = properties.getProperty("flutter.sdk")
|
||||||
|
require(flutterSdkPath != null) { "flutter.sdk not set in local.properties" }
|
||||||
|
flutterSdkPath
|
||||||
|
}
|
||||||
|
|
||||||
|
includeBuild("$flutterSdkPath/packages/flutter_tools/gradle")
|
||||||
|
|
||||||
|
repositories {
|
||||||
|
google()
|
||||||
|
mavenCentral()
|
||||||
|
gradlePluginPortal()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
plugins {
|
||||||
|
id("dev.flutter.flutter-plugin-loader") version "1.0.0"
|
||||||
|
id("com.android.application") version "8.9.1" apply false
|
||||||
|
id("org.jetbrains.kotlin.android") version "2.1.0" apply false
|
||||||
|
}
|
||||||
|
|
||||||
|
include(":app")
|
||||||
|
Before Width: | Height: | Size: 207 KiB |
|
Before Width: | Height: | Size: 13 KiB |
@ -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 |
11
build.yaml
@ -1,11 +0,0 @@
|
|||||||
targets:
|
|
||||||
$default:
|
|
||||||
builders:
|
|
||||||
drift_dev:
|
|
||||||
options:
|
|
||||||
sql:
|
|
||||||
dialect: sqlite
|
|
||||||
options:
|
|
||||||
version: "3.38"
|
|
||||||
modules:
|
|
||||||
- fts5
|
|
||||||
@ -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,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@ -1,226 +0,0 @@
|
|||||||
import 'package:audio_service/audio_service.dart';
|
|
||||||
import 'package:auto_route/auto_route.dart';
|
|
||||||
import 'package:flutter/material.dart';
|
|
||||||
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
|
||||||
|
|
||||||
import '../cache/image_cache.dart';
|
|
||||||
import '../models/support.dart';
|
|
||||||
import '../services/audio_service.dart';
|
|
||||||
import '../state/audio.dart';
|
|
||||||
import '../state/theme.dart';
|
|
||||||
import 'app_router.dart';
|
|
||||||
import 'images.dart';
|
|
||||||
import 'pages/now_playing_page.dart';
|
|
||||||
|
|
||||||
class NowPlayingBar extends HookConsumerWidget {
|
|
||||||
const NowPlayingBar({
|
|
||||||
super.key,
|
|
||||||
});
|
|
||||||
|
|
||||||
@override
|
|
||||||
Widget build(BuildContext context, WidgetRef ref) {
|
|
||||||
final colors = ref.watch(mediaItemThemeProvider).valueOrNull;
|
|
||||||
final noItem = ref.watch(mediaItemProvider).valueOrNull == null;
|
|
||||||
|
|
||||||
final widget = GestureDetector(
|
|
||||||
onTap: () {
|
|
||||||
context.navigateTo(const NowPlayingRoute());
|
|
||||||
},
|
|
||||||
child: Material(
|
|
||||||
elevation: 3,
|
|
||||||
color: colors?.darkBackground,
|
|
||||||
// surfaceTintColor: theme?.colorScheme.background,
|
|
||||||
child: Column(
|
|
||||||
children: [
|
|
||||||
SizedBox(
|
|
||||||
height: 70,
|
|
||||||
child: Row(
|
|
||||||
mainAxisSize: MainAxisSize.max,
|
|
||||||
children: const [
|
|
||||||
Padding(
|
|
||||||
padding: EdgeInsets.all(10),
|
|
||||||
child: _ArtImage(),
|
|
||||||
),
|
|
||||||
Expanded(
|
|
||||||
child: Padding(
|
|
||||||
padding: EdgeInsets.only(right: 4),
|
|
||||||
child: _TrackInfo(),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
Padding(
|
|
||||||
padding: EdgeInsets.only(right: 16, top: 2),
|
|
||||||
child: PlayPauseButton(size: 48),
|
|
||||||
),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
),
|
|
||||||
const _ProgressBar(),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
),
|
|
||||||
);
|
|
||||||
|
|
||||||
if (noItem) {
|
|
||||||
return Container();
|
|
||||||
}
|
|
||||||
|
|
||||||
if (colors != null) {
|
|
||||||
return Theme(data: colors.theme, child: widget);
|
|
||||||
} else {
|
|
||||||
return widget;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
class _ArtImage extends HookConsumerWidget {
|
|
||||||
const _ArtImage();
|
|
||||||
|
|
||||||
@override
|
|
||||||
Widget build(BuildContext context, WidgetRef ref) {
|
|
||||||
final imageCache = ref.watch(imageCacheProvider);
|
|
||||||
final uri =
|
|
||||||
ref.watch(mediaItemProvider.select((e) => e.valueOrNull?.artUri));
|
|
||||||
final cacheKey = ref.watch(mediaItemDataProvider.select(
|
|
||||||
(value) => value?.artCache?.thumbnailArtCacheKey,
|
|
||||||
));
|
|
||||||
|
|
||||||
UriCacheInfo? cache;
|
|
||||||
if (uri != null && cacheKey != null) {
|
|
||||||
cache = UriCacheInfo(
|
|
||||||
uri: uri,
|
|
||||||
cacheKey: cacheKey,
|
|
||||||
cacheManager: imageCache,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
return AnimatedSwitcher(
|
|
||||||
duration: const Duration(milliseconds: 150),
|
|
||||||
child: CardClip(
|
|
||||||
key: ValueKey(cacheKey ?? 'default'),
|
|
||||||
child: cache == null
|
|
||||||
? const PlaceholderImage()
|
|
||||||
: UriCacheInfoImage(
|
|
||||||
cache: cache,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
class _TrackInfo extends HookConsumerWidget {
|
|
||||||
const _TrackInfo();
|
|
||||||
|
|
||||||
@override
|
|
||||||
Widget build(BuildContext context, WidgetRef ref) {
|
|
||||||
final item = ref.watch(mediaItemProvider);
|
|
||||||
|
|
||||||
return Column(
|
|
||||||
mainAxisAlignment: MainAxisAlignment.center,
|
|
||||||
crossAxisAlignment: CrossAxisAlignment.start,
|
|
||||||
children: [
|
|
||||||
...item.when(
|
|
||||||
data: (data) => [
|
|
||||||
// Text(
|
|
||||||
// data?.title ?? 'Nothing!!!',
|
|
||||||
// maxLines: 1,
|
|
||||||
// softWrap: false,
|
|
||||||
// overflow: TextOverflow.fade,
|
|
||||||
// style: Theme.of(context).textTheme.labelLarge,
|
|
||||||
// ),
|
|
||||||
ScrollableText(
|
|
||||||
data?.title ?? 'Nothing!!!',
|
|
||||||
style: Theme.of(context).textTheme.labelLarge,
|
|
||||||
),
|
|
||||||
const SizedBox(height: 2),
|
|
||||||
Text(
|
|
||||||
data?.artist ?? 'Nothing!!!',
|
|
||||||
maxLines: 1,
|
|
||||||
softWrap: false,
|
|
||||||
overflow: TextOverflow.fade,
|
|
||||||
style: Theme.of(context).textTheme.labelMedium,
|
|
||||||
),
|
|
||||||
],
|
|
||||||
error: (_, __) => const [Text('Error!')],
|
|
||||||
loading: () => const [Text('loading.....')],
|
|
||||||
),
|
|
||||||
],
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
class PlayPauseButton extends HookConsumerWidget {
|
|
||||||
final double size;
|
|
||||||
|
|
||||||
const PlayPauseButton({
|
|
||||||
super.key,
|
|
||||||
required this.size,
|
|
||||||
});
|
|
||||||
|
|
||||||
@override
|
|
||||||
Widget build(BuildContext context, WidgetRef ref) {
|
|
||||||
final playing = ref.watch(playingProvider);
|
|
||||||
final state = ref.watch(processingStateProvider);
|
|
||||||
|
|
||||||
Widget icon;
|
|
||||||
if (state == AudioProcessingState.loading ||
|
|
||||||
state == AudioProcessingState.buffering) {
|
|
||||||
icon = Stack(
|
|
||||||
alignment: Alignment.center,
|
|
||||||
children: [
|
|
||||||
const Icon(Icons.circle),
|
|
||||||
SizedBox(
|
|
||||||
height: size / 3,
|
|
||||||
width: size / 3,
|
|
||||||
child: CircularProgressIndicator(
|
|
||||||
strokeWidth: size / 16,
|
|
||||||
color: Theme.of(context).colorScheme.background,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
],
|
|
||||||
);
|
|
||||||
} else if (playing) {
|
|
||||||
icon = const Icon(Icons.pause_circle_rounded);
|
|
||||||
} else {
|
|
||||||
icon = const Icon(Icons.play_circle_rounded);
|
|
||||||
}
|
|
||||||
|
|
||||||
return IconButton(
|
|
||||||
iconSize: size,
|
|
||||||
padding: EdgeInsets.zero,
|
|
||||||
onPressed: () {
|
|
||||||
if (playing) {
|
|
||||||
ref.read(audioControlProvider).pause();
|
|
||||||
} else {
|
|
||||||
ref.read(audioControlProvider).play();
|
|
||||||
}
|
|
||||||
},
|
|
||||||
icon: icon,
|
|
||||||
color: Theme.of(context).colorScheme.onBackground,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
class _ProgressBar extends HookConsumerWidget {
|
|
||||||
const _ProgressBar();
|
|
||||||
|
|
||||||
@override
|
|
||||||
Widget build(BuildContext context, WidgetRef ref) {
|
|
||||||
final colors = ref.watch(mediaItemThemeProvider).valueOrNull;
|
|
||||||
final position = ref.watch(positionProvider);
|
|
||||||
final duration = ref.watch(durationProvider);
|
|
||||||
|
|
||||||
return Container(
|
|
||||||
height: 4,
|
|
||||||
color: colors?.darkerBackground,
|
|
||||||
child: Row(
|
|
||||||
children: [
|
|
||||||
Flexible(
|
|
||||||
flex: position,
|
|
||||||
child: Container(color: colors?.onDarkerBackground),
|
|
||||||
),
|
|
||||||
Flexible(flex: duration - position, child: Container()),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@ -1,152 +0,0 @@
|
|||||||
import 'dart:math';
|
|
||||||
|
|
||||||
import 'package:auto_route/auto_route.dart';
|
|
||||||
import 'package:fast_immutable_collections/fast_immutable_collections.dart';
|
|
||||||
import 'package:flutter/material.dart';
|
|
||||||
import 'package:flutter_staggered_grid_view/flutter_staggered_grid_view.dart';
|
|
||||||
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
|
||||||
|
|
||||||
import '../../database/database.dart';
|
|
||||||
import '../../models/query.dart';
|
|
||||||
import '../../models/support.dart';
|
|
||||||
import '../../services/audio_service.dart';
|
|
||||||
import '../../state/music.dart';
|
|
||||||
import '../../state/settings.dart';
|
|
||||||
import '../app_router.dart';
|
|
||||||
import '../buttons.dart';
|
|
||||||
import '../images.dart';
|
|
||||||
import '../items.dart';
|
|
||||||
|
|
||||||
class ArtistPage extends HookConsumerWidget {
|
|
||||||
final String id;
|
|
||||||
|
|
||||||
const ArtistPage({
|
|
||||||
super.key,
|
|
||||||
@pathParam required this.id,
|
|
||||||
});
|
|
||||||
|
|
||||||
@override
|
|
||||||
Widget build(BuildContext context, WidgetRef ref) {
|
|
||||||
ref.listen(sourceIdProvider, (_, __) => context.router.popUntilRoot());
|
|
||||||
|
|
||||||
final artist = ref.watch(artistProvider(id));
|
|
||||||
final albums = ref.watch(albumsByArtistIdProvider(id));
|
|
||||||
|
|
||||||
return Scaffold(
|
|
||||||
floatingActionButton: RadioPlayFab(
|
|
||||||
onPressed: () => artist.hasValue
|
|
||||||
? ref.read(audioControlProvider).playRadio(
|
|
||||||
context: QueueContextType.artist,
|
|
||||||
contextId: artist.valueOrNull!.id,
|
|
||||||
query: ListQuery(
|
|
||||||
filters: IList([
|
|
||||||
FilterWith.equals(
|
|
||||||
column: 'artist_id',
|
|
||||||
value: artist.valueOrNull!.id,
|
|
||||||
)
|
|
||||||
]),
|
|
||||||
),
|
|
||||||
getSongs: (query) => ref
|
|
||||||
.read(databaseProvider)
|
|
||||||
.songsList(ref.read(sourceIdProvider), query)
|
|
||||||
.get(),
|
|
||||||
)
|
|
||||||
: null,
|
|
||||||
),
|
|
||||||
body: CustomScrollView(
|
|
||||||
slivers: [
|
|
||||||
SliverToBoxAdapter(
|
|
||||||
child: Stack(
|
|
||||||
alignment: Alignment.bottomCenter,
|
|
||||||
fit: StackFit.passthrough,
|
|
||||||
children: [
|
|
||||||
ArtistArtImage(
|
|
||||||
artistId: id,
|
|
||||||
thumbnail: false,
|
|
||||||
height: 400,
|
|
||||||
),
|
|
||||||
Padding(
|
|
||||||
padding: const EdgeInsets.all(12),
|
|
||||||
child: _Title(text: artist.valueOrNull?.name ?? ''),
|
|
||||||
),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
),
|
|
||||||
albums.when(
|
|
||||||
data: (albums) {
|
|
||||||
albums = albums.sort((a, b) => (b.year ?? 0) - (a.year ?? 0));
|
|
||||||
return SliverPadding(
|
|
||||||
padding:
|
|
||||||
const EdgeInsets.symmetric(horizontal: 24, vertical: 24),
|
|
||||||
sliver: SliverAlignedGrid.count(
|
|
||||||
crossAxisCount: 2,
|
|
||||||
mainAxisSpacing: 12,
|
|
||||||
crossAxisSpacing: 24,
|
|
||||||
itemCount: albums.length,
|
|
||||||
itemBuilder: (context, i) {
|
|
||||||
final album = albums.elementAt(i);
|
|
||||||
return AlbumCard(
|
|
||||||
album: album,
|
|
||||||
subtitle: AlbumSubtitle.year,
|
|
||||||
onTap: () => context.navigateTo(AlbumSongsRoute(
|
|
||||||
id: album.id,
|
|
||||||
)),
|
|
||||||
);
|
|
||||||
},
|
|
||||||
),
|
|
||||||
);
|
|
||||||
},
|
|
||||||
error: (_, __) => SliverToBoxAdapter(
|
|
||||||
child: Container(color: Colors.red),
|
|
||||||
),
|
|
||||||
loading: () => const SliverToBoxAdapter(
|
|
||||||
child: CircularProgressIndicator(),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
class _Title extends StatelessWidget {
|
|
||||||
final String text;
|
|
||||||
|
|
||||||
const _Title({
|
|
||||||
required this.text,
|
|
||||||
});
|
|
||||||
|
|
||||||
@override
|
|
||||||
Widget build(BuildContext context) {
|
|
||||||
return Text(
|
|
||||||
text,
|
|
||||||
textAlign: TextAlign.center,
|
|
||||||
style: Theme.of(context).textTheme.displayMedium!.copyWith(
|
|
||||||
fontWeight: FontWeight.bold,
|
|
||||||
color: Colors.white,
|
|
||||||
shadows: [
|
|
||||||
Shadow(
|
|
||||||
offset: Offset.fromDirection(pi / 4, 3),
|
|
||||||
blurRadius: 16,
|
|
||||||
color: Colors.black26,
|
|
||||||
),
|
|
||||||
Shadow(
|
|
||||||
offset: Offset.fromDirection(3 * pi / 4, 3),
|
|
||||||
blurRadius: 16,
|
|
||||||
color: Colors.black26,
|
|
||||||
),
|
|
||||||
Shadow(
|
|
||||||
offset: Offset.fromDirection(5 * pi / 4, 3),
|
|
||||||
blurRadius: 16,
|
|
||||||
color: Colors.black26,
|
|
||||||
),
|
|
||||||
Shadow(
|
|
||||||
offset: Offset.fromDirection(7 * pi / 4, 3),
|
|
||||||
blurRadius: 16,
|
|
||||||
color: Colors.black26,
|
|
||||||
),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@ -1,216 +0,0 @@
|
|||||||
import 'package:auto_route/auto_route.dart';
|
|
||||||
import 'package:flutter/material.dart';
|
|
||||||
import 'package:flutter/services.dart';
|
|
||||||
import 'package:flutter_hooks/flutter_hooks.dart';
|
|
||||||
import 'package:flutter_svg/flutter_svg.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/settings.dart';
|
|
||||||
import '../app_router.dart';
|
|
||||||
import '../now_playing_bar.dart';
|
|
||||||
|
|
||||||
part 'bottom_nav_page.g.dart';
|
|
||||||
|
|
||||||
@Riverpod(keepAlive: true)
|
|
||||||
TabObserver bottomTabObserver(BottomTabObserverRef ref) {
|
|
||||||
return TabObserver();
|
|
||||||
}
|
|
||||||
|
|
||||||
@Riverpod(keepAlive: true)
|
|
||||||
Stream<String> bottomTabPath(BottomTabPathRef ref) async* {
|
|
||||||
final observer = ref.watch(bottomTabObserverProvider);
|
|
||||||
await for (var tab in observer.path) {
|
|
||||||
yield tab;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@Riverpod(keepAlive: true)
|
|
||||||
class LastBottomNavStateService extends _$LastBottomNavStateService {
|
|
||||||
@override
|
|
||||||
Future<void> build() async {
|
|
||||||
final db = ref.watch(databaseProvider);
|
|
||||||
final tab = ref.watch(bottomTabPathProvider).valueOrNull;
|
|
||||||
if (tab == null || tab == 'settings' || tab == 'search') {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
await db.saveLastBottomNavState(LastBottomNavStateData(id: 1, tab: tab));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
class BottomNavTabsPage extends HookConsumerWidget {
|
|
||||||
const BottomNavTabsPage({super.key});
|
|
||||||
|
|
||||||
@override
|
|
||||||
Widget build(BuildContext context, WidgetRef ref) {
|
|
||||||
final observer = ref.watch(bottomTabObserverProvider);
|
|
||||||
const navElevation = 3.0;
|
|
||||||
|
|
||||||
return AutoTabsRouter(
|
|
||||||
lazyLoad: false,
|
|
||||||
inheritNavigatorObservers: false,
|
|
||||||
navigatorObservers: () => [observer],
|
|
||||||
routes: const [
|
|
||||||
LibraryRouter(),
|
|
||||||
BrowseRouter(),
|
|
||||||
SearchRouter(),
|
|
||||||
SettingsRouter(),
|
|
||||||
],
|
|
||||||
builder: (context, child, animation) {
|
|
||||||
return AnnotatedRegion<SystemUiOverlayStyle>(
|
|
||||||
value: SystemUiOverlayStyle.light.copyWith(
|
|
||||||
systemNavigationBarColor: ElevationOverlay.applySurfaceTint(
|
|
||||||
Theme.of(context).colorScheme.surface,
|
|
||||||
Theme.of(context).colorScheme.surfaceTint,
|
|
||||||
navElevation,
|
|
||||||
),
|
|
||||||
statusBarColor: Colors.transparent,
|
|
||||||
),
|
|
||||||
child: Scaffold(
|
|
||||||
body: Stack(
|
|
||||||
alignment: AlignmentDirectional.bottomStart,
|
|
||||||
children: [
|
|
||||||
FadeTransition(
|
|
||||||
opacity: animation,
|
|
||||||
child: child,
|
|
||||||
),
|
|
||||||
const OfflineIndicator(),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
bottomNavigationBar: const _BottomNavBar(
|
|
||||||
navElevation: navElevation,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
);
|
|
||||||
},
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
class OfflineIndicator extends HookConsumerWidget {
|
|
||||||
const OfflineIndicator({
|
|
||||||
super.key,
|
|
||||||
});
|
|
||||||
|
|
||||||
@override
|
|
||||||
Widget build(BuildContext context, WidgetRef ref) {
|
|
||||||
final offline = ref.watch(offlineModeProvider);
|
|
||||||
final testing = useState(false);
|
|
||||||
|
|
||||||
if (!offline) {
|
|
||||||
return Container();
|
|
||||||
}
|
|
||||||
|
|
||||||
return Padding(
|
|
||||||
padding: const EdgeInsetsDirectional.only(
|
|
||||||
start: 20,
|
|
||||||
bottom: 20,
|
|
||||||
),
|
|
||||||
child: FilledButton.tonal(
|
|
||||||
style: const ButtonStyle(
|
|
||||||
padding: MaterialStatePropertyAll<EdgeInsetsGeometry>(
|
|
||||||
EdgeInsets.zero,
|
|
||||||
),
|
|
||||||
fixedSize: MaterialStatePropertyAll<Size>(
|
|
||||||
Size(42, 42),
|
|
||||||
),
|
|
||||||
minimumSize: MaterialStatePropertyAll<Size>(
|
|
||||||
Size(42, 42),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
onPressed: () async {
|
|
||||||
testing.value = true;
|
|
||||||
await ref.read(offlineModeProvider.notifier).setMode(false);
|
|
||||||
testing.value = false;
|
|
||||||
},
|
|
||||||
child: testing.value
|
|
||||||
? const SizedBox(
|
|
||||||
height: 16,
|
|
||||||
width: 16,
|
|
||||||
child: CircularProgressIndicator(strokeWidth: 2.5),
|
|
||||||
)
|
|
||||||
: const Padding(
|
|
||||||
padding: EdgeInsets.only(left: 2, bottom: 2),
|
|
||||||
child: Icon(
|
|
||||||
Icons.cloud_off_rounded,
|
|
||||||
// color: Theme.of(context).colorScheme.secondary,
|
|
||||||
size: 20,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
class _BottomNavBar extends HookConsumerWidget {
|
|
||||||
final double navElevation;
|
|
||||||
|
|
||||||
const _BottomNavBar({
|
|
||||||
required this.navElevation,
|
|
||||||
});
|
|
||||||
|
|
||||||
@override
|
|
||||||
Widget build(BuildContext context, WidgetRef ref) {
|
|
||||||
final tabsRouter = AutoTabsRouter.of(context);
|
|
||||||
|
|
||||||
useListenableSelector(tabsRouter, () => tabsRouter.activeIndex);
|
|
||||||
|
|
||||||
return Column(
|
|
||||||
mainAxisSize: MainAxisSize.min,
|
|
||||||
mainAxisAlignment: MainAxisAlignment.end,
|
|
||||||
children: [
|
|
||||||
const NowPlayingBar(),
|
|
||||||
NavigationBar(
|
|
||||||
elevation: navElevation,
|
|
||||||
height: 50,
|
|
||||||
labelBehavior: NavigationDestinationLabelBehavior.alwaysHide,
|
|
||||||
selectedIndex: tabsRouter.activeIndex,
|
|
||||||
onDestinationSelected: (index) {
|
|
||||||
// TODO: replace this with a proper first-time setup flow
|
|
||||||
final hasActiveSource = ref.read(settingsServiceProvider.select(
|
|
||||||
(value) => value.activeSource != null,
|
|
||||||
));
|
|
||||||
|
|
||||||
if (!hasActiveSource) {
|
|
||||||
tabsRouter.setActiveIndex(3);
|
|
||||||
} else {
|
|
||||||
tabsRouter.setActiveIndex(index);
|
|
||||||
}
|
|
||||||
},
|
|
||||||
destinations: [
|
|
||||||
const NavigationDestination(
|
|
||||||
icon: Icon(Icons.music_note),
|
|
||||||
label: 'Library',
|
|
||||||
),
|
|
||||||
NavigationDestination(
|
|
||||||
icon: Builder(builder: (context) {
|
|
||||||
return SvgPicture.asset(
|
|
||||||
'assets/tag_FILL0_wght400_GRAD0_opsz24.svg',
|
|
||||||
colorFilter: ColorFilter.mode(
|
|
||||||
IconTheme.of(context).color!.withOpacity(
|
|
||||||
IconTheme.of(context).opacity ?? 1,
|
|
||||||
),
|
|
||||||
BlendMode.srcIn,
|
|
||||||
),
|
|
||||||
height: 28,
|
|
||||||
);
|
|
||||||
}),
|
|
||||||
label: 'Browse',
|
|
||||||
),
|
|
||||||
const NavigationDestination(
|
|
||||||
icon: Icon(Icons.search_rounded),
|
|
||||||
label: 'Search',
|
|
||||||
),
|
|
||||||
const NavigationDestination(
|
|
||||||
icon: Icon(Icons.settings_rounded),
|
|
||||||
label: 'Settings',
|
|
||||||
),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
],
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@ -1,56 +0,0 @@
|
|||||||
// GENERATED CODE - DO NOT MODIFY BY HAND
|
|
||||||
|
|
||||||
part of 'bottom_nav_page.dart';
|
|
||||||
|
|
||||||
// **************************************************************************
|
|
||||||
// RiverpodGenerator
|
|
||||||
// **************************************************************************
|
|
||||||
|
|
||||||
String _$bottomTabObserverHash() => r'e10c0b870f9b9052ad85fea4342569932edfeefb';
|
|
||||||
|
|
||||||
/// See also [bottomTabObserver].
|
|
||||||
@ProviderFor(bottomTabObserver)
|
|
||||||
final bottomTabObserverProvider = Provider<TabObserver>.internal(
|
|
||||||
bottomTabObserver,
|
|
||||||
name: r'bottomTabObserverProvider',
|
|
||||||
debugGetCreateSourceHash: const bool.fromEnvironment('dart.vm.product')
|
|
||||||
? null
|
|
||||||
: _$bottomTabObserverHash,
|
|
||||||
dependencies: null,
|
|
||||||
allTransitiveDependencies: null,
|
|
||||||
);
|
|
||||||
|
|
||||||
typedef BottomTabObserverRef = ProviderRef<TabObserver>;
|
|
||||||
String _$bottomTabPathHash() => r'62539f7bf5b8f7e5f0531f564e634228ba1506bf';
|
|
||||||
|
|
||||||
/// See also [bottomTabPath].
|
|
||||||
@ProviderFor(bottomTabPath)
|
|
||||||
final bottomTabPathProvider = StreamProvider<String>.internal(
|
|
||||||
bottomTabPath,
|
|
||||||
name: r'bottomTabPathProvider',
|
|
||||||
debugGetCreateSourceHash: const bool.fromEnvironment('dart.vm.product')
|
|
||||||
? null
|
|
||||||
: _$bottomTabPathHash,
|
|
||||||
dependencies: null,
|
|
||||||
allTransitiveDependencies: null,
|
|
||||||
);
|
|
||||||
|
|
||||||
typedef BottomTabPathRef = StreamProviderRef<String>;
|
|
||||||
String _$lastBottomNavStateServiceHash() =>
|
|
||||||
r'487cb94cbb70884642c05a72524eb6fd7a4d12ce';
|
|
||||||
|
|
||||||
/// See also [LastBottomNavStateService].
|
|
||||||
@ProviderFor(LastBottomNavStateService)
|
|
||||||
final lastBottomNavStateServiceProvider =
|
|
||||||
AsyncNotifierProvider<LastBottomNavStateService, void>.internal(
|
|
||||||
LastBottomNavStateService.new,
|
|
||||||
name: r'lastBottomNavStateServiceProvider',
|
|
||||||
debugGetCreateSourceHash: const bool.fromEnvironment('dart.vm.product')
|
|
||||||
? null
|
|
||||||
: _$lastBottomNavStateServiceHash,
|
|
||||||
dependencies: null,
|
|
||||||
allTransitiveDependencies: null,
|
|
||||||
);
|
|
||||||
|
|
||||||
typedef _$LastBottomNavStateService = AsyncNotifier<void>;
|
|
||||||
// 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,281 +0,0 @@
|
|||||||
import 'package:auto_route/auto_route.dart';
|
|
||||||
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 'package:sliver_tools/sliver_tools.dart';
|
|
||||||
|
|
||||||
import '../../database/database.dart';
|
|
||||||
import '../../models/music.dart';
|
|
||||||
import '../../models/query.dart';
|
|
||||||
import '../../models/support.dart';
|
|
||||||
import '../../services/audio_service.dart';
|
|
||||||
import '../../services/cache_service.dart';
|
|
||||||
import '../../state/music.dart';
|
|
||||||
import '../../state/settings.dart';
|
|
||||||
import '../app_router.dart';
|
|
||||||
import '../buttons.dart';
|
|
||||||
import '../images.dart';
|
|
||||||
import '../items.dart';
|
|
||||||
|
|
||||||
part 'browse_page.g.dart';
|
|
||||||
|
|
||||||
@riverpod
|
|
||||||
Stream<List<Album>> albumsCategoryList(
|
|
||||||
AlbumsCategoryListRef ref,
|
|
||||||
ListQuery opt,
|
|
||||||
) {
|
|
||||||
final db = ref.watch(databaseProvider);
|
|
||||||
final sourceId = ref.watch(sourceIdProvider);
|
|
||||||
|
|
||||||
return db.albumsList(sourceId, opt).watch();
|
|
||||||
}
|
|
||||||
|
|
||||||
class BrowsePage extends HookConsumerWidget {
|
|
||||||
const BrowsePage({super.key});
|
|
||||||
|
|
||||||
@override
|
|
||||||
Widget build(BuildContext context, WidgetRef ref) {
|
|
||||||
final l = AppLocalizations.of(context);
|
|
||||||
|
|
||||||
final frequent = ref
|
|
||||||
.watch(albumsCategoryListProvider(const ListQuery(
|
|
||||||
page: Pagination(limit: 20),
|
|
||||||
sort: SortBy(column: 'frequent_rank'),
|
|
||||||
filters: IListConst([
|
|
||||||
FilterWith.isNull(column: 'frequent_rank', invert: true),
|
|
||||||
]),
|
|
||||||
)))
|
|
||||||
.valueOrNull;
|
|
||||||
final recent = ref
|
|
||||||
.watch(albumsCategoryListProvider(const ListQuery(
|
|
||||||
page: Pagination(limit: 20),
|
|
||||||
sort: SortBy(column: 'recent_rank'),
|
|
||||||
filters: IListConst([
|
|
||||||
FilterWith.isNull(column: 'recent_rank', invert: true),
|
|
||||||
]),
|
|
||||||
)))
|
|
||||||
.valueOrNull;
|
|
||||||
final starred = ref
|
|
||||||
.watch(albumsCategoryListProvider(const ListQuery(
|
|
||||||
page: Pagination(limit: 20),
|
|
||||||
sort: SortBy(column: 'starred'),
|
|
||||||
filters: IListConst([
|
|
||||||
FilterWith.isNull(column: 'starred', invert: true),
|
|
||||||
]),
|
|
||||||
)))
|
|
||||||
.valueOrNull;
|
|
||||||
final random = ref
|
|
||||||
.watch(albumsCategoryListProvider(const ListQuery(
|
|
||||||
page: Pagination(limit: 20),
|
|
||||||
sort: SortBy(column: 'RANDOM()'),
|
|
||||||
)))
|
|
||||||
.valueOrNull;
|
|
||||||
|
|
||||||
final genres = ref
|
|
||||||
.watch(albumGenresProvider(const Pagination(
|
|
||||||
limit: 20,
|
|
||||||
)))
|
|
||||||
.valueOrNull;
|
|
||||||
|
|
||||||
return Scaffold(
|
|
||||||
floatingActionButton: RadioPlayFab(
|
|
||||||
onPressed: () {
|
|
||||||
ref.read(audioControlProvider).playRadio(
|
|
||||||
context: QueueContextType.library,
|
|
||||||
getSongs: (query) => ref
|
|
||||||
.read(databaseProvider)
|
|
||||||
.songsList(ref.read(sourceIdProvider), query)
|
|
||||||
.get(),
|
|
||||||
);
|
|
||||||
},
|
|
||||||
),
|
|
||||||
body: CustomScrollView(
|
|
||||||
slivers: [
|
|
||||||
const SliverSafeArea(
|
|
||||||
sliver: SliverPadding(padding: EdgeInsets.only(top: 8)),
|
|
||||||
),
|
|
||||||
_GenreCategory(
|
|
||||||
title: 'Genres',
|
|
||||||
items: genres?.toList() ?? [],
|
|
||||||
),
|
|
||||||
_AlbumCategory(
|
|
||||||
title: l.resourcesSortByFrequentlyPlayed,
|
|
||||||
items: frequent ?? [],
|
|
||||||
),
|
|
||||||
_AlbumCategory(
|
|
||||||
title: l.resourcesSortByRecentlyPlayed,
|
|
||||||
items: recent ?? [],
|
|
||||||
),
|
|
||||||
_AlbumCategory(
|
|
||||||
title: l.resourcesFilterStarred,
|
|
||||||
items: starred ?? [],
|
|
||||||
),
|
|
||||||
_AlbumCategory(
|
|
||||||
title: l.resourcesSortByRandom,
|
|
||||||
items: random ?? [],
|
|
||||||
),
|
|
||||||
const SliverToBoxAdapter(child: FabPadding()),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
class _GenreCategory extends HookConsumerWidget {
|
|
||||||
final String title;
|
|
||||||
final List<String> items;
|
|
||||||
|
|
||||||
const _GenreCategory({
|
|
||||||
required this.title,
|
|
||||||
required this.items,
|
|
||||||
});
|
|
||||||
|
|
||||||
@override
|
|
||||||
Widget build(BuildContext context, WidgetRef ref) {
|
|
||||||
return SliverPadding(
|
|
||||||
padding: const EdgeInsets.only(bottom: 16),
|
|
||||||
sliver: _Category(
|
|
||||||
title: title,
|
|
||||||
height: 140,
|
|
||||||
itemWidth: 140,
|
|
||||||
items: items.map((genre) => _GenreItem(genre: genre)).toList(),
|
|
||||||
),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
class _GenreItem extends HookConsumerWidget {
|
|
||||||
final String genre;
|
|
||||||
|
|
||||||
const _GenreItem({
|
|
||||||
required this.genre,
|
|
||||||
});
|
|
||||||
|
|
||||||
@override
|
|
||||||
Widget build(BuildContext context, WidgetRef ref) {
|
|
||||||
final albums = ref
|
|
||||||
.watch(albumsByGenreProvider(
|
|
||||||
genre,
|
|
||||||
const Pagination(limit: 4),
|
|
||||||
))
|
|
||||||
.valueOrNull;
|
|
||||||
final cache = ref.watch(cacheServiceProvider);
|
|
||||||
|
|
||||||
final theme = Theme.of(context);
|
|
||||||
|
|
||||||
if (albums == null) {
|
|
||||||
return Container();
|
|
||||||
}
|
|
||||||
|
|
||||||
return ImageCard(
|
|
||||||
onTap: () {
|
|
||||||
context.navigateTo(GenreSongsRoute(genre: genre));
|
|
||||||
},
|
|
||||||
child: Stack(
|
|
||||||
alignment: AlignmentDirectional.center,
|
|
||||||
children: [
|
|
||||||
CardClip(
|
|
||||||
child: MultiImage(
|
|
||||||
cacheInfo: albums.map((album) => cache.albumArt(album)),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
Material(
|
|
||||||
type: MaterialType.canvas,
|
|
||||||
color: theme.colorScheme.secondaryContainer,
|
|
||||||
elevation: 5,
|
|
||||||
child: Padding(
|
|
||||||
padding: const EdgeInsets.symmetric(horizontal: 5, vertical: 1),
|
|
||||||
child: SizedBox(
|
|
||||||
width: double.infinity,
|
|
||||||
child: Text(
|
|
||||||
genre,
|
|
||||||
textAlign: TextAlign.center,
|
|
||||||
style: theme.textTheme.labelLarge,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
class _AlbumCategory extends HookConsumerWidget {
|
|
||||||
final String title;
|
|
||||||
final List<Album> items;
|
|
||||||
|
|
||||||
const _AlbumCategory({
|
|
||||||
required this.title,
|
|
||||||
required this.items,
|
|
||||||
});
|
|
||||||
|
|
||||||
@override
|
|
||||||
Widget build(BuildContext context, WidgetRef ref) {
|
|
||||||
return _Category(
|
|
||||||
title: title,
|
|
||||||
height: 190,
|
|
||||||
itemWidth: 140,
|
|
||||||
items: items
|
|
||||||
.map(
|
|
||||||
(album) => AlbumCard(
|
|
||||||
album: album,
|
|
||||||
onTap: () => context.navigateTo(
|
|
||||||
AlbumSongsRoute(
|
|
||||||
id: album.id,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
)
|
|
||||||
.toList(),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
class _Category extends HookConsumerWidget {
|
|
||||||
final String title;
|
|
||||||
final List<Widget> items;
|
|
||||||
final double height;
|
|
||||||
final double itemWidth;
|
|
||||||
|
|
||||||
const _Category({
|
|
||||||
required this.title,
|
|
||||||
required this.items,
|
|
||||||
required this.height,
|
|
||||||
required this.itemWidth,
|
|
||||||
});
|
|
||||||
|
|
||||||
@override
|
|
||||||
Widget build(BuildContext context, WidgetRef ref) {
|
|
||||||
return MultiSliver(
|
|
||||||
children: [
|
|
||||||
SliverToBoxAdapter(
|
|
||||||
child: Padding(
|
|
||||||
padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 16),
|
|
||||||
child: Text(
|
|
||||||
title,
|
|
||||||
style: Theme.of(context).textTheme.headlineMedium,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
SliverToBoxAdapter(
|
|
||||||
child: SizedBox(
|
|
||||||
height: height,
|
|
||||||
child: ListView.separated(
|
|
||||||
padding: const EdgeInsets.symmetric(horizontal: 16),
|
|
||||||
scrollDirection: Axis.horizontal,
|
|
||||||
itemCount: items.length,
|
|
||||||
itemBuilder: (context, index) => SizedBox(
|
|
||||||
width: itemWidth,
|
|
||||||
child: items[index],
|
|
||||||
),
|
|
||||||
separatorBuilder: (context, index) => const SizedBox(width: 8),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
],
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@ -1,114 +0,0 @@
|
|||||||
// GENERATED CODE - DO NOT MODIFY BY HAND
|
|
||||||
|
|
||||||
part of 'browse_page.dart';
|
|
||||||
|
|
||||||
// **************************************************************************
|
|
||||||
// RiverpodGenerator
|
|
||||||
// **************************************************************************
|
|
||||||
|
|
||||||
String _$albumsCategoryListHash() =>
|
|
||||||
r'e0516a585bf39e8140c72c08fd41f33a817c747d';
|
|
||||||
|
|
||||||
/// 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 AlbumsCategoryListRef = AutoDisposeStreamProviderRef<List<Album>>;
|
|
||||||
|
|
||||||
/// See also [albumsCategoryList].
|
|
||||||
@ProviderFor(albumsCategoryList)
|
|
||||||
const albumsCategoryListProvider = AlbumsCategoryListFamily();
|
|
||||||
|
|
||||||
/// See also [albumsCategoryList].
|
|
||||||
class AlbumsCategoryListFamily extends Family<AsyncValue<List<Album>>> {
|
|
||||||
/// See also [albumsCategoryList].
|
|
||||||
const AlbumsCategoryListFamily();
|
|
||||||
|
|
||||||
/// See also [albumsCategoryList].
|
|
||||||
AlbumsCategoryListProvider call(
|
|
||||||
ListQuery opt,
|
|
||||||
) {
|
|
||||||
return AlbumsCategoryListProvider(
|
|
||||||
opt,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
@override
|
|
||||||
AlbumsCategoryListProvider getProviderOverride(
|
|
||||||
covariant AlbumsCategoryListProvider provider,
|
|
||||||
) {
|
|
||||||
return call(
|
|
||||||
provider.opt,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
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'albumsCategoryListProvider';
|
|
||||||
}
|
|
||||||
|
|
||||||
/// See also [albumsCategoryList].
|
|
||||||
class AlbumsCategoryListProvider
|
|
||||||
extends AutoDisposeStreamProvider<List<Album>> {
|
|
||||||
/// See also [albumsCategoryList].
|
|
||||||
AlbumsCategoryListProvider(
|
|
||||||
this.opt,
|
|
||||||
) : super.internal(
|
|
||||||
(ref) => albumsCategoryList(
|
|
||||||
ref,
|
|
||||||
opt,
|
|
||||||
),
|
|
||||||
from: albumsCategoryListProvider,
|
|
||||||
name: r'albumsCategoryListProvider',
|
|
||||||
debugGetCreateSourceHash:
|
|
||||||
const bool.fromEnvironment('dart.vm.product')
|
|
||||||
? null
|
|
||||||
: _$albumsCategoryListHash,
|
|
||||||
dependencies: AlbumsCategoryListFamily._dependencies,
|
|
||||||
allTransitiveDependencies:
|
|
||||||
AlbumsCategoryListFamily._allTransitiveDependencies,
|
|
||||||
);
|
|
||||||
|
|
||||||
final ListQuery opt;
|
|
||||||
|
|
||||||
@override
|
|
||||||
bool operator ==(Object other) {
|
|
||||||
return other is AlbumsCategoryListProvider && other.opt == opt;
|
|
||||||
}
|
|
||||||
|
|
||||||
@override
|
|
||||||
int get hashCode {
|
|
||||||
var hash = _SystemHash.combine(0, runtimeType.hashCode);
|
|
||||||
hash = _SystemHash.combine(hash, opt.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,41 +0,0 @@
|
|||||||
import 'package:auto_route/auto_route.dart';
|
|
||||||
import 'package:flutter/material.dart';
|
|
||||||
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
|
||||||
|
|
||||||
import '../../database/database.dart';
|
|
||||||
import '../../state/settings.dart';
|
|
||||||
import '../app_router.dart';
|
|
||||||
import '../hooks/use_list_query_paging_controller.dart';
|
|
||||||
import '../items.dart';
|
|
||||||
import '../lists.dart';
|
|
||||||
|
|
||||||
class LibraryAlbumsPage extends HookConsumerWidget {
|
|
||||||
const LibraryAlbumsPage({super.key});
|
|
||||||
|
|
||||||
@override
|
|
||||||
Widget build(BuildContext context, WidgetRef ref) {
|
|
||||||
final pagingController = useLibraryPagingController(
|
|
||||||
ref,
|
|
||||||
libraryTabIndex: 0,
|
|
||||||
getItems: (query) {
|
|
||||||
final db = ref.read(databaseProvider);
|
|
||||||
final sourceId = ref.read(sourceIdProvider);
|
|
||||||
|
|
||||||
return ref.read(offlineModeProvider)
|
|
||||||
? db.albumsListDownloaded(sourceId, query).get()
|
|
||||||
: db.albumsList(sourceId, query).get();
|
|
||||||
},
|
|
||||||
);
|
|
||||||
|
|
||||||
return PagedGridQueryView(
|
|
||||||
pagingController: pagingController,
|
|
||||||
refreshSyncAll: true,
|
|
||||||
itemBuilder: (context, item, index, size) => AlbumCard(
|
|
||||||
album: item,
|
|
||||||
style:
|
|
||||||
size == GridSize.small ? CardStyle.imageOnly : CardStyle.withText,
|
|
||||||
onTap: () => context.navigateTo(AlbumSongsRoute(id: item.id)),
|
|
||||||
),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@ -1,39 +0,0 @@
|
|||||||
import 'package:auto_route/auto_route.dart';
|
|
||||||
import 'package:flutter/material.dart';
|
|
||||||
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
|
||||||
|
|
||||||
import '../../database/database.dart';
|
|
||||||
import '../../state/settings.dart';
|
|
||||||
import '../app_router.dart';
|
|
||||||
import '../hooks/use_list_query_paging_controller.dart';
|
|
||||||
import '../items.dart';
|
|
||||||
import '../lists.dart';
|
|
||||||
|
|
||||||
class LibraryArtistsPage extends HookConsumerWidget {
|
|
||||||
const LibraryArtistsPage({super.key});
|
|
||||||
|
|
||||||
@override
|
|
||||||
Widget build(BuildContext context, WidgetRef ref) {
|
|
||||||
final pagingController = useLibraryPagingController(
|
|
||||||
ref,
|
|
||||||
libraryTabIndex: 1,
|
|
||||||
getItems: (query) {
|
|
||||||
final db = ref.read(databaseProvider);
|
|
||||||
final sourceId = ref.read(sourceIdProvider);
|
|
||||||
|
|
||||||
return ref.read(offlineModeProvider)
|
|
||||||
? db.artistsListDownloaded(sourceId, query).get()
|
|
||||||
: db.artistsList(sourceId, query).get();
|
|
||||||
},
|
|
||||||
);
|
|
||||||
|
|
||||||
return PagedListQueryView(
|
|
||||||
pagingController: pagingController,
|
|
||||||
refreshSyncAll: true,
|
|
||||||
itemBuilder: (context, item, index) => ArtistListTile(
|
|
||||||
artist: item,
|
|
||||||
onTap: () => context.navigateTo(ArtistRoute(id: item.id)),
|
|
||||||
),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@ -1,635 +0,0 @@
|
|||||||
import 'package:auto_route/auto_route.dart';
|
|
||||||
import 'package:collection/collection.dart';
|
|
||||||
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 'package:riverpod_annotation/riverpod_annotation.dart';
|
|
||||||
|
|
||||||
import '../../database/database.dart';
|
|
||||||
import '../../models/query.dart';
|
|
||||||
import '../app_router.dart';
|
|
||||||
import '../context_menus.dart';
|
|
||||||
|
|
||||||
part 'library_page.g.dart';
|
|
||||||
|
|
||||||
@Riverpod(keepAlive: true)
|
|
||||||
TabObserver libraryTabObserver(LibraryTabObserverRef ref) {
|
|
||||||
return TabObserver();
|
|
||||||
}
|
|
||||||
|
|
||||||
@Riverpod(keepAlive: true)
|
|
||||||
Stream<String> libraryTabPath(LibraryTabPathRef ref) async* {
|
|
||||||
final observer = ref.watch(libraryTabObserverProvider);
|
|
||||||
await for (var tab in observer.path) {
|
|
||||||
yield tab;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@Riverpod(keepAlive: true)
|
|
||||||
class LastLibraryStateService extends _$LastLibraryStateService {
|
|
||||||
@override
|
|
||||||
Future<void> build() async {
|
|
||||||
final db = ref.watch(databaseProvider);
|
|
||||||
final tab = await ref.watch(libraryTabPathProvider.future);
|
|
||||||
|
|
||||||
await db.saveLastLibraryState(LastLibraryStateData(
|
|
||||||
id: 1,
|
|
||||||
tab: tab,
|
|
||||||
albumsList: ref.watch(libraryListQueryProvider(0)).query,
|
|
||||||
artistsList: ref.watch(libraryListQueryProvider(1)).query,
|
|
||||||
playlistsList: ref.watch(libraryListQueryProvider(2)).query,
|
|
||||||
songsList: ref.watch(libraryListQueryProvider(3)).query,
|
|
||||||
));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@Riverpod(keepAlive: true)
|
|
||||||
class LibraryLists extends _$LibraryLists {
|
|
||||||
@override
|
|
||||||
IList<LibraryListQuery> build() {
|
|
||||||
return const IListConst([
|
|
||||||
/// Albums
|
|
||||||
LibraryListQuery(
|
|
||||||
options: ListQueryOptions(
|
|
||||||
sortColumns: IListConst([
|
|
||||||
'albums.name',
|
|
||||||
'albums.created',
|
|
||||||
'albums.album_artist',
|
|
||||||
'albums.year',
|
|
||||||
]),
|
|
||||||
filterColumns: IListConst([
|
|
||||||
'albums.starred',
|
|
||||||
'albums.album_artist',
|
|
||||||
'albums.year',
|
|
||||||
'albums.genre',
|
|
||||||
]),
|
|
||||||
),
|
|
||||||
query: ListQuery(
|
|
||||||
page: Pagination(limit: 60),
|
|
||||||
sort: SortBy(column: 'albums.name'),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
|
|
||||||
/// Artists
|
|
||||||
LibraryListQuery(
|
|
||||||
options: ListQueryOptions(
|
|
||||||
sortColumns: IListConst([
|
|
||||||
'artists.name',
|
|
||||||
'artists.album_count',
|
|
||||||
]),
|
|
||||||
filterColumns: IListConst([
|
|
||||||
'artists.starred',
|
|
||||||
]),
|
|
||||||
),
|
|
||||||
query: ListQuery(
|
|
||||||
page: Pagination(limit: 30),
|
|
||||||
sort: SortBy(column: 'artists.name'),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
|
|
||||||
/// Playlists
|
|
||||||
LibraryListQuery(
|
|
||||||
options: ListQueryOptions(
|
|
||||||
sortColumns: IListConst([
|
|
||||||
'playlists.name',
|
|
||||||
'playlists.created',
|
|
||||||
'playlists.changed',
|
|
||||||
]),
|
|
||||||
filterColumns: IListConst([
|
|
||||||
'playlists.owner',
|
|
||||||
]),
|
|
||||||
),
|
|
||||||
query: ListQuery(
|
|
||||||
page: Pagination(limit: 30),
|
|
||||||
sort: SortBy(column: 'playlists.name'),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
|
|
||||||
/// Songs
|
|
||||||
LibraryListQuery(
|
|
||||||
options: ListQueryOptions(
|
|
||||||
sortColumns: IListConst([
|
|
||||||
'songs.album',
|
|
||||||
'songs.artist',
|
|
||||||
'songs.created',
|
|
||||||
'songs.title',
|
|
||||||
'songs.year',
|
|
||||||
]),
|
|
||||||
filterColumns: IListConst([
|
|
||||||
'songs.starred',
|
|
||||||
'songs.artist',
|
|
||||||
'songs.album',
|
|
||||||
'songs.year',
|
|
||||||
'songs.genre',
|
|
||||||
]),
|
|
||||||
),
|
|
||||||
query: ListQuery(
|
|
||||||
page: Pagination(limit: 30),
|
|
||||||
sort: SortBy(column: 'songs.album'),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
]);
|
|
||||||
}
|
|
||||||
|
|
||||||
Future<void> init() async {
|
|
||||||
final db = ref.read(databaseProvider);
|
|
||||||
final last = await db.getLastLibraryState().getSingleOrNull();
|
|
||||||
if (last == null) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
state = state
|
|
||||||
.replace(0, state[0].copyWith(query: last.albumsList))
|
|
||||||
.replace(1, state[1].copyWith(query: last.artistsList))
|
|
||||||
.replace(2, state[2].copyWith(query: last.playlistsList))
|
|
||||||
.replace(3, state[3].copyWith(query: last.songsList));
|
|
||||||
}
|
|
||||||
|
|
||||||
void setSortColumn(int index, String column) {
|
|
||||||
state = state.replace(
|
|
||||||
index,
|
|
||||||
state[index].copyWith.query.sort!(column: column),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
void toggleDirection(int index) {
|
|
||||||
final toggled = state[index].query.sort?.dir == SortDirection.asc
|
|
||||||
? SortDirection.desc
|
|
||||||
: SortDirection.asc;
|
|
||||||
state = state.replace(
|
|
||||||
index,
|
|
||||||
state[index].copyWith.query.sort!(dir: toggled),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
void setFilter(int index, FilterWith filter) {
|
|
||||||
state = state.replace(
|
|
||||||
index,
|
|
||||||
state[index].copyWith.query(
|
|
||||||
filters: state[index].query.filters.updateById(
|
|
||||||
[filter],
|
|
||||||
(e) => e.column,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
void removeFilter(int index, String column) {
|
|
||||||
state = state.replace(
|
|
||||||
index,
|
|
||||||
state[index].copyWith.query(
|
|
||||||
filters: state[index]
|
|
||||||
.query
|
|
||||||
.filters
|
|
||||||
.removeWhere((f) => f.column == column)),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
void clearFilters(int index) {
|
|
||||||
state = state.replace(index, state[index].copyWith.query(filters: IList()));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@Riverpod(keepAlive: true)
|
|
||||||
LibraryListQuery libraryListQuery(LibraryListQueryRef ref, int index) {
|
|
||||||
return ref.watch(libraryListsProvider.select((value) => value[index]));
|
|
||||||
}
|
|
||||||
|
|
||||||
class LibraryTabsPage extends HookConsumerWidget {
|
|
||||||
const LibraryTabsPage({super.key});
|
|
||||||
|
|
||||||
@override
|
|
||||||
Widget build(BuildContext context, WidgetRef ref) {
|
|
||||||
final observer = ref.watch(libraryTabObserverProvider);
|
|
||||||
|
|
||||||
return AutoTabsRouter.tabBar(
|
|
||||||
inheritNavigatorObservers: false,
|
|
||||||
navigatorObservers: () => [observer],
|
|
||||||
routes: const [
|
|
||||||
LibraryAlbumsRoute(),
|
|
||||||
LibraryArtistsRoute(),
|
|
||||||
LibraryPlaylistsRoute(),
|
|
||||||
LibrarySongsRoute(),
|
|
||||||
],
|
|
||||||
builder: (context, child, tabController) {
|
|
||||||
return Scaffold(
|
|
||||||
body: child,
|
|
||||||
floatingActionButton: const _LibraryFilterFab(),
|
|
||||||
);
|
|
||||||
},
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
class _LibraryFilterFab extends HookConsumerWidget {
|
|
||||||
const _LibraryFilterFab();
|
|
||||||
|
|
||||||
@override
|
|
||||||
Widget build(BuildContext context, WidgetRef ref) {
|
|
||||||
final tabsRouter = AutoTabsRouter.of(context);
|
|
||||||
final activeIndex =
|
|
||||||
useListenableSelector(tabsRouter, () => tabsRouter.activeIndex);
|
|
||||||
final tabHasFilters = ref.watch(libraryListQueryProvider(activeIndex)
|
|
||||||
.select((value) => value.query.filters.isNotEmpty));
|
|
||||||
|
|
||||||
List<Widget> dot = [];
|
|
||||||
if (tabHasFilters) {
|
|
||||||
dot.addAll([
|
|
||||||
PositionedDirectional(
|
|
||||||
top: 3,
|
|
||||||
end: 0,
|
|
||||||
child: Icon(
|
|
||||||
Icons.circle,
|
|
||||||
color: Theme.of(context).colorScheme.primaryContainer,
|
|
||||||
size: 11,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
const PositionedDirectional(
|
|
||||||
top: 5,
|
|
||||||
end: 1,
|
|
||||||
child: Icon(
|
|
||||||
Icons.circle,
|
|
||||||
size: 7,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
]);
|
|
||||||
}
|
|
||||||
|
|
||||||
return FloatingActionButton(
|
|
||||||
heroTag: null,
|
|
||||||
onPressed: () async {
|
|
||||||
showContextMenu(
|
|
||||||
context: context,
|
|
||||||
ref: ref,
|
|
||||||
builder: (context) => BottomSheetMenu(
|
|
||||||
child: LibraryMenu(
|
|
||||||
tabsRouter: tabsRouter,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
);
|
|
||||||
},
|
|
||||||
tooltip: 'List',
|
|
||||||
child: Stack(
|
|
||||||
children: [
|
|
||||||
const Icon(
|
|
||||||
Icons.sort_rounded,
|
|
||||||
size: 28,
|
|
||||||
),
|
|
||||||
...dot,
|
|
||||||
],
|
|
||||||
),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
class LibraryMenu extends HookConsumerWidget {
|
|
||||||
final TabsRouter tabsRouter;
|
|
||||||
|
|
||||||
const LibraryMenu({
|
|
||||||
super.key,
|
|
||||||
required this.tabsRouter,
|
|
||||||
});
|
|
||||||
|
|
||||||
@override
|
|
||||||
Widget build(BuildContext context, WidgetRef ref) {
|
|
||||||
return CustomScrollView(
|
|
||||||
slivers: [
|
|
||||||
SliverPadding(
|
|
||||||
padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 8),
|
|
||||||
sliver: SliverToBoxAdapter(
|
|
||||||
child: Row(
|
|
||||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
|
||||||
crossAxisAlignment: CrossAxisAlignment.center,
|
|
||||||
children: [
|
|
||||||
Row(
|
|
||||||
children: [
|
|
||||||
FilterChip(
|
|
||||||
shape: RoundedRectangleBorder(
|
|
||||||
borderRadius: BorderRadius.circular(100),
|
|
||||||
),
|
|
||||||
onSelected: (value) {},
|
|
||||||
selected: true,
|
|
||||||
label: const Icon(
|
|
||||||
Icons.grid_on,
|
|
||||||
size: 20,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
const SizedBox(width: 6),
|
|
||||||
FilterChip(
|
|
||||||
shape: RoundedRectangleBorder(
|
|
||||||
borderRadius: BorderRadius.circular(100),
|
|
||||||
),
|
|
||||||
onSelected: (value) {},
|
|
||||||
label: const Icon(
|
|
||||||
Icons.grid_view_outlined,
|
|
||||||
size: 20,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
const SizedBox(width: 6),
|
|
||||||
FilterChip(
|
|
||||||
shape: RoundedRectangleBorder(
|
|
||||||
borderRadius: BorderRadius.circular(100),
|
|
||||||
),
|
|
||||||
onSelected: (value) {},
|
|
||||||
label: const Icon(
|
|
||||||
Icons.format_list_bulleted,
|
|
||||||
size: 20,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
_FilterToggleButton(tabsRouter: tabsRouter),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
const SliverPadding(padding: EdgeInsets.only(top: 8)),
|
|
||||||
ListSortFilterOptions(index: tabsRouter.activeIndex),
|
|
||||||
const SliverPadding(padding: EdgeInsets.only(top: 16)),
|
|
||||||
],
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
class _FilterToggleButton extends HookConsumerWidget {
|
|
||||||
final TabsRouter tabsRouter;
|
|
||||||
|
|
||||||
const _FilterToggleButton({
|
|
||||||
required this.tabsRouter,
|
|
||||||
});
|
|
||||||
|
|
||||||
@override
|
|
||||||
Widget build(BuildContext context, WidgetRef ref) {
|
|
||||||
final tabHasFilters = ref.watch(
|
|
||||||
libraryListQueryProvider(tabsRouter.activeIndex)
|
|
||||||
.select((value) => value.query.filters.isNotEmpty));
|
|
||||||
|
|
||||||
return FilledButton(
|
|
||||||
onPressed: tabHasFilters
|
|
||||||
? () {
|
|
||||||
ref
|
|
||||||
.read(libraryListsProvider.notifier)
|
|
||||||
.clearFilters(tabsRouter.activeIndex);
|
|
||||||
}
|
|
||||||
: null,
|
|
||||||
child: const Icon(Icons.filter_list_off_rounded),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
class ListSortFilterOptions extends HookConsumerWidget {
|
|
||||||
final int index;
|
|
||||||
|
|
||||||
const ListSortFilterOptions({
|
|
||||||
super.key,
|
|
||||||
required this.index,
|
|
||||||
});
|
|
||||||
|
|
||||||
void Function()? _filterOnEdit(
|
|
||||||
String column,
|
|
||||||
BuildContext context,
|
|
||||||
WidgetRef ref,
|
|
||||||
) {
|
|
||||||
final type = column.split('.').last;
|
|
||||||
switch (type) {
|
|
||||||
case 'year':
|
|
||||||
return () {
|
|
||||||
// TODO: year filter dialog
|
|
||||||
// showDialog(
|
|
||||||
// context: context,
|
|
||||||
// builder: (context) {
|
|
||||||
// return Dialog(
|
|
||||||
// child: Text('adsf'),
|
|
||||||
// );
|
|
||||||
// },
|
|
||||||
// );
|
|
||||||
};
|
|
||||||
case 'genre':
|
|
||||||
case 'album_artist':
|
|
||||||
case 'owner':
|
|
||||||
case 'album':
|
|
||||||
case 'artist':
|
|
||||||
// TODO: other filter dialogs
|
|
||||||
return () {};
|
|
||||||
default:
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
void Function(bool? value)? _filterOnChanged(String column, WidgetRef ref) {
|
|
||||||
final type = column.split('.').last;
|
|
||||||
switch (type) {
|
|
||||||
case 'starred':
|
|
||||||
return (value) {
|
|
||||||
if (value!) {
|
|
||||||
ref.read(libraryListsProvider.notifier).setFilter(
|
|
||||||
index,
|
|
||||||
FilterWith.isNull(column: column, invert: true),
|
|
||||||
);
|
|
||||||
} else {
|
|
||||||
ref.read(libraryListsProvider.notifier).removeFilter(index, column);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
case 'year':
|
|
||||||
// TODO: add/remove filter
|
|
||||||
return null;
|
|
||||||
case 'genre':
|
|
||||||
case 'album_artist':
|
|
||||||
case 'owner':
|
|
||||||
case 'album':
|
|
||||||
case 'artist':
|
|
||||||
// TODO: add/remove filter
|
|
||||||
return null;
|
|
||||||
default:
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@override
|
|
||||||
Widget build(BuildContext context, WidgetRef ref) {
|
|
||||||
final list = ref.watch(libraryListQueryProvider(index));
|
|
||||||
|
|
||||||
return SliverList(
|
|
||||||
delegate: SliverChildListDelegate.fixed([
|
|
||||||
Padding(
|
|
||||||
padding: const EdgeInsets.symmetric(horizontal: 16),
|
|
||||||
child: Text(
|
|
||||||
'Sort by',
|
|
||||||
style: Theme.of(context).textTheme.titleLarge,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
const SizedBox(height: 8),
|
|
||||||
for (var column in list.options.sortColumns)
|
|
||||||
SortOptionTile(
|
|
||||||
column: column,
|
|
||||||
value: list.query.sort!.copyWith(column: column),
|
|
||||||
groupValue: list.query.sort!,
|
|
||||||
onColumnChanged: (column) {
|
|
||||||
if (column != null) {
|
|
||||||
ref
|
|
||||||
.read(libraryListsProvider.notifier)
|
|
||||||
.setSortColumn(index, column);
|
|
||||||
}
|
|
||||||
},
|
|
||||||
onDirectionToggle: () =>
|
|
||||||
ref.read(libraryListsProvider.notifier).toggleDirection(index),
|
|
||||||
),
|
|
||||||
Padding(
|
|
||||||
padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 8),
|
|
||||||
child: Text(
|
|
||||||
'Filter',
|
|
||||||
style: Theme.of(context).textTheme.titleLarge,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
for (var column in list.options.filterColumns)
|
|
||||||
FilterOptionTile(
|
|
||||||
column: column,
|
|
||||||
state: list.query.filters.singleWhereOrNull(
|
|
||||||
(e) => e.column == column,
|
|
||||||
),
|
|
||||||
onEdit: _filterOnEdit(column, context, ref),
|
|
||||||
onChanged: _filterOnChanged(column, ref),
|
|
||||||
)
|
|
||||||
]),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
class SortOptionTile extends HookConsumerWidget {
|
|
||||||
final String column;
|
|
||||||
final SortBy value;
|
|
||||||
final SortBy groupValue;
|
|
||||||
final void Function(String? value) onColumnChanged;
|
|
||||||
final void Function() onDirectionToggle;
|
|
||||||
|
|
||||||
const SortOptionTile({
|
|
||||||
super.key,
|
|
||||||
required this.column,
|
|
||||||
required this.value,
|
|
||||||
required this.groupValue,
|
|
||||||
required this.onColumnChanged,
|
|
||||||
required this.onDirectionToggle,
|
|
||||||
});
|
|
||||||
|
|
||||||
String _sortTitle(AppLocalizations l, String type) {
|
|
||||||
type = type.split('.').last;
|
|
||||||
switch (type) {
|
|
||||||
case 'name':
|
|
||||||
return l.resourcesSortByName;
|
|
||||||
case 'album_artist':
|
|
||||||
return l.resourcesSortByArtist;
|
|
||||||
case 'created':
|
|
||||||
return l.resourcesSortByAdded;
|
|
||||||
case 'year':
|
|
||||||
return l.resourcesSortByYear;
|
|
||||||
case 'album_count':
|
|
||||||
return l.resourcesSortByAlbumCount;
|
|
||||||
case 'changed':
|
|
||||||
return l.resourcesSortByUpdated;
|
|
||||||
case 'album':
|
|
||||||
return l.resourcesSortByAlbum;
|
|
||||||
case 'artist':
|
|
||||||
return l.resourcesSortByArtist;
|
|
||||||
case 'title':
|
|
||||||
return l.resourcesSortByTitle;
|
|
||||||
default:
|
|
||||||
return '';
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@override
|
|
||||||
Widget build(BuildContext context, WidgetRef ref) {
|
|
||||||
final l = AppLocalizations.of(context);
|
|
||||||
|
|
||||||
return RadioListTile<String?>(
|
|
||||||
value: value.column,
|
|
||||||
groupValue: groupValue.column,
|
|
||||||
onChanged: onColumnChanged,
|
|
||||||
selected: value.column == groupValue.column,
|
|
||||||
title: Text(_sortTitle(l, column)),
|
|
||||||
secondary: value.column == groupValue.column
|
|
||||||
? IconButton(
|
|
||||||
icon: Icon(
|
|
||||||
value.dir == SortDirection.desc
|
|
||||||
? Icons.arrow_upward_rounded
|
|
||||||
: Icons.arrow_downward_rounded,
|
|
||||||
),
|
|
||||||
onPressed: onDirectionToggle,
|
|
||||||
)
|
|
||||||
: null,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
class FilterOptionTile extends HookConsumerWidget {
|
|
||||||
final String column;
|
|
||||||
final FilterWith? state;
|
|
||||||
final void Function(bool? value)? onChanged;
|
|
||||||
final void Function()? onEdit;
|
|
||||||
|
|
||||||
const FilterOptionTile({
|
|
||||||
super.key,
|
|
||||||
required this.column,
|
|
||||||
required this.state,
|
|
||||||
required this.onChanged,
|
|
||||||
this.onEdit,
|
|
||||||
});
|
|
||||||
|
|
||||||
String _filterTitle(AppLocalizations l, String type) {
|
|
||||||
type = type.split('.').last;
|
|
||||||
switch (type) {
|
|
||||||
case 'starred':
|
|
||||||
return l.resourcesFilterStarred;
|
|
||||||
case 'year':
|
|
||||||
return l.resourcesFilterYear;
|
|
||||||
case 'genre':
|
|
||||||
return l.resourcesFilterGenre;
|
|
||||||
case 'album_artist':
|
|
||||||
return l.resourcesFilterArtist;
|
|
||||||
case 'owner':
|
|
||||||
return l.resourcesFilterOwner;
|
|
||||||
case 'album':
|
|
||||||
return l.resourcesFilterAlbum;
|
|
||||||
case 'artist':
|
|
||||||
return l.resourcesFilterArtist;
|
|
||||||
default:
|
|
||||||
return '';
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@override
|
|
||||||
Widget build(BuildContext context, WidgetRef ref) {
|
|
||||||
final l = AppLocalizations.of(context);
|
|
||||||
|
|
||||||
return CheckboxListTile(
|
|
||||||
value: state == null
|
|
||||||
? false
|
|
||||||
: state!.map(
|
|
||||||
equals: (value) => value.invert ? null : true,
|
|
||||||
greaterThan: (value) => true,
|
|
||||||
isNull: (_) => true,
|
|
||||||
betweenInt: (_) => true,
|
|
||||||
isIn: (value) => value.invert ? null : true,
|
|
||||||
),
|
|
||||||
tristate: state?.map(
|
|
||||||
equals: (value) => true,
|
|
||||||
greaterThan: (value) => false,
|
|
||||||
isNull: (_) => false,
|
|
||||||
betweenInt: (_) => false,
|
|
||||||
isIn: (_) => true,
|
|
||||||
) ??
|
|
||||||
false,
|
|
||||||
title: Text(_filterTitle(l, column)),
|
|
||||||
secondary: onEdit == null
|
|
||||||
? null
|
|
||||||
: IconButton(
|
|
||||||
icon: const Icon(Icons.edit_rounded),
|
|
||||||
onPressed: onEdit,
|
|
||||||
),
|
|
||||||
controlAffinity: ListTileControlAffinity.leading,
|
|
||||||
onChanged: onChanged,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@ -1,176 +0,0 @@
|
|||||||
// GENERATED CODE - DO NOT MODIFY BY HAND
|
|
||||||
|
|
||||||
part of 'library_page.dart';
|
|
||||||
|
|
||||||
// **************************************************************************
|
|
||||||
// RiverpodGenerator
|
|
||||||
// **************************************************************************
|
|
||||||
|
|
||||||
String _$libraryTabObserverHash() =>
|
|
||||||
r'a976ea55e2168e4684114c47592f25a2b187f15f';
|
|
||||||
|
|
||||||
/// See also [libraryTabObserver].
|
|
||||||
@ProviderFor(libraryTabObserver)
|
|
||||||
final libraryTabObserverProvider = Provider<TabObserver>.internal(
|
|
||||||
libraryTabObserver,
|
|
||||||
name: r'libraryTabObserverProvider',
|
|
||||||
debugGetCreateSourceHash: const bool.fromEnvironment('dart.vm.product')
|
|
||||||
? null
|
|
||||||
: _$libraryTabObserverHash,
|
|
||||||
dependencies: null,
|
|
||||||
allTransitiveDependencies: null,
|
|
||||||
);
|
|
||||||
|
|
||||||
typedef LibraryTabObserverRef = ProviderRef<TabObserver>;
|
|
||||||
String _$libraryTabPathHash() => r'fe60984ea9d629683d344f809749b1b9362735fa';
|
|
||||||
|
|
||||||
/// See also [libraryTabPath].
|
|
||||||
@ProviderFor(libraryTabPath)
|
|
||||||
final libraryTabPathProvider = StreamProvider<String>.internal(
|
|
||||||
libraryTabPath,
|
|
||||||
name: r'libraryTabPathProvider',
|
|
||||||
debugGetCreateSourceHash: const bool.fromEnvironment('dart.vm.product')
|
|
||||||
? null
|
|
||||||
: _$libraryTabPathHash,
|
|
||||||
dependencies: null,
|
|
||||||
allTransitiveDependencies: null,
|
|
||||||
);
|
|
||||||
|
|
||||||
typedef LibraryTabPathRef = StreamProviderRef<String>;
|
|
||||||
String _$libraryListQueryHash() => r'6079338e19e0249aaa09868dd405fd3aefc42c2b';
|
|
||||||
|
|
||||||
/// 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 LibraryListQueryRef = ProviderRef<LibraryListQuery>;
|
|
||||||
|
|
||||||
/// See also [libraryListQuery].
|
|
||||||
@ProviderFor(libraryListQuery)
|
|
||||||
const libraryListQueryProvider = LibraryListQueryFamily();
|
|
||||||
|
|
||||||
/// See also [libraryListQuery].
|
|
||||||
class LibraryListQueryFamily extends Family<LibraryListQuery> {
|
|
||||||
/// See also [libraryListQuery].
|
|
||||||
const LibraryListQueryFamily();
|
|
||||||
|
|
||||||
/// See also [libraryListQuery].
|
|
||||||
LibraryListQueryProvider call(
|
|
||||||
int index,
|
|
||||||
) {
|
|
||||||
return LibraryListQueryProvider(
|
|
||||||
index,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
@override
|
|
||||||
LibraryListQueryProvider getProviderOverride(
|
|
||||||
covariant LibraryListQueryProvider provider,
|
|
||||||
) {
|
|
||||||
return call(
|
|
||||||
provider.index,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
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'libraryListQueryProvider';
|
|
||||||
}
|
|
||||||
|
|
||||||
/// See also [libraryListQuery].
|
|
||||||
class LibraryListQueryProvider extends Provider<LibraryListQuery> {
|
|
||||||
/// See also [libraryListQuery].
|
|
||||||
LibraryListQueryProvider(
|
|
||||||
this.index,
|
|
||||||
) : super.internal(
|
|
||||||
(ref) => libraryListQuery(
|
|
||||||
ref,
|
|
||||||
index,
|
|
||||||
),
|
|
||||||
from: libraryListQueryProvider,
|
|
||||||
name: r'libraryListQueryProvider',
|
|
||||||
debugGetCreateSourceHash:
|
|
||||||
const bool.fromEnvironment('dart.vm.product')
|
|
||||||
? null
|
|
||||||
: _$libraryListQueryHash,
|
|
||||||
dependencies: LibraryListQueryFamily._dependencies,
|
|
||||||
allTransitiveDependencies:
|
|
||||||
LibraryListQueryFamily._allTransitiveDependencies,
|
|
||||||
);
|
|
||||||
|
|
||||||
final int index;
|
|
||||||
|
|
||||||
@override
|
|
||||||
bool operator ==(Object other) {
|
|
||||||
return other is LibraryListQueryProvider && other.index == index;
|
|
||||||
}
|
|
||||||
|
|
||||||
@override
|
|
||||||
int get hashCode {
|
|
||||||
var hash = _SystemHash.combine(0, runtimeType.hashCode);
|
|
||||||
hash = _SystemHash.combine(hash, index.hashCode);
|
|
||||||
|
|
||||||
return _SystemHash.finish(hash);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
String _$lastLibraryStateServiceHash() =>
|
|
||||||
r'a49e26b5dc0fcb0f697ec2def08e7336f64c4cb3';
|
|
||||||
|
|
||||||
/// See also [LastLibraryStateService].
|
|
||||||
@ProviderFor(LastLibraryStateService)
|
|
||||||
final lastLibraryStateServiceProvider =
|
|
||||||
AsyncNotifierProvider<LastLibraryStateService, void>.internal(
|
|
||||||
LastLibraryStateService.new,
|
|
||||||
name: r'lastLibraryStateServiceProvider',
|
|
||||||
debugGetCreateSourceHash: const bool.fromEnvironment('dart.vm.product')
|
|
||||||
? null
|
|
||||||
: _$lastLibraryStateServiceHash,
|
|
||||||
dependencies: null,
|
|
||||||
allTransitiveDependencies: null,
|
|
||||||
);
|
|
||||||
|
|
||||||
typedef _$LastLibraryStateService = AsyncNotifier<void>;
|
|
||||||
String _$libraryListsHash() => r'7c9fd1ca3b0d70253e0f5d8197abf18b3a18c995';
|
|
||||||
|
|
||||||
/// See also [LibraryLists].
|
|
||||||
@ProviderFor(LibraryLists)
|
|
||||||
final libraryListsProvider =
|
|
||||||
NotifierProvider<LibraryLists, IList<LibraryListQuery>>.internal(
|
|
||||||
LibraryLists.new,
|
|
||||||
name: r'libraryListsProvider',
|
|
||||||
debugGetCreateSourceHash:
|
|
||||||
const bool.fromEnvironment('dart.vm.product') ? null : _$libraryListsHash,
|
|
||||||
dependencies: null,
|
|
||||||
allTransitiveDependencies: null,
|
|
||||||
);
|
|
||||||
|
|
||||||
typedef _$LibraryLists = Notifier<IList<LibraryListQuery>>;
|
|
||||||
// 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,39 +0,0 @@
|
|||||||
import 'package:auto_route/auto_route.dart';
|
|
||||||
import 'package:flutter/material.dart';
|
|
||||||
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
|
||||||
|
|
||||||
import '../../database/database.dart';
|
|
||||||
import '../../state/settings.dart';
|
|
||||||
import '../app_router.dart';
|
|
||||||
import '../hooks/use_list_query_paging_controller.dart';
|
|
||||||
import '../items.dart';
|
|
||||||
import '../lists.dart';
|
|
||||||
|
|
||||||
class LibraryPlaylistsPage extends HookConsumerWidget {
|
|
||||||
const LibraryPlaylistsPage({super.key});
|
|
||||||
|
|
||||||
@override
|
|
||||||
Widget build(BuildContext context, WidgetRef ref) {
|
|
||||||
final pagingController = useLibraryPagingController(
|
|
||||||
ref,
|
|
||||||
libraryTabIndex: 2,
|
|
||||||
getItems: (query) {
|
|
||||||
final db = ref.read(databaseProvider);
|
|
||||||
final sourceId = ref.read(sourceIdProvider);
|
|
||||||
|
|
||||||
return ref.read(offlineModeProvider)
|
|
||||||
? db.playlistsListDownloaded(sourceId, query).get()
|
|
||||||
: db.playlistsList(sourceId, query).get();
|
|
||||||
},
|
|
||||||
);
|
|
||||||
|
|
||||||
return PagedListQueryView(
|
|
||||||
pagingController: pagingController,
|
|
||||||
refreshSyncAll: true,
|
|
||||||
itemBuilder: (context, item, index) => PlaylistListTile(
|
|
||||||
playlist: item,
|
|
||||||
onTap: () => context.navigateTo(PlaylistSongsRoute(id: item.id)),
|
|
||||||
),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@ -1,81 +0,0 @@
|
|||||||
import 'package:flutter/material.dart';
|
|
||||||
import 'package:flutter_hooks/flutter_hooks.dart';
|
|
||||||
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
|
||||||
import 'package:riverpod_annotation/riverpod_annotation.dart';
|
|
||||||
|
|
||||||
import '../../database/database.dart';
|
|
||||||
import '../../models/music.dart';
|
|
||||||
import '../../models/query.dart';
|
|
||||||
import '../../models/support.dart';
|
|
||||||
import '../../services/audio_service.dart';
|
|
||||||
import '../../state/settings.dart';
|
|
||||||
import '../hooks/use_list_query_paging_controller.dart';
|
|
||||||
import '../items.dart';
|
|
||||||
import '../lists.dart';
|
|
||||||
import 'library_page.dart';
|
|
||||||
import 'songs_page.dart';
|
|
||||||
|
|
||||||
part 'library_songs_page.g.dart';
|
|
||||||
|
|
||||||
@riverpod
|
|
||||||
Future<List<Song>> songsList(SongsListRef ref, ListQuery opt) {
|
|
||||||
final db = ref.watch(databaseProvider);
|
|
||||||
final sourceId = ref.watch(sourceIdProvider);
|
|
||||||
|
|
||||||
return db.songsList(sourceId, opt).get();
|
|
||||||
}
|
|
||||||
|
|
||||||
class LibrarySongsPage extends HookConsumerWidget {
|
|
||||||
const LibrarySongsPage({super.key});
|
|
||||||
|
|
||||||
@override
|
|
||||||
Widget build(BuildContext context, WidgetRef ref) {
|
|
||||||
final audio = ref.watch(audioControlProvider);
|
|
||||||
|
|
||||||
final query = ref.watch(libraryListQueryProvider(3).select(
|
|
||||||
(value) => value.query,
|
|
||||||
));
|
|
||||||
|
|
||||||
final getSongs = useCallback(
|
|
||||||
(ListQuery query) {
|
|
||||||
final db = ref.read(databaseProvider);
|
|
||||||
final sourceId = ref.read(sourceIdProvider);
|
|
||||||
|
|
||||||
return ref.read(offlineModeProvider)
|
|
||||||
? db.songsListDownloaded(sourceId, query).get()
|
|
||||||
: db.songsList(sourceId, query).get();
|
|
||||||
},
|
|
||||||
[],
|
|
||||||
);
|
|
||||||
|
|
||||||
final play = useCallback(
|
|
||||||
({int? index, bool? shuffle}) => audio.playSongs(
|
|
||||||
query: query,
|
|
||||||
getSongs: getSongs,
|
|
||||||
startIndex: index,
|
|
||||||
context: QueueContextType.song,
|
|
||||||
shuffle: shuffle,
|
|
||||||
),
|
|
||||||
[query, getSongs],
|
|
||||||
);
|
|
||||||
|
|
||||||
final pagingController = useLibraryPagingController(
|
|
||||||
ref,
|
|
||||||
libraryTabIndex: 3,
|
|
||||||
getItems: getSongs,
|
|
||||||
);
|
|
||||||
|
|
||||||
return PagedListQueryView(
|
|
||||||
pagingController: pagingController,
|
|
||||||
refreshSyncAll: true,
|
|
||||||
itemBuilder: (context, item, index) => QueueContext(
|
|
||||||
type: QueueContextType.song,
|
|
||||||
child: SongListTile(
|
|
||||||
song: item,
|
|
||||||
image: true,
|
|
||||||
onTap: () => play(index: index),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@ -1,111 +0,0 @@
|
|||||||
// GENERATED CODE - DO NOT MODIFY BY HAND
|
|
||||||
|
|
||||||
part of 'library_songs_page.dart';
|
|
||||||
|
|
||||||
// **************************************************************************
|
|
||||||
// RiverpodGenerator
|
|
||||||
// **************************************************************************
|
|
||||||
|
|
||||||
String _$songsListHash() => r'a3149eb61f8f1ff326e9b1de0ac1c02d7baa831f';
|
|
||||||
|
|
||||||
/// 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 SongsListRef = AutoDisposeFutureProviderRef<List<Song>>;
|
|
||||||
|
|
||||||
/// See also [songsList].
|
|
||||||
@ProviderFor(songsList)
|
|
||||||
const songsListProvider = SongsListFamily();
|
|
||||||
|
|
||||||
/// See also [songsList].
|
|
||||||
class SongsListFamily extends Family<AsyncValue<List<Song>>> {
|
|
||||||
/// See also [songsList].
|
|
||||||
const SongsListFamily();
|
|
||||||
|
|
||||||
/// See also [songsList].
|
|
||||||
SongsListProvider call(
|
|
||||||
ListQuery opt,
|
|
||||||
) {
|
|
||||||
return SongsListProvider(
|
|
||||||
opt,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
@override
|
|
||||||
SongsListProvider getProviderOverride(
|
|
||||||
covariant SongsListProvider provider,
|
|
||||||
) {
|
|
||||||
return call(
|
|
||||||
provider.opt,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
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'songsListProvider';
|
|
||||||
}
|
|
||||||
|
|
||||||
/// See also [songsList].
|
|
||||||
class SongsListProvider extends AutoDisposeFutureProvider<List<Song>> {
|
|
||||||
/// See also [songsList].
|
|
||||||
SongsListProvider(
|
|
||||||
this.opt,
|
|
||||||
) : super.internal(
|
|
||||||
(ref) => songsList(
|
|
||||||
ref,
|
|
||||||
opt,
|
|
||||||
),
|
|
||||||
from: songsListProvider,
|
|
||||||
name: r'songsListProvider',
|
|
||||||
debugGetCreateSourceHash:
|
|
||||||
const bool.fromEnvironment('dart.vm.product')
|
|
||||||
? null
|
|
||||||
: _$songsListHash,
|
|
||||||
dependencies: SongsListFamily._dependencies,
|
|
||||||
allTransitiveDependencies: SongsListFamily._allTransitiveDependencies,
|
|
||||||
);
|
|
||||||
|
|
||||||
final ListQuery opt;
|
|
||||||
|
|
||||||
@override
|
|
||||||
bool operator ==(Object other) {
|
|
||||||
return other is SongsListProvider && other.opt == opt;
|
|
||||||
}
|
|
||||||
|
|
||||||
@override
|
|
||||||
int get hashCode {
|
|
||||||
var hash = _SystemHash.combine(0, runtimeType.hashCode);
|
|
||||||
hash = _SystemHash.combine(hash, opt.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,429 +0,0 @@
|
|||||||
import 'dart:math';
|
|
||||||
|
|
||||||
import 'package:audio_service/audio_service.dart';
|
|
||||||
import 'package:auto_size_text/auto_size_text.dart';
|
|
||||||
import 'package:flutter/material.dart';
|
|
||||||
import 'package:flutter/services.dart';
|
|
||||||
import 'package:flutter_hooks/flutter_hooks.dart';
|
|
||||||
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
|
||||||
import 'package:text_scroll/text_scroll.dart';
|
|
||||||
|
|
||||||
import '../../cache/image_cache.dart';
|
|
||||||
import '../../models/support.dart';
|
|
||||||
import '../../services/audio_service.dart';
|
|
||||||
import '../../state/audio.dart';
|
|
||||||
import '../../state/theme.dart';
|
|
||||||
import '../context_menus.dart';
|
|
||||||
import '../gradient.dart';
|
|
||||||
import '../images.dart';
|
|
||||||
import '../now_playing_bar.dart';
|
|
||||||
|
|
||||||
class NowPlayingPage extends HookConsumerWidget {
|
|
||||||
const NowPlayingPage({super.key});
|
|
||||||
|
|
||||||
@override
|
|
||||||
Widget build(BuildContext context, WidgetRef ref) {
|
|
||||||
final colors = ref.watch(mediaItemThemeProvider).valueOrNull;
|
|
||||||
final itemData = ref.watch(mediaItemDataProvider);
|
|
||||||
|
|
||||||
final theme = Theme.of(context);
|
|
||||||
|
|
||||||
final scaffold = AnnotatedRegion<SystemUiOverlayStyle>(
|
|
||||||
value: SystemUiOverlayStyle.light.copyWith(
|
|
||||||
systemNavigationBarColor: colors?.gradientLow,
|
|
||||||
statusBarColor: Colors.transparent,
|
|
||||||
),
|
|
||||||
child: Scaffold(
|
|
||||||
appBar: AppBar(
|
|
||||||
title: Column(
|
|
||||||
crossAxisAlignment: CrossAxisAlignment.start,
|
|
||||||
children: [
|
|
||||||
Text(
|
|
||||||
itemData?.contextType.value ?? '',
|
|
||||||
style: theme.textTheme.labelMedium,
|
|
||||||
maxLines: 1,
|
|
||||||
softWrap: false,
|
|
||||||
overflow: TextOverflow.fade,
|
|
||||||
),
|
|
||||||
// Text(
|
|
||||||
// itemData?.contextTitle ?? '',
|
|
||||||
// style: theme.textTheme.titleMedium,
|
|
||||||
// maxLines: 1,
|
|
||||||
// softWrap: false,
|
|
||||||
// overflow: TextOverflow.fade,
|
|
||||||
// ),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
),
|
|
||||||
body: Stack(
|
|
||||||
children: [
|
|
||||||
const MediaItemGradient(),
|
|
||||||
Padding(
|
|
||||||
padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 8),
|
|
||||||
child: Column(
|
|
||||||
children: const [
|
|
||||||
Expanded(
|
|
||||||
child: Padding(
|
|
||||||
padding: EdgeInsets.symmetric(horizontal: 16),
|
|
||||||
child: _Art(),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
SizedBox(height: 24),
|
|
||||||
Padding(
|
|
||||||
padding: EdgeInsets.symmetric(horizontal: 16),
|
|
||||||
child: _TrackInfo(),
|
|
||||||
),
|
|
||||||
SizedBox(height: 8),
|
|
||||||
_Progress(),
|
|
||||||
Padding(
|
|
||||||
padding: EdgeInsets.symmetric(horizontal: 16),
|
|
||||||
child: _Controls(),
|
|
||||||
),
|
|
||||||
SizedBox(height: 64),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
),
|
|
||||||
);
|
|
||||||
|
|
||||||
if (colors != null) {
|
|
||||||
return Theme(data: colors.theme, child: scaffold);
|
|
||||||
} else {
|
|
||||||
return scaffold;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
class _Art extends HookConsumerWidget {
|
|
||||||
const _Art();
|
|
||||||
|
|
||||||
@override
|
|
||||||
Widget build(BuildContext context, WidgetRef ref) {
|
|
||||||
final itemData = ref.watch(mediaItemDataProvider);
|
|
||||||
final imageCache = ref.watch(imageCacheProvider);
|
|
||||||
|
|
||||||
UriCacheInfo? cacheInfo;
|
|
||||||
if (itemData?.artCache != null) {
|
|
||||||
cacheInfo = UriCacheInfo(
|
|
||||||
uri: itemData!.artCache!.fullArtUri,
|
|
||||||
cacheKey: itemData.artCache!.fullArtCacheKey,
|
|
||||||
cacheManager: imageCache,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
return AnimatedSwitcher(
|
|
||||||
duration: const Duration(milliseconds: 150),
|
|
||||||
child: CardClip(
|
|
||||||
key: ValueKey(cacheInfo?.cacheKey ?? 'default'),
|
|
||||||
child: cacheInfo != null
|
|
||||||
? CardClip(
|
|
||||||
square: false,
|
|
||||||
child: UriCacheInfoImage(
|
|
||||||
// height: 300,
|
|
||||||
fit: BoxFit.contain,
|
|
||||||
placeholderStyle: PlaceholderStyle.spinner,
|
|
||||||
cache: cacheInfo,
|
|
||||||
),
|
|
||||||
)
|
|
||||||
: const PlaceholderImage(thumbnail: false),
|
|
||||||
),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
class _TrackInfo extends HookConsumerWidget {
|
|
||||||
const _TrackInfo();
|
|
||||||
|
|
||||||
@override
|
|
||||||
Widget build(BuildContext context, WidgetRef ref) {
|
|
||||||
final item = ref.watch(mediaItemProvider).valueOrNull;
|
|
||||||
final theme = Theme.of(context);
|
|
||||||
|
|
||||||
return Row(
|
|
||||||
children: [
|
|
||||||
Expanded(
|
|
||||||
child: Column(
|
|
||||||
crossAxisAlignment: CrossAxisAlignment.start,
|
|
||||||
children: [
|
|
||||||
ScrollableText(
|
|
||||||
item?.title ?? '',
|
|
||||||
style: theme.textTheme.headlineSmall,
|
|
||||||
speed: 50,
|
|
||||||
),
|
|
||||||
Text(
|
|
||||||
item?.artist ?? '',
|
|
||||||
style: theme.textTheme.titleMedium!,
|
|
||||||
maxLines: 1,
|
|
||||||
softWrap: false,
|
|
||||||
overflow: TextOverflow.fade,
|
|
||||||
),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
),
|
|
||||||
IconButton(
|
|
||||||
icon: const Icon(
|
|
||||||
Icons.star_outline_rounded,
|
|
||||||
size: 36,
|
|
||||||
),
|
|
||||||
onPressed: () {},
|
|
||||||
),
|
|
||||||
],
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
class ScrollableText extends StatelessWidget {
|
|
||||||
final String text;
|
|
||||||
final TextStyle? style;
|
|
||||||
final double speed;
|
|
||||||
|
|
||||||
const ScrollableText(
|
|
||||||
this.text, {
|
|
||||||
super.key,
|
|
||||||
this.style,
|
|
||||||
this.speed = 35,
|
|
||||||
});
|
|
||||||
|
|
||||||
@override
|
|
||||||
Widget build(BuildContext context) {
|
|
||||||
final defaultStyle = DefaultTextStyle.of(context);
|
|
||||||
|
|
||||||
return AutoSizeText(
|
|
||||||
text,
|
|
||||||
presetFontSizes: style != null && style?.fontSize != null
|
|
||||||
? [style!.fontSize!]
|
|
||||||
: [defaultStyle.style.fontSize ?? 12],
|
|
||||||
style: style,
|
|
||||||
maxLines: 1,
|
|
||||||
// softWrap: false,
|
|
||||||
overflowReplacement: TextScroll(
|
|
||||||
'$text ',
|
|
||||||
style: style,
|
|
||||||
delayBefore: const Duration(seconds: 3),
|
|
||||||
pauseBetween: const Duration(seconds: 4),
|
|
||||||
mode: TextScrollMode.endless,
|
|
||||||
velocity: Velocity(pixelsPerSecond: Offset(speed, 0)),
|
|
||||||
),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
class _Progress extends HookConsumerWidget {
|
|
||||||
const _Progress();
|
|
||||||
|
|
||||||
@override
|
|
||||||
Widget build(BuildContext context, WidgetRef ref) {
|
|
||||||
final colors = ref.watch(mediaItemThemeProvider).valueOrNull;
|
|
||||||
final position = ref.watch(positionProvider);
|
|
||||||
final duration = ref.watch(durationProvider);
|
|
||||||
final audio = ref.watch(audioControlProvider);
|
|
||||||
|
|
||||||
final changeValue = useState(position.toDouble());
|
|
||||||
final changing = useState(false);
|
|
||||||
|
|
||||||
return Column(
|
|
||||||
children: [
|
|
||||||
Slider(
|
|
||||||
value: changing.value ? changeValue.value : position.toDouble(),
|
|
||||||
min: 0,
|
|
||||||
max: max(duration.toDouble(), position.toDouble()),
|
|
||||||
thumbColor: colors?.theme.colorScheme.onBackground,
|
|
||||||
activeColor: colors?.theme.colorScheme.onBackground,
|
|
||||||
inactiveColor: colors?.theme.colorScheme.surface,
|
|
||||||
onChanged: (value) {
|
|
||||||
changeValue.value = value;
|
|
||||||
},
|
|
||||||
onChangeStart: (value) {
|
|
||||||
changing.value = true;
|
|
||||||
},
|
|
||||||
onChangeEnd: (value) {
|
|
||||||
changing.value = false;
|
|
||||||
audio.seek(Duration(seconds: value.toInt()));
|
|
||||||
},
|
|
||||||
),
|
|
||||||
Padding(
|
|
||||||
padding: const EdgeInsets.symmetric(horizontal: 24),
|
|
||||||
child: DefaultTextStyle(
|
|
||||||
style: Theme.of(context).textTheme.titleMedium!,
|
|
||||||
child: Row(
|
|
||||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
|
||||||
children: [
|
|
||||||
Text(Duration(
|
|
||||||
seconds: changing.value
|
|
||||||
? changeValue.value.toInt()
|
|
||||||
: position)
|
|
||||||
.toString()
|
|
||||||
.substring(2, 7)),
|
|
||||||
Text(Duration(seconds: duration).toString().substring(2, 7)),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
),
|
|
||||||
)
|
|
||||||
],
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
class RepeatButton extends HookConsumerWidget {
|
|
||||||
final double size;
|
|
||||||
|
|
||||||
const RepeatButton({
|
|
||||||
super.key,
|
|
||||||
required this.size,
|
|
||||||
});
|
|
||||||
|
|
||||||
@override
|
|
||||||
Widget build(BuildContext context, WidgetRef ref) {
|
|
||||||
final audio = ref.watch(audioControlProvider);
|
|
||||||
final repeat = ref.watch(repeatModeProvider);
|
|
||||||
|
|
||||||
IconData icon;
|
|
||||||
void Function() action;
|
|
||||||
|
|
||||||
switch (repeat) {
|
|
||||||
case AudioServiceRepeatMode.all:
|
|
||||||
case AudioServiceRepeatMode.group:
|
|
||||||
icon = Icons.repeat_on_rounded;
|
|
||||||
action = () => audio.setRepeatMode(AudioServiceRepeatMode.one);
|
|
||||||
break;
|
|
||||||
case AudioServiceRepeatMode.one:
|
|
||||||
icon = Icons.repeat_one_on_rounded;
|
|
||||||
action = () => audio.setRepeatMode(AudioServiceRepeatMode.none);
|
|
||||||
break;
|
|
||||||
default:
|
|
||||||
icon = Icons.repeat_rounded;
|
|
||||||
action = () => audio.setRepeatMode(AudioServiceRepeatMode.all);
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
|
|
||||||
return IconButton(
|
|
||||||
icon: Icon(icon),
|
|
||||||
padding: EdgeInsets.zero,
|
|
||||||
iconSize: 30,
|
|
||||||
onPressed: action,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
class ShuffleButton extends HookConsumerWidget {
|
|
||||||
final double size;
|
|
||||||
|
|
||||||
const ShuffleButton({
|
|
||||||
super.key,
|
|
||||||
required this.size,
|
|
||||||
});
|
|
||||||
|
|
||||||
@override
|
|
||||||
Widget build(BuildContext context, WidgetRef ref) {
|
|
||||||
final audio = ref.watch(audioControlProvider);
|
|
||||||
final shuffle = ref.watch(shuffleModeProvider);
|
|
||||||
final queueMode = ref.watch(queueModeProvider).valueOrNull;
|
|
||||||
|
|
||||||
IconData icon;
|
|
||||||
void Function() action;
|
|
||||||
|
|
||||||
switch (shuffle) {
|
|
||||||
case AudioServiceShuffleMode.all:
|
|
||||||
case AudioServiceShuffleMode.group:
|
|
||||||
icon = Icons.shuffle_on_rounded;
|
|
||||||
action = () => audio.setShuffleMode(AudioServiceShuffleMode.none);
|
|
||||||
break;
|
|
||||||
default:
|
|
||||||
icon = Icons.shuffle_rounded;
|
|
||||||
action = () => audio.setShuffleMode(AudioServiceShuffleMode.all);
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
|
|
||||||
return IconButton(
|
|
||||||
icon: Icon(queueMode == QueueMode.radio ? Icons.radio_rounded : icon),
|
|
||||||
padding: EdgeInsets.zero,
|
|
||||||
iconSize: 30,
|
|
||||||
onPressed: queueMode == QueueMode.radio ? null : action,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
class _Controls extends HookConsumerWidget {
|
|
||||||
const _Controls();
|
|
||||||
|
|
||||||
@override
|
|
||||||
Widget build(BuildContext context, WidgetRef ref) {
|
|
||||||
final base = ref.watch(baseThemeProvider);
|
|
||||||
final audio = ref.watch(audioControlProvider);
|
|
||||||
|
|
||||||
return IconTheme(
|
|
||||||
data: IconThemeData(color: base.theme.colorScheme.onBackground),
|
|
||||||
child: Column(
|
|
||||||
children: [
|
|
||||||
SizedBox(
|
|
||||||
height: 100,
|
|
||||||
child: Row(
|
|
||||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
|
||||||
children: [
|
|
||||||
const RepeatButton(size: 30),
|
|
||||||
IconButton(
|
|
||||||
icon: const Icon(Icons.skip_previous_rounded),
|
|
||||||
padding: EdgeInsets.zero,
|
|
||||||
iconSize: 60,
|
|
||||||
onPressed: () => audio.skipToPrevious(),
|
|
||||||
),
|
|
||||||
const PlayPauseButton(size: 90),
|
|
||||||
IconButton(
|
|
||||||
icon: const Icon(Icons.skip_next_rounded),
|
|
||||||
padding: EdgeInsets.zero,
|
|
||||||
iconSize: 60,
|
|
||||||
onPressed: () => audio.skipToNext(),
|
|
||||||
),
|
|
||||||
const ShuffleButton(size: 30),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
),
|
|
||||||
SizedBox(
|
|
||||||
height: 40,
|
|
||||||
child: Row(
|
|
||||||
// crossAxisAlignment: CrossAxisAlignment.stretch,
|
|
||||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
|
||||||
children: [
|
|
||||||
IconButton(
|
|
||||||
icon: const Icon(Icons.queue_music_rounded),
|
|
||||||
padding: EdgeInsets.zero,
|
|
||||||
iconSize: 30,
|
|
||||||
onPressed: () {},
|
|
||||||
),
|
|
||||||
const _MoreButton(),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
class _MoreButton extends HookConsumerWidget {
|
|
||||||
const _MoreButton();
|
|
||||||
|
|
||||||
@override
|
|
||||||
Widget build(BuildContext context, WidgetRef ref) {
|
|
||||||
final song = ref.watch(mediaItemSongProvider).valueOrNull;
|
|
||||||
|
|
||||||
return IconButton(
|
|
||||||
icon: const Icon(Icons.more_horiz),
|
|
||||||
padding: EdgeInsets.zero,
|
|
||||||
iconSize: 30,
|
|
||||||
onPressed: song != null
|
|
||||||
? () {
|
|
||||||
showContextMenu(
|
|
||||||
context: context,
|
|
||||||
ref: ref,
|
|
||||||
builder: (context) => BottomSheetMenu(
|
|
||||||
child: SongContextMenu(song: song),
|
|
||||||
),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
: null,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@ -1,247 +0,0 @@
|
|||||||
import 'package:auto_route/auto_route.dart';
|
|
||||||
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:flutter_keyboard_visibility/flutter_keyboard_visibility.dart';
|
|
||||||
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
|
||||||
import 'package:riverpod_annotation/riverpod_annotation.dart';
|
|
||||||
|
|
||||||
import '../../database/database.dart';
|
|
||||||
import '../../models/music.dart';
|
|
||||||
import '../../models/query.dart';
|
|
||||||
import '../../models/support.dart';
|
|
||||||
import '../../services/audio_service.dart';
|
|
||||||
import '../../state/music.dart';
|
|
||||||
import '../../state/settings.dart';
|
|
||||||
import '../app_router.dart';
|
|
||||||
import '../items.dart';
|
|
||||||
import 'songs_page.dart';
|
|
||||||
|
|
||||||
part 'search_page.g.dart';
|
|
||||||
|
|
||||||
@riverpod
|
|
||||||
class SearchQuery extends _$SearchQuery {
|
|
||||||
@override
|
|
||||||
String? build() {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
void setQuery(String query) {
|
|
||||||
state = query;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@riverpod
|
|
||||||
FutureOr<SearchResults> searchResult(SearchResultRef ref) async {
|
|
||||||
final query = ref.watch(searchQueryProvider);
|
|
||||||
final db = ref.watch(databaseProvider);
|
|
||||||
final sourceId = ref.watch(sourceIdProvider);
|
|
||||||
|
|
||||||
final ftsQuery = '(source_id : $sourceId) AND (- source_id : "$query"*)';
|
|
||||||
|
|
||||||
final songRowIds = await db.searchSongs(ftsQuery, 5, 0).get();
|
|
||||||
final songs = await db.songsInRowIds(songRowIds).get();
|
|
||||||
final albumRowIds = await db.searchAlbums(ftsQuery, 5, 0).get();
|
|
||||||
final albums = await db.albumsInRowIds(albumRowIds).get();
|
|
||||||
final artistRowIds = await db.searchArtists(ftsQuery, 5, 0).get();
|
|
||||||
final artists = await db.artistsInRowIds(artistRowIds).get();
|
|
||||||
|
|
||||||
return SearchResults(
|
|
||||||
songs: songs.lock,
|
|
||||||
albums: albums.lock,
|
|
||||||
artists: artists.lock,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
class SearchPage extends HookConsumerWidget {
|
|
||||||
const SearchPage({super.key});
|
|
||||||
|
|
||||||
@override
|
|
||||||
Widget build(BuildContext context, WidgetRef ref) {
|
|
||||||
final results = ref.watch(searchResultProvider).valueOrNull;
|
|
||||||
|
|
||||||
return KeyboardDismissOnTap(
|
|
||||||
dismissOnCapturedTaps: true,
|
|
||||||
child: Scaffold(
|
|
||||||
body: SafeArea(
|
|
||||||
child: CustomScrollView(
|
|
||||||
reverse: true,
|
|
||||||
slivers: [
|
|
||||||
const SliverToBoxAdapter(child: _SearchBar()),
|
|
||||||
if (results != null && results.songs.isNotEmpty)
|
|
||||||
_SongsSection(songs: results.songs),
|
|
||||||
if (results != null && results.albums.isNotEmpty)
|
|
||||||
_AlbumsSection(albums: results.albums),
|
|
||||||
if (results != null && results.artists.isNotEmpty)
|
|
||||||
_ArtistsSection(artists: results.artists),
|
|
||||||
if (results != null)
|
|
||||||
const SliverPadding(padding: EdgeInsets.only(top: 96))
|
|
||||||
],
|
|
||||||
),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
class _SearchBar extends HookConsumerWidget {
|
|
||||||
const _SearchBar();
|
|
||||||
|
|
||||||
@override
|
|
||||||
Widget build(BuildContext context, WidgetRef ref) {
|
|
||||||
final controller = useTextEditingController(text: '');
|
|
||||||
|
|
||||||
final theme = Theme.of(context);
|
|
||||||
final l = AppLocalizations.of(context);
|
|
||||||
|
|
||||||
return Container(
|
|
||||||
color: ElevationOverlay.applySurfaceTint(
|
|
||||||
theme.colorScheme.surface,
|
|
||||||
theme.colorScheme.surfaceTint,
|
|
||||||
1,
|
|
||||||
),
|
|
||||||
child: Padding(
|
|
||||||
padding: const EdgeInsets.only(
|
|
||||||
right: 24,
|
|
||||||
left: 24,
|
|
||||||
bottom: 24,
|
|
||||||
top: 8,
|
|
||||||
),
|
|
||||||
child: IgnoreKeyboardDismiss(
|
|
||||||
child: TextFormField(
|
|
||||||
controller: controller,
|
|
||||||
decoration: InputDecoration(
|
|
||||||
hintText: l.searchInputPlaceholder,
|
|
||||||
),
|
|
||||||
onChanged: (value) {
|
|
||||||
ref.read(searchQueryProvider.notifier).setQuery(value);
|
|
||||||
},
|
|
||||||
),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
class _SectionHeader extends HookConsumerWidget {
|
|
||||||
final String title;
|
|
||||||
|
|
||||||
const _SectionHeader({required this.title});
|
|
||||||
|
|
||||||
@override
|
|
||||||
Widget build(BuildContext context, WidgetRef ref) {
|
|
||||||
final theme = Theme.of(context).textTheme;
|
|
||||||
|
|
||||||
return Padding(
|
|
||||||
padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 8),
|
|
||||||
child: Text(title, style: theme.headlineMedium),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
class _Section extends HookConsumerWidget {
|
|
||||||
final String title;
|
|
||||||
final Iterable<Widget> children;
|
|
||||||
|
|
||||||
const _Section({
|
|
||||||
required this.title,
|
|
||||||
required this.children,
|
|
||||||
});
|
|
||||||
|
|
||||||
@override
|
|
||||||
Widget build(BuildContext context, WidgetRef ref) {
|
|
||||||
return SliverList(
|
|
||||||
delegate: SliverChildListDelegate([
|
|
||||||
const SizedBox(height: 16),
|
|
||||||
...children.toList().reversed,
|
|
||||||
_SectionHeader(title: title),
|
|
||||||
]),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
class _SongsSection extends HookConsumerWidget {
|
|
||||||
final IList<Song>? songs;
|
|
||||||
|
|
||||||
const _SongsSection({required this.songs});
|
|
||||||
|
|
||||||
@override
|
|
||||||
Widget build(BuildContext context, WidgetRef ref) {
|
|
||||||
final l = AppLocalizations.of(context);
|
|
||||||
|
|
||||||
return _Section(
|
|
||||||
title: l.resourcesSongName(100),
|
|
||||||
children: (songs ?? <Song>[]).map(
|
|
||||||
(song) => QueueContext(
|
|
||||||
type: QueueContextType.album,
|
|
||||||
id: song.albumId!,
|
|
||||||
child: SongListTile(
|
|
||||||
song: song,
|
|
||||||
image: true,
|
|
||||||
onTap: () async {
|
|
||||||
const query = ListQuery(
|
|
||||||
sort: SortBy(column: 'disc, track'),
|
|
||||||
);
|
|
||||||
final albumSongs = await ref.read(
|
|
||||||
albumSongsListProvider(song.albumId!, query).future,
|
|
||||||
);
|
|
||||||
|
|
||||||
ref.read(audioControlProvider).playSongs(
|
|
||||||
context: QueueContextType.album,
|
|
||||||
contextId: song.albumId!,
|
|
||||||
shuffle: true,
|
|
||||||
startIndex: albumSongs.indexOf(song),
|
|
||||||
query: query,
|
|
||||||
getSongs: (query) => ref.read(
|
|
||||||
albumSongsListProvider(song.albumId!, query).future),
|
|
||||||
);
|
|
||||||
},
|
|
||||||
),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
class _AlbumsSection extends HookConsumerWidget {
|
|
||||||
final IList<Album>? albums;
|
|
||||||
|
|
||||||
const _AlbumsSection({required this.albums});
|
|
||||||
|
|
||||||
@override
|
|
||||||
Widget build(BuildContext context, WidgetRef ref) {
|
|
||||||
final l = AppLocalizations.of(context);
|
|
||||||
|
|
||||||
return _Section(
|
|
||||||
title: l.resourcesAlbumName(100),
|
|
||||||
children: (albums ?? <Album>[]).map(
|
|
||||||
(album) => AlbumListTile(
|
|
||||||
album: album,
|
|
||||||
onTap: () => context.navigateTo(AlbumSongsRoute(id: album.id)),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
class _ArtistsSection extends HookConsumerWidget {
|
|
||||||
final IList<Artist>? artists;
|
|
||||||
|
|
||||||
const _ArtistsSection({required this.artists});
|
|
||||||
|
|
||||||
@override
|
|
||||||
Widget build(BuildContext context, WidgetRef ref) {
|
|
||||||
final l = AppLocalizations.of(context);
|
|
||||||
|
|
||||||
return _Section(
|
|
||||||
title: l.resourcesArtistName(100),
|
|
||||||
children: (artists ?? <Artist>[]).map(
|
|
||||||
(artist) => ArtistListTile(
|
|
||||||
artist: artist,
|
|
||||||
onTap: () => context.navigateTo(ArtistRoute(id: artist.id)),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@ -1,38 +0,0 @@
|
|||||||
// GENERATED CODE - DO NOT MODIFY BY HAND
|
|
||||||
|
|
||||||
part of 'search_page.dart';
|
|
||||||
|
|
||||||
// **************************************************************************
|
|
||||||
// RiverpodGenerator
|
|
||||||
// **************************************************************************
|
|
||||||
|
|
||||||
String _$searchResultHash() => r'e5240c0c51937e1e946138d27aeaea93dc0231c3';
|
|
||||||
|
|
||||||
/// See also [searchResult].
|
|
||||||
@ProviderFor(searchResult)
|
|
||||||
final searchResultProvider = AutoDisposeFutureProvider<SearchResults>.internal(
|
|
||||||
searchResult,
|
|
||||||
name: r'searchResultProvider',
|
|
||||||
debugGetCreateSourceHash:
|
|
||||||
const bool.fromEnvironment('dart.vm.product') ? null : _$searchResultHash,
|
|
||||||
dependencies: null,
|
|
||||||
allTransitiveDependencies: null,
|
|
||||||
);
|
|
||||||
|
|
||||||
typedef SearchResultRef = AutoDisposeFutureProviderRef<SearchResults>;
|
|
||||||
String _$searchQueryHash() => r'f7624215b3d5a8b917cb0af239666a19a18d91d5';
|
|
||||||
|
|
||||||
/// See also [SearchQuery].
|
|
||||||
@ProviderFor(SearchQuery)
|
|
||||||
final searchQueryProvider =
|
|
||||||
AutoDisposeNotifierProvider<SearchQuery, String?>.internal(
|
|
||||||
SearchQuery.new,
|
|
||||||
name: r'searchQueryProvider',
|
|
||||||
debugGetCreateSourceHash:
|
|
||||||
const bool.fromEnvironment('dart.vm.product') ? null : _$searchQueryHash,
|
|
||||||
dependencies: null,
|
|
||||||
allTransitiveDependencies: null,
|
|
||||||
);
|
|
||||||
|
|
||||||
typedef _$SearchQuery = AutoDisposeNotifier<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,448 +0,0 @@
|
|||||||
import 'dart:math';
|
|
||||||
|
|
||||||
import 'package:auto_route/auto_route.dart';
|
|
||||||
import 'package:fast_immutable_collections/fast_immutable_collections.dart';
|
|
||||||
import 'package:flutter/foundation.dart';
|
|
||||||
import 'package:flutter/material.dart';
|
|
||||||
import 'package:flutter_gen/gen_l10n/app_localizations.dart';
|
|
||||||
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
|
||||||
import 'package:path/path.dart' as p;
|
|
||||||
import 'package:share_plus/share_plus.dart';
|
|
||||||
import 'package:url_launcher/url_launcher.dart';
|
|
||||||
|
|
||||||
import '../../log.dart';
|
|
||||||
import '../../models/support.dart';
|
|
||||||
import '../../services/settings_service.dart';
|
|
||||||
import '../../state/init.dart';
|
|
||||||
import '../../state/settings.dart';
|
|
||||||
import '../app_router.dart';
|
|
||||||
import '../dialogs.dart';
|
|
||||||
|
|
||||||
const kHorizontalPadding = 16.0;
|
|
||||||
|
|
||||||
class SettingsPage extends HookConsumerWidget {
|
|
||||||
const SettingsPage({super.key});
|
|
||||||
|
|
||||||
@override
|
|
||||||
Widget build(BuildContext context, WidgetRef ref) {
|
|
||||||
final l = AppLocalizations.of(context);
|
|
||||||
// final downloads = ref.watch(downloadServiceProvider.select(
|
|
||||||
// (value) => value.downloads,
|
|
||||||
// ));
|
|
||||||
|
|
||||||
return Scaffold(
|
|
||||||
body: ListView(
|
|
||||||
children: [
|
|
||||||
const SizedBox(height: 96),
|
|
||||||
_SectionHeader(l.settingsServersName),
|
|
||||||
const _Sources(),
|
|
||||||
_SectionHeader(l.settingsNetworkName),
|
|
||||||
const _Network(),
|
|
||||||
_SectionHeader(l.settingsAboutName),
|
|
||||||
_About(),
|
|
||||||
// const _SectionHeader('Downloads'),
|
|
||||||
// _Section(
|
|
||||||
// children: downloads
|
|
||||||
// .map(
|
|
||||||
// (e) => ListTile(
|
|
||||||
// isThreeLine: true,
|
|
||||||
// title: Text(e.filename ?? e.url),
|
|
||||||
// subtitle: Column(
|
|
||||||
// mainAxisAlignment: MainAxisAlignment.start,
|
|
||||||
// children: [
|
|
||||||
// Row(children: [Text('Progress: ${e.progress}%')]),
|
|
||||||
// Row(children: [Text('Status: ${e.status})')]),
|
|
||||||
// Text('Status: ${e.savedDir}'),
|
|
||||||
// ],
|
|
||||||
// ),
|
|
||||||
// trailing:
|
|
||||||
// CircularProgressIndicator(value: e.progress / 100),
|
|
||||||
// ),
|
|
||||||
// )
|
|
||||||
// .toList(),
|
|
||||||
// ),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
class _Section extends StatelessWidget {
|
|
||||||
final List<Widget> children;
|
|
||||||
|
|
||||||
const _Section({required this.children});
|
|
||||||
|
|
||||||
@override
|
|
||||||
Widget build(BuildContext context) {
|
|
||||||
return Column(
|
|
||||||
crossAxisAlignment: CrossAxisAlignment.stretch,
|
|
||||||
children: [
|
|
||||||
...children,
|
|
||||||
const SizedBox(height: 32),
|
|
||||||
],
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
class _SectionHeader extends StatelessWidget {
|
|
||||||
final String title;
|
|
||||||
|
|
||||||
const _SectionHeader(this.title);
|
|
||||||
|
|
||||||
@override
|
|
||||||
Widget build(BuildContext context) {
|
|
||||||
final theme = Theme.of(context);
|
|
||||||
|
|
||||||
return Column(
|
|
||||||
children: [
|
|
||||||
SizedBox(
|
|
||||||
width: double.infinity,
|
|
||||||
child: Padding(
|
|
||||||
padding: const EdgeInsets.symmetric(horizontal: kHorizontalPadding),
|
|
||||||
child: Text(
|
|
||||||
title,
|
|
||||||
style: theme.textTheme.displaySmall,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
const SizedBox(height: 12),
|
|
||||||
],
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
class _Network extends StatelessWidget {
|
|
||||||
const _Network();
|
|
||||||
|
|
||||||
@override
|
|
||||||
Widget build(BuildContext context) {
|
|
||||||
return const _Section(
|
|
||||||
children: [
|
|
||||||
_OfflineMode(),
|
|
||||||
_MaxBitrateWifi(),
|
|
||||||
_MaxBitrateMobile(),
|
|
||||||
_StreamFormat(),
|
|
||||||
],
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
class _About extends HookConsumerWidget {
|
|
||||||
_About();
|
|
||||||
|
|
||||||
final _homepage = Uri.parse('https://github.com/austinried/subtracks');
|
|
||||||
final _donate = Uri.parse('https://ko-fi.com/austinried');
|
|
||||||
|
|
||||||
@override
|
|
||||||
Widget build(BuildContext context, WidgetRef ref) {
|
|
||||||
final l = AppLocalizations.of(context);
|
|
||||||
final pkg = ref.watch(packageInfoProvider).requireValue;
|
|
||||||
|
|
||||||
return _Section(
|
|
||||||
children: [
|
|
||||||
ListTile(
|
|
||||||
title: const Text('subtracks'),
|
|
||||||
subtitle: Text(l.settingsAboutVersion(pkg.version)),
|
|
||||||
),
|
|
||||||
ListTile(
|
|
||||||
title: Text(l.settingsAboutActionsLicenses),
|
|
||||||
// trailing: const Icon(Icons.open_in_new_rounded),
|
|
||||||
onTap: () {},
|
|
||||||
),
|
|
||||||
ListTile(
|
|
||||||
title: Text(l.settingsAboutActionsProjectHomepage),
|
|
||||||
subtitle: Text(_homepage.toString()),
|
|
||||||
trailing: const Icon(Icons.open_in_new_rounded),
|
|
||||||
onTap: () => launchUrl(
|
|
||||||
_homepage,
|
|
||||||
mode: LaunchMode.externalApplication,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
ListTile(
|
|
||||||
title: Text(l.settingsAboutActionsSupport),
|
|
||||||
subtitle: Text(_donate.toString()),
|
|
||||||
trailing: const Icon(Icons.open_in_new_rounded),
|
|
||||||
onTap: () => launchUrl(
|
|
||||||
_donate,
|
|
||||||
mode: LaunchMode.externalApplication,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
const SizedBox(height: 12),
|
|
||||||
const _ShareLogsButton(),
|
|
||||||
],
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
class _ShareLogsButton extends StatelessWidget {
|
|
||||||
const _ShareLogsButton();
|
|
||||||
|
|
||||||
@override
|
|
||||||
Widget build(BuildContext context) {
|
|
||||||
final l = AppLocalizations.of(context);
|
|
||||||
|
|
||||||
return Row(
|
|
||||||
mainAxisAlignment: MainAxisAlignment.center,
|
|
||||||
children: [
|
|
||||||
OutlinedButton.icon(
|
|
||||||
icon: const Icon(Icons.share),
|
|
||||||
label: Text(l.settingsAboutShareLogs),
|
|
||||||
onPressed: () async {
|
|
||||||
final files = await logFiles();
|
|
||||||
if (files.isEmpty) return;
|
|
||||||
|
|
||||||
// ignore: use_build_context_synchronously
|
|
||||||
final value = await showDialog<String>(
|
|
||||||
context: context,
|
|
||||||
builder: (context) => MultipleChoiceDialog<String>(
|
|
||||||
title: l.settingsAboutChooseLog,
|
|
||||||
current: files.first.path,
|
|
||||||
options: files
|
|
||||||
.map((e) => MultiChoiceOption.string(
|
|
||||||
title: p.basename(e.path),
|
|
||||||
option: e.path,
|
|
||||||
))
|
|
||||||
.toIList(),
|
|
||||||
),
|
|
||||||
);
|
|
||||||
|
|
||||||
if (value == null) return;
|
|
||||||
Share.shareXFiles(
|
|
||||||
[XFile(value, mimeType: 'text/plain')],
|
|
||||||
subject: 'Logs from subtracks: ${String.fromCharCodes(
|
|
||||||
List.generate(8, (_) => Random().nextInt(26) + 65),
|
|
||||||
)}',
|
|
||||||
);
|
|
||||||
},
|
|
||||||
),
|
|
||||||
],
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
class _MaxBitrateWifi extends HookConsumerWidget {
|
|
||||||
const _MaxBitrateWifi();
|
|
||||||
|
|
||||||
@override
|
|
||||||
Widget build(BuildContext context, WidgetRef ref) {
|
|
||||||
final bitrate = ref.watch(settingsServiceProvider.select(
|
|
||||||
(value) => value.app.maxBitrateWifi,
|
|
||||||
));
|
|
||||||
final l = AppLocalizations.of(context);
|
|
||||||
|
|
||||||
return _MaxBitrateOption(
|
|
||||||
title: l.settingsNetworkOptionsMaxBitrateWifiTitle,
|
|
||||||
bitrate: bitrate,
|
|
||||||
onChange: (value) {
|
|
||||||
ref.read(settingsServiceProvider.notifier).setMaxBitrateWifi(value);
|
|
||||||
},
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
class _MaxBitrateMobile extends HookConsumerWidget {
|
|
||||||
const _MaxBitrateMobile();
|
|
||||||
|
|
||||||
@override
|
|
||||||
Widget build(BuildContext context, WidgetRef ref) {
|
|
||||||
final bitrate = ref.watch(settingsServiceProvider.select(
|
|
||||||
(value) => value.app.maxBitrateMobile,
|
|
||||||
));
|
|
||||||
final l = AppLocalizations.of(context);
|
|
||||||
|
|
||||||
return _MaxBitrateOption(
|
|
||||||
title: l.settingsNetworkOptionsMaxBitrateMobileTitle,
|
|
||||||
bitrate: bitrate,
|
|
||||||
onChange: (value) {
|
|
||||||
ref.read(settingsServiceProvider.notifier).setMaxBitrateMobile(value);
|
|
||||||
},
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
class _MaxBitrateOption extends HookConsumerWidget {
|
|
||||||
final String title;
|
|
||||||
final int bitrate;
|
|
||||||
final void Function(int value) onChange;
|
|
||||||
|
|
||||||
const _MaxBitrateOption({
|
|
||||||
required this.title,
|
|
||||||
required this.bitrate,
|
|
||||||
required this.onChange,
|
|
||||||
});
|
|
||||||
|
|
||||||
static const options = [0, 24, 32, 64, 96, 128, 192, 256, 320];
|
|
||||||
|
|
||||||
String _bitrateText(AppLocalizations l, int bitrate) {
|
|
||||||
return bitrate == 0
|
|
||||||
? l.settingsNetworkValuesUnlimitedKbps
|
|
||||||
: l.settingsNetworkValuesKbps(bitrate.toString());
|
|
||||||
}
|
|
||||||
|
|
||||||
@override
|
|
||||||
Widget build(BuildContext context, WidgetRef ref) {
|
|
||||||
final l = AppLocalizations.of(context);
|
|
||||||
|
|
||||||
return ListTile(
|
|
||||||
title: Text(title),
|
|
||||||
subtitle: Text(_bitrateText(l, bitrate)),
|
|
||||||
onTap: () async {
|
|
||||||
final value = await showDialog<int>(
|
|
||||||
context: context,
|
|
||||||
builder: (context) => MultipleChoiceDialog<int>(
|
|
||||||
title: title,
|
|
||||||
current: bitrate,
|
|
||||||
options: options
|
|
||||||
.map((opt) => MultiChoiceOption.int(
|
|
||||||
title: _bitrateText(l, opt),
|
|
||||||
option: opt,
|
|
||||||
))
|
|
||||||
.toIList(),
|
|
||||||
),
|
|
||||||
);
|
|
||||||
|
|
||||||
if (value != null) {
|
|
||||||
onChange(value);
|
|
||||||
}
|
|
||||||
},
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
class _StreamFormat extends HookConsumerWidget {
|
|
||||||
const _StreamFormat();
|
|
||||||
|
|
||||||
static const options = ['', 'mp3', 'opus', 'ogg'];
|
|
||||||
|
|
||||||
@override
|
|
||||||
Widget build(BuildContext context, WidgetRef ref) {
|
|
||||||
final streamFormat = ref.watch(
|
|
||||||
settingsServiceProvider.select((value) => value.app.streamFormat),
|
|
||||||
);
|
|
||||||
final l = AppLocalizations.of(context);
|
|
||||||
|
|
||||||
return ListTile(
|
|
||||||
title: Text(l.settingsNetworkOptionsStreamFormat),
|
|
||||||
subtitle: Text(
|
|
||||||
streamFormat ?? l.settingsNetworkOptionsStreamFormatServerDefault,
|
|
||||||
),
|
|
||||||
onTap: () async {
|
|
||||||
final value = await showDialog<String>(
|
|
||||||
context: context,
|
|
||||||
builder: (context) => MultipleChoiceDialog<String>(
|
|
||||||
title: l.settingsNetworkOptionsStreamFormat,
|
|
||||||
current: streamFormat ?? '',
|
|
||||||
options: options
|
|
||||||
.map((opt) => MultiChoiceOption.string(
|
|
||||||
title: opt == ''
|
|
||||||
? l.settingsNetworkOptionsStreamFormatServerDefault
|
|
||||||
: opt,
|
|
||||||
option: opt,
|
|
||||||
))
|
|
||||||
.toIList(),
|
|
||||||
),
|
|
||||||
);
|
|
||||||
|
|
||||||
if (value != null) {
|
|
||||||
ref
|
|
||||||
.read(settingsServiceProvider.notifier)
|
|
||||||
.setStreamFormat(value == '' ? null : value);
|
|
||||||
}
|
|
||||||
},
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
class _OfflineMode extends HookConsumerWidget {
|
|
||||||
const _OfflineMode();
|
|
||||||
|
|
||||||
@override
|
|
||||||
Widget build(BuildContext context, WidgetRef ref) {
|
|
||||||
final offline = ref.watch(offlineModeProvider);
|
|
||||||
final l = AppLocalizations.of(context);
|
|
||||||
|
|
||||||
return SwitchListTile(
|
|
||||||
value: offline,
|
|
||||||
title: Text(l.settingsNetworkOptionsOfflineMode),
|
|
||||||
subtitle: offline
|
|
||||||
? Text(l.settingsNetworkOptionsOfflineModeOn)
|
|
||||||
: Text(l.settingsNetworkOptionsOfflineModeOff),
|
|
||||||
onChanged: (value) {
|
|
||||||
ref.read(offlineModeProvider.notifier).setMode(value);
|
|
||||||
},
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
class _Sources extends HookConsumerWidget {
|
|
||||||
const _Sources();
|
|
||||||
|
|
||||||
@override
|
|
||||||
Widget build(BuildContext context, WidgetRef ref) {
|
|
||||||
final sources = ref.watch(settingsServiceProvider.select(
|
|
||||||
(value) => value.sources,
|
|
||||||
));
|
|
||||||
final activeSource = ref.watch(settingsServiceProvider.select(
|
|
||||||
(value) => value.activeSource,
|
|
||||||
));
|
|
||||||
|
|
||||||
final l = AppLocalizations.of(context);
|
|
||||||
|
|
||||||
return _Section(
|
|
||||||
children: [
|
|
||||||
for (var source in sources)
|
|
||||||
RadioListTile<int>(
|
|
||||||
value: source.id,
|
|
||||||
groupValue: activeSource?.id,
|
|
||||||
onChanged: (value) {
|
|
||||||
ref
|
|
||||||
.read(settingsServiceProvider.notifier)
|
|
||||||
.setActiveSource(source.id);
|
|
||||||
},
|
|
||||||
title: Text(source.name),
|
|
||||||
subtitle: Text(
|
|
||||||
source.address.toString(),
|
|
||||||
maxLines: 1,
|
|
||||||
softWrap: false,
|
|
||||||
overflow: TextOverflow.fade,
|
|
||||||
),
|
|
||||||
secondary: IconButton(
|
|
||||||
icon: const Icon(Icons.edit_rounded),
|
|
||||||
onPressed: () {
|
|
||||||
context.pushRoute(SourceRoute(id: source.id));
|
|
||||||
},
|
|
||||||
),
|
|
||||||
),
|
|
||||||
const SizedBox(height: 8),
|
|
||||||
Row(
|
|
||||||
mainAxisAlignment: MainAxisAlignment.center,
|
|
||||||
children: [
|
|
||||||
OutlinedButton.icon(
|
|
||||||
icon: const Icon(Icons.add_rounded),
|
|
||||||
label: Text(l.settingsServersActionsAdd),
|
|
||||||
onPressed: () {
|
|
||||||
context.pushRoute(SourceRoute());
|
|
||||||
},
|
|
||||||
),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
// TODO: remove
|
|
||||||
if (kDebugMode)
|
|
||||||
Row(
|
|
||||||
mainAxisAlignment: MainAxisAlignment.center,
|
|
||||||
children: [
|
|
||||||
OutlinedButton.icon(
|
|
||||||
icon: const Icon(Icons.add_rounded),
|
|
||||||
label: const Text('Add TEST'),
|
|
||||||
onPressed: () {
|
|
||||||
ref
|
|
||||||
.read(settingsServiceProvider.notifier)
|
|
||||||
.addTestSource('TEST');
|
|
||||||
},
|
|
||||||
),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
],
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@ -1,511 +0,0 @@
|
|||||||
import 'dart:async';
|
|
||||||
|
|
||||||
import 'package:auto_route/auto_route.dart';
|
|
||||||
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 'package:sliver_tools/sliver_tools.dart';
|
|
||||||
|
|
||||||
import '../../models/music.dart';
|
|
||||||
import '../../models/query.dart';
|
|
||||||
import '../../models/support.dart';
|
|
||||||
import '../../services/audio_service.dart';
|
|
||||||
import '../../services/cache_service.dart';
|
|
||||||
import '../../state/music.dart';
|
|
||||||
import '../../state/settings.dart';
|
|
||||||
import '../../state/theme.dart';
|
|
||||||
import '../buttons.dart';
|
|
||||||
import '../context_menus.dart';
|
|
||||||
import '../gradient.dart';
|
|
||||||
import '../hooks/use_download_actions.dart';
|
|
||||||
import '../hooks/use_list_query_paging_controller.dart';
|
|
||||||
import '../images.dart';
|
|
||||||
import '../items.dart';
|
|
||||||
import '../lists.dart';
|
|
||||||
|
|
||||||
class AlbumSongsPage extends HookConsumerWidget {
|
|
||||||
final String id;
|
|
||||||
|
|
||||||
const AlbumSongsPage({
|
|
||||||
super.key,
|
|
||||||
@pathParam required this.id,
|
|
||||||
});
|
|
||||||
|
|
||||||
@override
|
|
||||||
Widget build(BuildContext context, WidgetRef ref) {
|
|
||||||
final album = ref.watch(albumProvider(id)).valueOrNull;
|
|
||||||
final audio = ref.watch(audioControlProvider);
|
|
||||||
final colors = ref.watch(albumArtThemeProvider(id)).valueOrNull;
|
|
||||||
final key = useState(GlobalKey());
|
|
||||||
|
|
||||||
if (album == null) {
|
|
||||||
return Container();
|
|
||||||
}
|
|
||||||
|
|
||||||
final query = useMemoized(() => const ListQuery(
|
|
||||||
page: Pagination(limit: 30),
|
|
||||||
sort: SortBy(column: 'disc, track'),
|
|
||||||
));
|
|
||||||
|
|
||||||
final getSongs = useCallback(
|
|
||||||
(ListQuery query) => ref.read(albumSongsListProvider(id, query).future),
|
|
||||||
[id],
|
|
||||||
);
|
|
||||||
|
|
||||||
final play = useCallback(
|
|
||||||
({int? index, bool? shuffle}) => audio.playSongs(
|
|
||||||
query: query,
|
|
||||||
getSongs: getSongs,
|
|
||||||
startIndex: index,
|
|
||||||
context: QueueContextType.album,
|
|
||||||
contextId: id,
|
|
||||||
shuffle: shuffle,
|
|
||||||
),
|
|
||||||
[id, query, getSongs],
|
|
||||||
);
|
|
||||||
|
|
||||||
return QueueContext(
|
|
||||||
id: id,
|
|
||||||
type: QueueContextType.album,
|
|
||||||
child: _SongsPage(
|
|
||||||
query: query,
|
|
||||||
getSongs: getSongs,
|
|
||||||
fab: ShuffleFab(onPressed: () => play(shuffle: true)),
|
|
||||||
onSongTap: (song, index) => play(index: index),
|
|
||||||
background: AlbumArtGradient(key: key.value, id: id),
|
|
||||||
colors: colors,
|
|
||||||
header: _AlbumHeader(
|
|
||||||
album: album,
|
|
||||||
play: () => play(shuffle: false),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
class _AlbumHeader extends HookConsumerWidget {
|
|
||||||
final Album album;
|
|
||||||
final void Function() play;
|
|
||||||
|
|
||||||
const _AlbumHeader({
|
|
||||||
required this.album,
|
|
||||||
required this.play,
|
|
||||||
});
|
|
||||||
|
|
||||||
@override
|
|
||||||
Widget build(BuildContext context, WidgetRef ref) {
|
|
||||||
final cache = ref.watch(cacheServiceProvider);
|
|
||||||
|
|
||||||
final downloadActions = useAlbumDownloadActions(
|
|
||||||
context: context,
|
|
||||||
ref: ref,
|
|
||||||
album: album,
|
|
||||||
);
|
|
||||||
|
|
||||||
final l = AppLocalizations.of(context);
|
|
||||||
|
|
||||||
return _Header(
|
|
||||||
title: album.name,
|
|
||||||
subtitle: album.albumArtist,
|
|
||||||
imageCache: cache.albumArt(album, thumbnail: false),
|
|
||||||
playText: l.resourcesAlbumActionsPlay,
|
|
||||||
onPlay: play,
|
|
||||||
onMore: () => showContextMenu(
|
|
||||||
context: context,
|
|
||||||
ref: ref,
|
|
||||||
builder: (context) => BottomSheetMenu(
|
|
||||||
child: AlbumContextMenu(album: album),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
downloadActions: downloadActions,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
class PlaylistSongsPage extends HookConsumerWidget {
|
|
||||||
final String id;
|
|
||||||
|
|
||||||
const PlaylistSongsPage({
|
|
||||||
super.key,
|
|
||||||
@pathParam required this.id,
|
|
||||||
});
|
|
||||||
|
|
||||||
@override
|
|
||||||
Widget build(BuildContext context, WidgetRef ref) {
|
|
||||||
final playlist = ref.watch(playlistProvider(id)).valueOrNull;
|
|
||||||
final audio = ref.watch(audioControlProvider);
|
|
||||||
final colors = ref.watch(playlistArtThemeProvider(id)).valueOrNull;
|
|
||||||
|
|
||||||
if (playlist == null) {
|
|
||||||
return Container();
|
|
||||||
}
|
|
||||||
|
|
||||||
final query = useMemoized(() => const ListQuery(
|
|
||||||
page: Pagination(limit: 30),
|
|
||||||
sort: SortBy(column: 'playlist_songs.position'),
|
|
||||||
));
|
|
||||||
|
|
||||||
final getSongs = useCallback(
|
|
||||||
(ListQuery query) =>
|
|
||||||
ref.read(playlistSongsListProvider(id, query).future),
|
|
||||||
[id],
|
|
||||||
);
|
|
||||||
|
|
||||||
final play = useCallback(
|
|
||||||
({int? index, bool? shuffle}) => audio.playSongs(
|
|
||||||
query: query,
|
|
||||||
getSongs: getSongs,
|
|
||||||
startIndex: index,
|
|
||||||
context: QueueContextType.playlist,
|
|
||||||
contextId: id,
|
|
||||||
shuffle: shuffle,
|
|
||||||
),
|
|
||||||
[id, query, getSongs],
|
|
||||||
);
|
|
||||||
|
|
||||||
return QueueContext(
|
|
||||||
id: id,
|
|
||||||
type: QueueContextType.playlist,
|
|
||||||
child: _SongsPage(
|
|
||||||
query: query,
|
|
||||||
getSongs: getSongs,
|
|
||||||
fab: ShuffleFab(onPressed: () => play(shuffle: true)),
|
|
||||||
onSongTap: (song, index) => play(index: index),
|
|
||||||
songImage: true,
|
|
||||||
background: PlaylistArtGradient(id: id),
|
|
||||||
colors: colors,
|
|
||||||
header: _PlaylistHeader(
|
|
||||||
playlist: playlist,
|
|
||||||
play: () => play(shuffle: false),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
class _PlaylistHeader extends HookConsumerWidget {
|
|
||||||
final Playlist playlist;
|
|
||||||
final void Function() play;
|
|
||||||
|
|
||||||
const _PlaylistHeader({
|
|
||||||
required this.playlist,
|
|
||||||
required this.play,
|
|
||||||
});
|
|
||||||
|
|
||||||
@override
|
|
||||||
Widget build(BuildContext context, WidgetRef ref) {
|
|
||||||
final cache = ref.watch(cacheServiceProvider);
|
|
||||||
|
|
||||||
final downloadActions = usePlaylistDownloadActions(
|
|
||||||
context: context,
|
|
||||||
ref: ref,
|
|
||||||
playlist: playlist,
|
|
||||||
);
|
|
||||||
|
|
||||||
final l = AppLocalizations.of(context);
|
|
||||||
|
|
||||||
return _Header(
|
|
||||||
title: playlist.name,
|
|
||||||
subtitle: playlist.comment,
|
|
||||||
imageCache: cache.playlistArt(playlist, thumbnail: false),
|
|
||||||
playText: l.resourcesPlaylistActionsPlay,
|
|
||||||
onPlay: play,
|
|
||||||
onMore: () {
|
|
||||||
showContextMenu(
|
|
||||||
context: context,
|
|
||||||
ref: ref,
|
|
||||||
builder: (context) => BottomSheetMenu(
|
|
||||||
size: MenuSize.small,
|
|
||||||
child: PlaylistContextMenu(playlist: playlist),
|
|
||||||
),
|
|
||||||
);
|
|
||||||
},
|
|
||||||
downloadActions: downloadActions,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
class GenreSongsPage extends HookConsumerWidget {
|
|
||||||
final String genre;
|
|
||||||
|
|
||||||
const GenreSongsPage({
|
|
||||||
super.key,
|
|
||||||
@pathParam required this.genre,
|
|
||||||
});
|
|
||||||
|
|
||||||
@override
|
|
||||||
Widget build(BuildContext context, WidgetRef ref) {
|
|
||||||
final query = useMemoized(
|
|
||||||
() => ListQuery(
|
|
||||||
page: const Pagination(limit: 30),
|
|
||||||
sort: const SortBy(
|
|
||||||
column: 'albums.created DESC, albums.name, songs.disc, songs.track',
|
|
||||||
),
|
|
||||||
filters: IList(
|
|
||||||
[FilterWith.equals(column: 'songs.genre', value: genre)],
|
|
||||||
),
|
|
||||||
),
|
|
||||||
[genre],
|
|
||||||
);
|
|
||||||
|
|
||||||
final getSongs = useCallback(
|
|
||||||
(ListQuery query) => ref.read(songsByAlbumListProvider(query).future),
|
|
||||||
[],
|
|
||||||
);
|
|
||||||
|
|
||||||
final play = useCallback(
|
|
||||||
({int? index, bool? shuffle}) => ref.read(audioControlProvider).playRadio(
|
|
||||||
context: QueueContextType.genre,
|
|
||||||
contextId: genre,
|
|
||||||
query: query,
|
|
||||||
getSongs: getSongs,
|
|
||||||
),
|
|
||||||
[query, getSongs],
|
|
||||||
);
|
|
||||||
|
|
||||||
return QueueContext(
|
|
||||||
id: genre,
|
|
||||||
type: QueueContextType.album,
|
|
||||||
child: _SongsPage(
|
|
||||||
query: query,
|
|
||||||
getSongs: getSongs,
|
|
||||||
// onSongTap: (song, index) => play(index: index),
|
|
||||||
songImage: true,
|
|
||||||
background: const BackgroundGradient(),
|
|
||||||
fab: RadioPlayFab(
|
|
||||||
onPressed: () => play(),
|
|
||||||
),
|
|
||||||
header: _GenreHeader(genre: genre),
|
|
||||||
),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
class _GenreHeader extends HookConsumerWidget {
|
|
||||||
final String genre;
|
|
||||||
|
|
||||||
const _GenreHeader({
|
|
||||||
required this.genre,
|
|
||||||
});
|
|
||||||
|
|
||||||
@override
|
|
||||||
Widget build(BuildContext context, WidgetRef ref) {
|
|
||||||
final count = ref.watch(songsByGenreCountProvider(genre)).valueOrNull ?? 0;
|
|
||||||
|
|
||||||
final l = AppLocalizations.of(context);
|
|
||||||
|
|
||||||
return _Header(
|
|
||||||
title: genre,
|
|
||||||
subtitle: l.resourcesSongCount(count),
|
|
||||||
downloadActions: const [],
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
class QueueContext extends InheritedWidget {
|
|
||||||
final QueueContextType type;
|
|
||||||
final String? id;
|
|
||||||
|
|
||||||
const QueueContext({
|
|
||||||
super.key,
|
|
||||||
required this.type,
|
|
||||||
this.id,
|
|
||||||
required super.child,
|
|
||||||
});
|
|
||||||
|
|
||||||
static QueueContext? maybeOf(BuildContext context) {
|
|
||||||
return context.dependOnInheritedWidgetOfExactType<QueueContext>();
|
|
||||||
}
|
|
||||||
|
|
||||||
static QueueContext of(BuildContext context) {
|
|
||||||
final QueueContext? result = maybeOf(context);
|
|
||||||
assert(result != null, 'No QueueContext found in context');
|
|
||||||
return result!;
|
|
||||||
}
|
|
||||||
|
|
||||||
@override
|
|
||||||
bool updateShouldNotify(covariant QueueContext oldWidget) =>
|
|
||||||
oldWidget.id != id || oldWidget.type != type;
|
|
||||||
}
|
|
||||||
|
|
||||||
class _SongsPage extends HookConsumerWidget {
|
|
||||||
final ListQuery query;
|
|
||||||
final FutureOr<List<Song>> Function(ListQuery query) getSongs;
|
|
||||||
final void Function(Song song, int index)? onSongTap;
|
|
||||||
final bool songImage;
|
|
||||||
final Widget background;
|
|
||||||
final Widget fab;
|
|
||||||
final ColorTheme? colors;
|
|
||||||
final Widget header;
|
|
||||||
|
|
||||||
const _SongsPage({
|
|
||||||
required this.query,
|
|
||||||
required this.getSongs,
|
|
||||||
this.onSongTap,
|
|
||||||
this.songImage = false,
|
|
||||||
required this.background,
|
|
||||||
required this.fab,
|
|
||||||
this.colors,
|
|
||||||
required this.header,
|
|
||||||
});
|
|
||||||
|
|
||||||
@override
|
|
||||||
Widget build(BuildContext context, WidgetRef ref) {
|
|
||||||
final base = ref.watch(baseThemeProvider);
|
|
||||||
ref.listen(musicSourceProvider, (previous, next) {
|
|
||||||
if (next.id != previous?.id) {
|
|
||||||
context.router.popUntilRoot();
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
final pagingController = useListQueryPagingController(
|
|
||||||
ref,
|
|
||||||
query: query,
|
|
||||||
getItems: getSongs,
|
|
||||||
);
|
|
||||||
|
|
||||||
final widget = Scaffold(
|
|
||||||
floatingActionButton: fab,
|
|
||||||
body: CustomScrollView(
|
|
||||||
slivers: [
|
|
||||||
SliverStack(
|
|
||||||
children: [
|
|
||||||
SliverPositioned.fill(
|
|
||||||
child: Container(
|
|
||||||
color: base.gradientLow,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
SliverPositioned.directional(
|
|
||||||
textDirection: TextDirection.ltr,
|
|
||||||
start: 0,
|
|
||||||
end: 0,
|
|
||||||
top: 0,
|
|
||||||
child: background,
|
|
||||||
),
|
|
||||||
MultiSliver(
|
|
||||||
children: [
|
|
||||||
SliverSafeArea(
|
|
||||||
sliver: SliverToBoxAdapter(
|
|
||||||
child: Material(
|
|
||||||
type: MaterialType.transparency,
|
|
||||||
child: Padding(
|
|
||||||
padding: const EdgeInsets.all(16),
|
|
||||||
child: header,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
PagedListQueryView(
|
|
||||||
pagingController: pagingController,
|
|
||||||
useSliver: true,
|
|
||||||
itemBuilder: (context, item, index) => SongListTile(
|
|
||||||
song: item,
|
|
||||||
image: songImage,
|
|
||||||
onTap: () =>
|
|
||||||
onSongTap != null ? onSongTap!(item, index) : null,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
);
|
|
||||||
|
|
||||||
if (colors != null) {
|
|
||||||
return Theme(data: colors!.theme, child: widget);
|
|
||||||
} else {
|
|
||||||
return widget;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
class _Header extends HookConsumerWidget {
|
|
||||||
final UriCacheInfo? imageCache;
|
|
||||||
final String title;
|
|
||||||
final String? subtitle;
|
|
||||||
final String? playText;
|
|
||||||
final void Function()? onPlay;
|
|
||||||
final FutureOr<void> Function()? onMore;
|
|
||||||
final List<DownloadAction> downloadActions;
|
|
||||||
|
|
||||||
const _Header({
|
|
||||||
this.imageCache,
|
|
||||||
required this.title,
|
|
||||||
this.subtitle,
|
|
||||||
this.playText,
|
|
||||||
this.onPlay,
|
|
||||||
this.onMore,
|
|
||||||
required this.downloadActions,
|
|
||||||
});
|
|
||||||
|
|
||||||
@override
|
|
||||||
Widget build(BuildContext context, WidgetRef ref) {
|
|
||||||
final inheritedStyle = DefaultTextStyle.of(context).style;
|
|
||||||
final theme = Theme.of(context);
|
|
||||||
|
|
||||||
return Column(
|
|
||||||
crossAxisAlignment: CrossAxisAlignment.center,
|
|
||||||
children: [
|
|
||||||
const SizedBox(height: 10),
|
|
||||||
if (imageCache != null)
|
|
||||||
CardClip(
|
|
||||||
square: false,
|
|
||||||
child: UriCacheInfoImage(
|
|
||||||
height: 300,
|
|
||||||
fit: BoxFit.contain,
|
|
||||||
placeholderStyle: PlaceholderStyle.spinner,
|
|
||||||
cache: imageCache!,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
const SizedBox(height: 16),
|
|
||||||
Column(
|
|
||||||
children: [
|
|
||||||
Text(
|
|
||||||
title,
|
|
||||||
style: theme.textTheme.titleLarge!.copyWith(
|
|
||||||
color: inheritedStyle.color,
|
|
||||||
fontWeight: FontWeight.w500,
|
|
||||||
),
|
|
||||||
textAlign: TextAlign.center,
|
|
||||||
),
|
|
||||||
Text(
|
|
||||||
subtitle ?? '',
|
|
||||||
style: theme.textTheme.titleMedium!.copyWith(
|
|
||||||
color: inheritedStyle.color,
|
|
||||||
),
|
|
||||||
textAlign: TextAlign.center,
|
|
||||||
),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
const SizedBox(height: 16),
|
|
||||||
Row(
|
|
||||||
mainAxisAlignment: MainAxisAlignment.spaceAround,
|
|
||||||
children: [
|
|
||||||
if (downloadActions.isNotEmpty)
|
|
||||||
IconButton(
|
|
||||||
onPressed: downloadActions.first.action,
|
|
||||||
icon: downloadActions.first.type == DownloadActionType.delete
|
|
||||||
? const Icon(Icons.download_done_rounded)
|
|
||||||
: downloadActions.first.iconBuilder(context),
|
|
||||||
),
|
|
||||||
if (onPlay != null)
|
|
||||||
FilledButton.icon(
|
|
||||||
onPressed: onPlay,
|
|
||||||
icon: const Icon(Icons.play_arrow_rounded),
|
|
||||||
label: Text(playText ?? ''),
|
|
||||||
),
|
|
||||||
if (onMore != null)
|
|
||||||
IconButton(
|
|
||||||
onPressed: onMore,
|
|
||||||
icon: const Icon(Icons.more_horiz),
|
|
||||||
)
|
|
||||||
],
|
|
||||||
),
|
|
||||||
],
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@ -1,283 +0,0 @@
|
|||||||
import 'package:auto_route/auto_route.dart';
|
|
||||||
import 'package:collection/collection.dart';
|
|
||||||
import 'package:drift/drift.dart' show Value;
|
|
||||||
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 '../../database/database.dart';
|
|
||||||
import '../../log.dart';
|
|
||||||
import '../../models/settings.dart';
|
|
||||||
import '../../services/settings_service.dart';
|
|
||||||
import '../items.dart';
|
|
||||||
import '../snackbars.dart';
|
|
||||||
|
|
||||||
class SourcePage extends HookConsumerWidget {
|
|
||||||
final int? id;
|
|
||||||
|
|
||||||
const SourcePage({
|
|
||||||
super.key,
|
|
||||||
@pathParam this.id,
|
|
||||||
});
|
|
||||||
|
|
||||||
@override
|
|
||||||
Widget build(BuildContext context, WidgetRef ref) {
|
|
||||||
final source = ref.watch(settingsServiceProvider.select(
|
|
||||||
(value) => value.sources.singleWhereOrNull((e) => e.id == id)
|
|
||||||
as SubsonicSettings?,
|
|
||||||
));
|
|
||||||
final form = useState(GlobalKey<FormState>()).value;
|
|
||||||
final theme = Theme.of(context);
|
|
||||||
final l = AppLocalizations.of(context);
|
|
||||||
final isSaving = useState(false);
|
|
||||||
final isDeleting = useState(false);
|
|
||||||
|
|
||||||
final name = LabeledTextField(
|
|
||||||
label: l.settingsServersFieldsName,
|
|
||||||
initialValue: source?.name,
|
|
||||||
required: true,
|
|
||||||
);
|
|
||||||
final address = LabeledTextField(
|
|
||||||
label: l.settingsServersFieldsAddress,
|
|
||||||
initialValue: source?.address.toString(),
|
|
||||||
keyboardType: TextInputType.url,
|
|
||||||
autofillHints: const [AutofillHints.url],
|
|
||||||
required: true,
|
|
||||||
validator: (value, label) {
|
|
||||||
if (!value!.contains(RegExp(r'https?:\/\/'))) {
|
|
||||||
return '$label must be a valid URL';
|
|
||||||
}
|
|
||||||
return null;
|
|
||||||
},
|
|
||||||
);
|
|
||||||
final username = LabeledTextField(
|
|
||||||
label: l.settingsServersFieldsUsername,
|
|
||||||
initialValue: source?.username,
|
|
||||||
autofillHints: const [AutofillHints.username],
|
|
||||||
required: true,
|
|
||||||
);
|
|
||||||
final password = LabeledTextField(
|
|
||||||
label: l.settingsServersFieldsPassword,
|
|
||||||
initialValue: source?.password,
|
|
||||||
obscureText: true,
|
|
||||||
autofillHints: const [AutofillHints.password],
|
|
||||||
required: true,
|
|
||||||
);
|
|
||||||
|
|
||||||
final forcePlaintextPassword = useState(!(source?.useTokenAuth ?? true));
|
|
||||||
final forcePlaintextSwitch = SwitchListTile(
|
|
||||||
value: forcePlaintextPassword.value,
|
|
||||||
title: Text(l.settingsServersOptionsForcePlaintextPasswordTitle),
|
|
||||||
subtitle: forcePlaintextPassword.value
|
|
||||||
? Text(l.settingsServersOptionsForcePlaintextPasswordDescriptionOn)
|
|
||||||
: Text(l.settingsServersOptionsForcePlaintextPasswordDescriptionOff),
|
|
||||||
onChanged: (value) => forcePlaintextPassword.value = value,
|
|
||||||
);
|
|
||||||
|
|
||||||
return WillPopScope(
|
|
||||||
onWillPop: () async => !isSaving.value && !isDeleting.value,
|
|
||||||
child: Scaffold(
|
|
||||||
appBar: AppBar(),
|
|
||||||
floatingActionButton: Row(
|
|
||||||
mainAxisAlignment: MainAxisAlignment.end,
|
|
||||||
children: [
|
|
||||||
if (source != null && source.isActive != true)
|
|
||||||
FloatingActionButton(
|
|
||||||
backgroundColor: theme.colorScheme.tertiaryContainer,
|
|
||||||
foregroundColor: theme.colorScheme.onTertiaryContainer,
|
|
||||||
onPressed: !isSaving.value && !isDeleting.value
|
|
||||||
? () async {
|
|
||||||
final router = context.router;
|
|
||||||
|
|
||||||
try {
|
|
||||||
isDeleting.value = true;
|
|
||||||
await ref
|
|
||||||
.read(settingsServiceProvider.notifier)
|
|
||||||
.deleteSource(source.id);
|
|
||||||
} finally {
|
|
||||||
isDeleting.value = false;
|
|
||||||
}
|
|
||||||
|
|
||||||
router.pop();
|
|
||||||
}
|
|
||||||
: null,
|
|
||||||
child: isDeleting.value
|
|
||||||
? SizedBox(
|
|
||||||
height: 24,
|
|
||||||
width: 24,
|
|
||||||
child: CircularProgressIndicator(
|
|
||||||
color: theme.colorScheme.onTertiaryContainer,
|
|
||||||
),
|
|
||||||
)
|
|
||||||
: const Icon(Icons.delete_forever_rounded),
|
|
||||||
),
|
|
||||||
const SizedBox(width: 12),
|
|
||||||
FloatingActionButton.extended(
|
|
||||||
heroTag: null,
|
|
||||||
icon: isSaving.value
|
|
||||||
? const SizedBox(
|
|
||||||
height: 24,
|
|
||||||
width: 24,
|
|
||||||
child: CircularProgressIndicator(),
|
|
||||||
)
|
|
||||||
: const Icon(Icons.save_rounded),
|
|
||||||
label: Text(l.settingsServersActionsSave),
|
|
||||||
onPressed: !isSaving.value && !isDeleting.value
|
|
||||||
? () async {
|
|
||||||
final router = context.router;
|
|
||||||
if (!form.currentState!.validate()) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
var error = false;
|
|
||||||
try {
|
|
||||||
isSaving.value = true;
|
|
||||||
if (source != null) {
|
|
||||||
await ref
|
|
||||||
.read(settingsServiceProvider.notifier)
|
|
||||||
.updateSource(
|
|
||||||
source.copyWith(
|
|
||||||
name: name.value,
|
|
||||||
address: Uri.parse(address.value),
|
|
||||||
username: username.value,
|
|
||||||
password: password.value,
|
|
||||||
useTokenAuth: !forcePlaintextPassword.value,
|
|
||||||
),
|
|
||||||
);
|
|
||||||
} else {
|
|
||||||
await ref
|
|
||||||
.read(settingsServiceProvider.notifier)
|
|
||||||
.createSource(
|
|
||||||
SourcesCompanion.insert(
|
|
||||||
name: name.value,
|
|
||||||
address: Uri.parse(address.value),
|
|
||||||
),
|
|
||||||
SubsonicSourcesCompanion.insert(
|
|
||||||
features: IList(),
|
|
||||||
username: username.value,
|
|
||||||
password: password.value,
|
|
||||||
useTokenAuth:
|
|
||||||
Value(!forcePlaintextPassword.value),
|
|
||||||
),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
} catch (e, st) {
|
|
||||||
showErrorSnackbar(context, e.toString());
|
|
||||||
log.severe('Saving source', e, st);
|
|
||||||
error = true;
|
|
||||||
} finally {
|
|
||||||
isSaving.value = false;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!error) {
|
|
||||||
router.pop();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
: null,
|
|
||||||
),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
body: Form(
|
|
||||||
key: form,
|
|
||||||
child: AutofillGroup(
|
|
||||||
child: ListView(
|
|
||||||
children: [
|
|
||||||
const SizedBox(height: 96 - kToolbarHeight),
|
|
||||||
Padding(
|
|
||||||
padding: const EdgeInsets.symmetric(horizontal: 16),
|
|
||||||
child: Text(
|
|
||||||
source == null
|
|
||||||
? l.settingsServersActionsAdd
|
|
||||||
: l.settingsServersActionsEdit,
|
|
||||||
style: theme.textTheme.displaySmall,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
name,
|
|
||||||
address,
|
|
||||||
username,
|
|
||||||
password,
|
|
||||||
const SizedBox(height: 24),
|
|
||||||
forcePlaintextSwitch,
|
|
||||||
const FabPadding(),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
class LabeledTextField extends HookConsumerWidget {
|
|
||||||
final String label;
|
|
||||||
final String? initialValue;
|
|
||||||
final bool obscureText;
|
|
||||||
final bool required;
|
|
||||||
final TextInputType? keyboardType;
|
|
||||||
final Iterable<String>? autofillHints;
|
|
||||||
final String? Function(String? value, String label)? validator;
|
|
||||||
|
|
||||||
// ignore: prefer_const_constructors_in_immutables
|
|
||||||
LabeledTextField({
|
|
||||||
super.key,
|
|
||||||
required this.label,
|
|
||||||
this.initialValue,
|
|
||||||
this.obscureText = false,
|
|
||||||
this.keyboardType,
|
|
||||||
this.validator,
|
|
||||||
this.autofillHints,
|
|
||||||
this.required = false,
|
|
||||||
});
|
|
||||||
|
|
||||||
late final TextEditingController _controller;
|
|
||||||
|
|
||||||
String get value => _controller.text;
|
|
||||||
|
|
||||||
String? _requiredValidator(String? value) {
|
|
||||||
if (value == null || value.isEmpty) {
|
|
||||||
return '$label is required';
|
|
||||||
}
|
|
||||||
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
@override
|
|
||||||
Widget build(BuildContext context, WidgetRef ref) {
|
|
||||||
final theme = Theme.of(context);
|
|
||||||
_controller = useTextEditingController(text: initialValue);
|
|
||||||
|
|
||||||
return Padding(
|
|
||||||
padding: const EdgeInsets.symmetric(horizontal: 16),
|
|
||||||
child: Column(
|
|
||||||
crossAxisAlignment: CrossAxisAlignment.stretch,
|
|
||||||
children: [
|
|
||||||
const SizedBox(height: 24),
|
|
||||||
Text(label, style: theme.textTheme.titleMedium),
|
|
||||||
TextFormField(
|
|
||||||
controller: _controller,
|
|
||||||
obscureText: obscureText,
|
|
||||||
keyboardType: keyboardType,
|
|
||||||
autofillHints: autofillHints,
|
|
||||||
validator: (value) {
|
|
||||||
String? error;
|
|
||||||
|
|
||||||
if (required) {
|
|
||||||
error = _requiredValidator(value);
|
|
||||||
if (error != null) {
|
|
||||||
return error;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (validator != null) {
|
|
||||||
return validator!(value, label);
|
|
||||||
}
|
|
||||||
return null;
|
|
||||||
},
|
|
||||||
),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@ -1,14 +0,0 @@
|
|||||||
import 'package:flutter/material.dart';
|
|
||||||
|
|
||||||
void showErrorSnackbar(BuildContext context, String message) {
|
|
||||||
final colors = Theme.of(context).colorScheme;
|
|
||||||
|
|
||||||
ScaffoldMessenger.of(context).showSnackBar(SnackBar(
|
|
||||||
content: Text(message, style: TextStyle(color: colors.onErrorContainer)),
|
|
||||||
backgroundColor: colors.errorContainer,
|
|
||||||
showCloseIcon: true,
|
|
||||||
closeIconColor: colors.onErrorContainer,
|
|
||||||
behavior: SnackBarBehavior.floating,
|
|
||||||
duration: const Duration(seconds: 10),
|
|
||||||
));
|
|
||||||
}
|
|
||||||
30
lib/cache/image_cache.dart
vendored
@ -1,30 +0,0 @@
|
|||||||
// ignore_for_file: implementation_imports
|
|
||||||
|
|
||||||
import 'package:flutter_cache_manager/flutter_cache_manager.dart';
|
|
||||||
import 'package:flutter_cache_manager/src/storage/file_system/file_system_io.dart';
|
|
||||||
import 'package:http/http.dart';
|
|
||||||
import 'package:riverpod_annotation/riverpod_annotation.dart';
|
|
||||||
|
|
||||||
import '../http/client.dart';
|
|
||||||
|
|
||||||
part 'image_cache.g.dart';
|
|
||||||
|
|
||||||
CacheManager _openImageCache(BaseClient httpClient) {
|
|
||||||
const key = 'images';
|
|
||||||
return CacheManager(
|
|
||||||
Config(
|
|
||||||
key,
|
|
||||||
stalePeriod: const Duration(days: 2147483647),
|
|
||||||
maxNrOfCacheObjects: 2147483647,
|
|
||||||
repo: JsonCacheInfoRepository(databaseName: key),
|
|
||||||
fileSystem: IOFileSystem(key),
|
|
||||||
fileService: HttpFileService(httpClient: httpClient),
|
|
||||||
),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
@Riverpod(keepAlive: true)
|
|
||||||
CacheManager imageCache(ImageCacheRef ref) {
|
|
||||||
final http = ref.watch(httpClientProvider);
|
|
||||||
return _openImageCache(http);
|
|
||||||
}
|
|
||||||
23
lib/cache/image_cache.g.dart
vendored
@ -1,23 +0,0 @@
|
|||||||
// GENERATED CODE - DO NOT MODIFY BY HAND
|
|
||||||
|
|
||||||
part of 'image_cache.dart';
|
|
||||||
|
|
||||||
// **************************************************************************
|
|
||||||
// RiverpodGenerator
|
|
||||||
// **************************************************************************
|
|
||||||
|
|
||||||
String _$imageCacheHash() => r'aaeb74898734c2776f594e05eb82262af20e079f';
|
|
||||||
|
|
||||||
/// See also [imageCache].
|
|
||||||
@ProviderFor(imageCache)
|
|
||||||
final imageCacheProvider = Provider<CacheManager>.internal(
|
|
||||||
imageCache,
|
|
||||||
name: r'imageCacheProvider',
|
|
||||||
debugGetCreateSourceHash:
|
|
||||||
const bool.fromEnvironment('dart.vm.product') ? null : _$imageCacheHash,
|
|
||||||
dependencies: null,
|
|
||||||
allTransitiveDependencies: null,
|
|
||||||
);
|
|
||||||
|
|
||||||
typedef ImageCacheRef = ProviderRef<CacheManager>;
|
|
||||||
// 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,72 +0,0 @@
|
|||||||
import 'dart:convert';
|
|
||||||
|
|
||||||
import 'package:drift/drift.dart';
|
|
||||||
import 'package:fast_immutable_collections/fast_immutable_collections.dart';
|
|
||||||
|
|
||||||
import '../models/query.dart';
|
|
||||||
import '../models/settings.dart';
|
|
||||||
|
|
||||||
class DurationSecondsConverter extends TypeConverter<Duration, int> {
|
|
||||||
const DurationSecondsConverter();
|
|
||||||
|
|
||||||
@override
|
|
||||||
Duration fromSql(int fromDb) => Duration(seconds: fromDb);
|
|
||||||
|
|
||||||
@override
|
|
||||||
int toSql(Duration value) => value.inSeconds;
|
|
||||||
}
|
|
||||||
|
|
||||||
class UriConverter extends TypeConverter<Uri, String> {
|
|
||||||
const UriConverter();
|
|
||||||
|
|
||||||
@override
|
|
||||||
Uri fromSql(String fromDb) => Uri.parse(fromDb);
|
|
||||||
|
|
||||||
@override
|
|
||||||
String toSql(Uri value) => value.toString();
|
|
||||||
}
|
|
||||||
|
|
||||||
class ListQueryConverter extends TypeConverter<ListQuery, String> {
|
|
||||||
const ListQueryConverter();
|
|
||||||
|
|
||||||
@override
|
|
||||||
ListQuery fromSql(String fromDb) => ListQuery.fromJson(jsonDecode(fromDb));
|
|
||||||
|
|
||||||
@override
|
|
||||||
String toSql(ListQuery value) => jsonEncode(value.toJson());
|
|
||||||
}
|
|
||||||
|
|
||||||
class SubsonicFeatureListConverter
|
|
||||||
extends TypeConverter<IList<SubsonicFeature>, String> {
|
|
||||||
const SubsonicFeatureListConverter();
|
|
||||||
|
|
||||||
@override
|
|
||||||
IList<SubsonicFeature> fromSql(String fromDb) {
|
|
||||||
return IList<SubsonicFeature>.fromJson(
|
|
||||||
jsonDecode(fromDb),
|
|
||||||
(item) => SubsonicFeature.values.byName(item as String),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
@override
|
|
||||||
String toSql(IList<SubsonicFeature> value) {
|
|
||||||
return jsonEncode(value.toJson((e) => e.toString()));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
class IListIntConverter extends TypeConverter<IList<int>, String> {
|
|
||||||
const IListIntConverter();
|
|
||||||
|
|
||||||
@override
|
|
||||||
IList<int> fromSql(String fromDb) {
|
|
||||||
return IList<int>.fromJson(
|
|
||||||
jsonDecode(fromDb),
|
|
||||||
(item) => int.parse(item as String),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
@override
|
|
||||||
String toSql(IList<int> value) {
|
|
||||||
return jsonEncode(value.toJson((e) => jsonEncode(e)));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@ -1,681 +0,0 @@
|
|||||||
import 'dart:io';
|
|
||||||
|
|
||||||
import 'package:collection/collection.dart';
|
|
||||||
import 'package:drift/drift.dart';
|
|
||||||
import 'package:drift/isolate.dart';
|
|
||||||
import 'package:drift/native.dart';
|
|
||||||
import 'package:fast_immutable_collections/fast_immutable_collections.dart';
|
|
||||||
import 'package:path/path.dart' as p;
|
|
||||||
import 'package:path_provider/path_provider.dart';
|
|
||||||
import 'package:riverpod_annotation/riverpod_annotation.dart';
|
|
||||||
|
|
||||||
import '../log.dart';
|
|
||||||
import '../models/music.dart';
|
|
||||||
import '../models/query.dart';
|
|
||||||
import '../models/settings.dart';
|
|
||||||
import '../models/support.dart';
|
|
||||||
import 'converters.dart';
|
|
||||||
import 'error_logging_database.dart';
|
|
||||||
|
|
||||||
part 'database.g.dart';
|
|
||||||
|
|
||||||
// don't exceed SQLITE_MAX_VARIABLE_NUMBER (32766 for version >= 3.32.0)
|
|
||||||
// https://www.sqlite.org/limits.html
|
|
||||||
const kSqliteMaxVariableNumber = 32766;
|
|
||||||
|
|
||||||
@DriftDatabase(include: {'tables.drift'})
|
|
||||||
class SubtracksDatabase extends _$SubtracksDatabase {
|
|
||||||
SubtracksDatabase() : super(_openConnection());
|
|
||||||
SubtracksDatabase.connection(QueryExecutor e) : super(e);
|
|
||||||
|
|
||||||
@override
|
|
||||||
int get schemaVersion => 1;
|
|
||||||
|
|
||||||
@override
|
|
||||||
MigrationStrategy get migration {
|
|
||||||
return MigrationStrategy(
|
|
||||||
beforeOpen: (details) async {
|
|
||||||
await customStatement('PRAGMA foreign_keys = ON');
|
|
||||||
},
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Runs a database opertion in a background isolate.
|
|
||||||
///
|
|
||||||
/// **Only pass top-level functions to [computation]!**
|
|
||||||
///
|
|
||||||
/// **Do not use non-serializable data inside [computation]!**
|
|
||||||
Future<Ret> background<Ret>(
|
|
||||||
FutureOr<Ret> Function(SubtracksDatabase) computation,
|
|
||||||
) async {
|
|
||||||
return computeWithDatabase(
|
|
||||||
connect: SubtracksDatabase.connection,
|
|
||||||
computation: computation,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
MultiSelectable<Album> albumsList(int sourceId, ListQuery opt) {
|
|
||||||
return filterAlbums(
|
|
||||||
(_) => _filterPredicate('albums', sourceId, opt),
|
|
||||||
(_) => _filterOrderBy(opt),
|
|
||||||
(_) => _filterLimit(opt),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
MultiSelectable<Album> albumsListDownloaded(int sourceId, ListQuery opt) {
|
|
||||||
return filterAlbumsDownloaded(
|
|
||||||
(_, __) => _filterPredicate('albums', sourceId, opt),
|
|
||||||
(_, __) => _filterOrderBy(opt),
|
|
||||||
(_, __) => _filterLimit(opt),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
MultiSelectable<Artist> artistsList(int sourceId, ListQuery opt) {
|
|
||||||
return filterArtists(
|
|
||||||
(_) => _filterPredicate('artists', sourceId, opt),
|
|
||||||
(_) => _filterOrderBy(opt),
|
|
||||||
(_) => _filterLimit(opt),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
MultiSelectable<Artist> artistsListDownloaded(int sourceId, ListQuery opt) {
|
|
||||||
return filterArtistsDownloaded(
|
|
||||||
(_, __, ___) => _filterPredicate('artists', sourceId, opt),
|
|
||||||
(_, __, ___) => _filterOrderBy(opt),
|
|
||||||
(_, __, ___) => _filterLimit(opt),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
MultiSelectable<Playlist> playlistsList(int sourceId, ListQuery opt) {
|
|
||||||
return filterPlaylists(
|
|
||||||
(_) => _filterPredicate('playlists', sourceId, opt),
|
|
||||||
(_) => _filterOrderBy(opt),
|
|
||||||
(_) => _filterLimit(opt),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
MultiSelectable<Playlist> playlistsListDownloaded(
|
|
||||||
int sourceId, ListQuery opt) {
|
|
||||||
return filterPlaylistsDownloaded(
|
|
||||||
(_, __, ___) => _filterPredicate('playlists', sourceId, opt),
|
|
||||||
(_, __, ___) => _filterOrderBy(opt),
|
|
||||||
(_, __, ___) => _filterLimit(opt),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
MultiSelectable<Song> songsList(int sourceId, ListQuery opt) {
|
|
||||||
return filterSongs(
|
|
||||||
(_) => _filterPredicate('songs', sourceId, opt),
|
|
||||||
(_) => _filterOrderBy(opt),
|
|
||||||
(_) => _filterLimit(opt),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
MultiSelectable<Song> songsListDownloaded(int sourceId, ListQuery opt) {
|
|
||||||
return filterSongsDownloaded(
|
|
||||||
(_) => _filterPredicate('songs', sourceId, opt),
|
|
||||||
(_) => _filterOrderBy(opt),
|
|
||||||
(_) => _filterLimit(opt),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
Expression<bool> _filterPredicate(String table, int sourceId, ListQuery opt) {
|
|
||||||
return opt.filters.map((filter) => buildFilter<bool>(filter)).fold(
|
|
||||||
CustomExpression('$table.source_id = $sourceId'),
|
|
||||||
(previousValue, element) => previousValue & element,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
OrderBy _filterOrderBy(ListQuery opt) {
|
|
||||||
return opt.sort != null
|
|
||||||
? OrderBy([_buildOrder(opt.sort!)])
|
|
||||||
: const OrderBy.nothing();
|
|
||||||
}
|
|
||||||
|
|
||||||
Limit _filterLimit(ListQuery opt) {
|
|
||||||
return Limit(opt.page.limit, opt.page.offset);
|
|
||||||
}
|
|
||||||
|
|
||||||
MultiSelectable<Song> albumSongsList(SourceId sid, ListQuery opt) {
|
|
||||||
return listQuery(
|
|
||||||
select(songs)
|
|
||||||
..where((tbl) =>
|
|
||||||
tbl.sourceId.equals(sid.sourceId) & tbl.albumId.equals(sid.id)),
|
|
||||||
opt,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
MultiSelectable<Song> songsByAlbumList(int sourceId, ListQuery opt) {
|
|
||||||
return filterSongsByGenre(
|
|
||||||
(_, __) => _filterPredicate('songs', sourceId, opt),
|
|
||||||
(_, __) => _filterOrderBy(opt),
|
|
||||||
(_, __) => _filterLimit(opt),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
MultiSelectable<Song> playlistSongsList(SourceId sid, ListQuery opt) {
|
|
||||||
return listQueryJoined(
|
|
||||||
select(songs).join([
|
|
||||||
innerJoin(
|
|
||||||
playlistSongs,
|
|
||||||
playlistSongs.sourceId.equalsExp(songs.sourceId) &
|
|
||||||
playlistSongs.songId.equalsExp(songs.id),
|
|
||||||
useColumns: false,
|
|
||||||
),
|
|
||||||
])
|
|
||||||
..where(playlistSongs.sourceId.equals(sid.sourceId) &
|
|
||||||
playlistSongs.playlistId.equals(sid.id)),
|
|
||||||
opt,
|
|
||||||
).map((row) => row.readTable(songs));
|
|
||||||
}
|
|
||||||
|
|
||||||
Future<void> saveArtists(Iterable<ArtistsCompanion> artists) async {
|
|
||||||
await batch((batch) {
|
|
||||||
batch.insertAllOnConflictUpdate(this.artists, artists);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
Future<void> deleteArtistsNotIn(int sourceId, Set<String> ids) {
|
|
||||||
return transaction(() async {
|
|
||||||
final allIds = (await (selectOnly(artists)
|
|
||||||
..addColumns([artists.id])
|
|
||||||
..where(artists.sourceId.equals(sourceId)))
|
|
||||||
.map((row) => row.read(artists.id))
|
|
||||||
.get())
|
|
||||||
.whereNotNull()
|
|
||||||
.toSet();
|
|
||||||
final downloadIds = (await artistIdsWithDownloadStatus(sourceId).get())
|
|
||||||
.whereNotNull()
|
|
||||||
.toSet();
|
|
||||||
|
|
||||||
final diff = allIds.difference(downloadIds).difference(ids);
|
|
||||||
for (var slice in diff.slices(kSqliteMaxVariableNumber)) {
|
|
||||||
await (delete(artists)
|
|
||||||
..where(
|
|
||||||
(tbl) => tbl.sourceId.equals(sourceId) & tbl.id.isIn(slice)))
|
|
||||||
.go();
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
Future<void> saveAlbums(Iterable<AlbumsCompanion> albums) async {
|
|
||||||
await batch((batch) {
|
|
||||||
batch.insertAllOnConflictUpdate(this.albums, albums);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
Future<void> deleteAlbumsNotIn(int sourceId, Set<String> ids) {
|
|
||||||
return transaction(() async {
|
|
||||||
final allIds = (await (selectOnly(albums)
|
|
||||||
..addColumns([albums.id])
|
|
||||||
..where(albums.sourceId.equals(sourceId)))
|
|
||||||
.map((row) => row.read(albums.id))
|
|
||||||
.get())
|
|
||||||
.whereNotNull()
|
|
||||||
.toSet();
|
|
||||||
final downloadIds = (await albumIdsWithDownloadStatus(sourceId).get())
|
|
||||||
.whereNotNull()
|
|
||||||
.toSet();
|
|
||||||
|
|
||||||
final diff = allIds.difference(downloadIds).difference(ids);
|
|
||||||
for (var slice in diff.slices(kSqliteMaxVariableNumber)) {
|
|
||||||
await (delete(albums)
|
|
||||||
..where(
|
|
||||||
(tbl) => tbl.sourceId.equals(sourceId) & tbl.id.isIn(slice)))
|
|
||||||
.go();
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
Future<void> savePlaylists(
|
|
||||||
Iterable<PlaylistWithSongsCompanion> playlistsWithSongs,
|
|
||||||
) async {
|
|
||||||
final playlists = playlistsWithSongs.map((e) => e.playist);
|
|
||||||
final playlistSongs = playlistsWithSongs.expand((e) => e.songs);
|
|
||||||
final sourceId = playlists.first.sourceId.value;
|
|
||||||
|
|
||||||
await (delete(this.playlistSongs)
|
|
||||||
..where(
|
|
||||||
(tbl) =>
|
|
||||||
tbl.sourceId.equals(sourceId) &
|
|
||||||
tbl.playlistId.isIn(playlists.map((e) => e.id.value)),
|
|
||||||
))
|
|
||||||
.go();
|
|
||||||
|
|
||||||
await batch((batch) {
|
|
||||||
batch.insertAllOnConflictUpdate(this.playlists, playlists);
|
|
||||||
batch.insertAllOnConflictUpdate(this.playlistSongs, playlistSongs);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
Future<void> deletePlaylistsNotIn(int sourceId, Set<String> ids) {
|
|
||||||
return transaction(() async {
|
|
||||||
final allIds = (await (selectOnly(playlists)
|
|
||||||
..addColumns([playlists.id])
|
|
||||||
..where(playlists.sourceId.equals(sourceId)))
|
|
||||||
.map((row) => row.read(playlists.id))
|
|
||||||
.get())
|
|
||||||
.whereNotNull()
|
|
||||||
.toSet();
|
|
||||||
final downloadIds = (await playlistIdsWithDownloadStatus(sourceId).get())
|
|
||||||
.whereNotNull()
|
|
||||||
.toSet();
|
|
||||||
|
|
||||||
final diff = allIds.difference(downloadIds).difference(ids);
|
|
||||||
for (var slice in diff.slices(kSqliteMaxVariableNumber)) {
|
|
||||||
await (delete(playlists)
|
|
||||||
..where(
|
|
||||||
(tbl) => tbl.sourceId.equals(sourceId) & tbl.id.isIn(slice)))
|
|
||||||
.go();
|
|
||||||
await (delete(playlistSongs)
|
|
||||||
..where((tbl) =>
|
|
||||||
tbl.sourceId.equals(sourceId) & tbl.playlistId.isIn(slice)))
|
|
||||||
.go();
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
Future<void> savePlaylistSongs(
|
|
||||||
int sourceId,
|
|
||||||
List<String> ids,
|
|
||||||
Iterable<PlaylistSongsCompanion> playlistSongs,
|
|
||||||
) async {
|
|
||||||
await (delete(this.playlistSongs)
|
|
||||||
..where(
|
|
||||||
(tbl) => tbl.sourceId.equals(sourceId) & tbl.playlistId.isIn(ids),
|
|
||||||
))
|
|
||||||
.go();
|
|
||||||
await batch((batch) {
|
|
||||||
batch.insertAllOnConflictUpdate(this.playlistSongs, playlistSongs);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
Future<void> saveSongs(Iterable<SongsCompanion> songs) async {
|
|
||||||
await batch((batch) {
|
|
||||||
batch.insertAllOnConflictUpdate(this.songs, songs);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
Future<void> deleteSongsNotIn(int sourceId, Set<String> ids) {
|
|
||||||
return transaction(() async {
|
|
||||||
final allIds = (await (selectOnly(songs)
|
|
||||||
..addColumns([songs.id])
|
|
||||||
..where(
|
|
||||||
songs.sourceId.equals(sourceId) &
|
|
||||||
songs.downloadFilePath.isNull() &
|
|
||||||
songs.downloadTaskId.isNull(),
|
|
||||||
))
|
|
||||||
.map((row) => row.read(songs.id))
|
|
||||||
.get())
|
|
||||||
.whereNotNull()
|
|
||||||
.toSet();
|
|
||||||
|
|
||||||
final diff = allIds.difference(ids);
|
|
||||||
for (var slice in diff.slices(kSqliteMaxVariableNumber)) {
|
|
||||||
await (delete(songs)
|
|
||||||
..where(
|
|
||||||
(tbl) => tbl.sourceId.equals(sourceId) & tbl.id.isIn(slice)))
|
|
||||||
.go();
|
|
||||||
await (delete(playlistSongs)
|
|
||||||
..where(
|
|
||||||
(tbl) => tbl.sourceId.equals(sourceId) & tbl.songId.isIn(slice),
|
|
||||||
))
|
|
||||||
.go();
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
Selectable<LastBottomNavStateData> getLastBottomNavState() {
|
|
||||||
return select(lastBottomNavState)..where((tbl) => tbl.id.equals(1));
|
|
||||||
}
|
|
||||||
|
|
||||||
Future<void> saveLastBottomNavState(LastBottomNavStateData update) {
|
|
||||||
return into(lastBottomNavState).insertOnConflictUpdate(update);
|
|
||||||
}
|
|
||||||
|
|
||||||
Selectable<LastLibraryStateData> getLastLibraryState() {
|
|
||||||
return select(lastLibraryState)..where((tbl) => tbl.id.equals(1));
|
|
||||||
}
|
|
||||||
|
|
||||||
Future<void> saveLastLibraryState(LastLibraryStateData update) {
|
|
||||||
return into(lastLibraryState).insertOnConflictUpdate(update);
|
|
||||||
}
|
|
||||||
|
|
||||||
Selectable<LastAudioStateData> getLastAudioState() {
|
|
||||||
return select(lastAudioState)..where((tbl) => tbl.id.equals(1));
|
|
||||||
}
|
|
||||||
|
|
||||||
Future<void> saveLastAudioState(LastAudioStateCompanion update) {
|
|
||||||
return into(lastAudioState).insertOnConflictUpdate(update);
|
|
||||||
}
|
|
||||||
|
|
||||||
Future<void> insertQueue(Iterable<QueueCompanion> songs) async {
|
|
||||||
await batch((batch) {
|
|
||||||
batch.insertAll(queue, songs);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
Future<void> clearQueue() async {
|
|
||||||
await delete(queue).go();
|
|
||||||
}
|
|
||||||
|
|
||||||
Future<void> setCurrentTrack(int index) async {
|
|
||||||
await transaction(() async {
|
|
||||||
await (update(queue)..where((tbl) => tbl.index.equals(index).not()))
|
|
||||||
.write(const QueueCompanion(currentTrack: Value(null)));
|
|
||||||
await (update(queue)..where((tbl) => tbl.index.equals(index)))
|
|
||||||
.write(const QueueCompanion(currentTrack: Value(true)));
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
Future<void> createSource(
|
|
||||||
SourcesCompanion source,
|
|
||||||
SubsonicSourcesCompanion subsonic,
|
|
||||||
) async {
|
|
||||||
await transaction(() async {
|
|
||||||
final count = await sourcesCount().getSingle();
|
|
||||||
if (count == 0) {
|
|
||||||
source = source.copyWith(isActive: const Value(true));
|
|
||||||
}
|
|
||||||
|
|
||||||
final id = await into(sources).insert(source);
|
|
||||||
subsonic = subsonic.copyWith(sourceId: Value(id));
|
|
||||||
await into(subsonicSources).insert(subsonic);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
Future<void> updateSource(SubsonicSettings source) async {
|
|
||||||
await transaction(() async {
|
|
||||||
await into(sources).insertOnConflictUpdate(source.toSourceInsertable());
|
|
||||||
await into(subsonicSources)
|
|
||||||
.insertOnConflictUpdate(source.toSubsonicInsertable());
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
Future<void> deleteSource(int sourceId) async {
|
|
||||||
await transaction(() async {
|
|
||||||
await (delete(subsonicSources)
|
|
||||||
..where((tbl) => tbl.sourceId.equals(sourceId)))
|
|
||||||
.go();
|
|
||||||
await (delete(sources)..where((tbl) => tbl.id.equals(sourceId))).go();
|
|
||||||
|
|
||||||
await (delete(songs)..where((tbl) => tbl.sourceId.equals(sourceId))).go();
|
|
||||||
await (delete(albums)..where((tbl) => tbl.sourceId.equals(sourceId)))
|
|
||||||
.go();
|
|
||||||
await (delete(artists)..where((tbl) => tbl.sourceId.equals(sourceId)))
|
|
||||||
.go();
|
|
||||||
await (delete(playlistSongs)
|
|
||||||
..where((tbl) => tbl.sourceId.equals(sourceId)))
|
|
||||||
.go();
|
|
||||||
await (delete(playlists)..where((tbl) => tbl.sourceId.equals(sourceId)))
|
|
||||||
.go();
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
Future<void> setActiveSource(int id) async {
|
|
||||||
await batch((batch) {
|
|
||||||
batch.update(
|
|
||||||
sources,
|
|
||||||
const SourcesCompanion(isActive: Value(null)),
|
|
||||||
where: (t) => t.id.isNotValue(id),
|
|
||||||
);
|
|
||||||
batch.update(
|
|
||||||
sources,
|
|
||||||
const SourcesCompanion(isActive: Value(true)),
|
|
||||||
where: (t) => t.id.equals(id),
|
|
||||||
);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
Future<void> updateSettings(AppSettingsCompanion settings) async {
|
|
||||||
await into(appSettings).insertOnConflictUpdate(settings);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
LazyDatabase _openConnection() {
|
|
||||||
return LazyDatabase(() async {
|
|
||||||
final dbFolder = await getApplicationDocumentsDirectory();
|
|
||||||
final file = File(p.join(dbFolder.path, 'subtracks.sqlite'));
|
|
||||||
// return NativeDatabase.createInBackground(file, logStatements: true);
|
|
||||||
|
|
||||||
return ErrorLoggingDatabase(
|
|
||||||
NativeDatabase.createInBackground(file),
|
|
||||||
(e, s) => log.severe('SQL error', e, s),
|
|
||||||
);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
@Riverpod(keepAlive: true)
|
|
||||||
SubtracksDatabase database(DatabaseRef ref) {
|
|
||||||
return SubtracksDatabase();
|
|
||||||
}
|
|
||||||
|
|
||||||
OrderingTerm _buildOrder(SortBy sort) {
|
|
||||||
OrderingMode? mode =
|
|
||||||
sort.dir == SortDirection.asc ? OrderingMode.asc : OrderingMode.desc;
|
|
||||||
return OrderingTerm(
|
|
||||||
expression: CustomExpression(sort.column),
|
|
||||||
mode: mode,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
SimpleSelectStatement<T, R> listQuery<T extends HasResultSet, R>(
|
|
||||||
SimpleSelectStatement<T, R> query,
|
|
||||||
ListQuery opt,
|
|
||||||
) {
|
|
||||||
if (opt.page.limit > 0) {
|
|
||||||
query.limit(opt.page.limit, offset: opt.page.offset);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (opt.sort != null) {
|
|
||||||
OrderingMode? mode = opt.sort != null && opt.sort!.dir == SortDirection.asc
|
|
||||||
? OrderingMode.asc
|
|
||||||
: OrderingMode.desc;
|
|
||||||
query.orderBy([
|
|
||||||
(t) => OrderingTerm(
|
|
||||||
expression: CustomExpression(opt.sort!.column),
|
|
||||||
mode: mode,
|
|
||||||
)
|
|
||||||
]);
|
|
||||||
}
|
|
||||||
|
|
||||||
for (var filter in opt.filters) {
|
|
||||||
query.where((tbl) => buildFilter(filter));
|
|
||||||
}
|
|
||||||
|
|
||||||
return query;
|
|
||||||
}
|
|
||||||
|
|
||||||
JoinedSelectStatement<T, R> listQueryJoined<T extends HasResultSet, R>(
|
|
||||||
JoinedSelectStatement<T, R> query,
|
|
||||||
ListQuery opt,
|
|
||||||
) {
|
|
||||||
if (opt.page.limit > 0) {
|
|
||||||
query.limit(opt.page.limit, offset: opt.page.offset);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (opt.sort != null) {
|
|
||||||
OrderingMode? mode = opt.sort != null && opt.sort!.dir == SortDirection.asc
|
|
||||||
? OrderingMode.asc
|
|
||||||
: OrderingMode.desc;
|
|
||||||
query.orderBy([
|
|
||||||
OrderingTerm(
|
|
||||||
expression: CustomExpression(opt.sort!.column),
|
|
||||||
mode: mode,
|
|
||||||
)
|
|
||||||
]);
|
|
||||||
}
|
|
||||||
|
|
||||||
for (var filter in opt.filters) {
|
|
||||||
query.where(buildFilter(filter));
|
|
||||||
}
|
|
||||||
|
|
||||||
return query;
|
|
||||||
}
|
|
||||||
|
|
||||||
CustomExpression<T> buildFilter<T extends Object>(
|
|
||||||
FilterWith filter,
|
|
||||||
) {
|
|
||||||
return filter.when(
|
|
||||||
equals: (column, value, invert) => CustomExpression<T>(
|
|
||||||
'$column ${invert ? '<>' : '='} \'$value\'',
|
|
||||||
),
|
|
||||||
greaterThan: (column, value, orEquals) => CustomExpression<T>(
|
|
||||||
'$column ${orEquals ? '>=' : '>'} $value',
|
|
||||||
),
|
|
||||||
isNull: (column, invert) => CustomExpression<T>(
|
|
||||||
'$column ${invert ? 'IS NOT' : 'IS'} NULL',
|
|
||||||
),
|
|
||||||
betweenInt: (column, from, to) => CustomExpression<T>(
|
|
||||||
'$column BETWEEN $from AND $to',
|
|
||||||
),
|
|
||||||
isIn: (column, invert, values) => CustomExpression<T>(
|
|
||||||
'$column ${invert ? 'NOT IN' : 'IN'} (${values.join(',')})',
|
|
||||||
),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
class AlbumSongsCompanion {
|
|
||||||
final AlbumsCompanion album;
|
|
||||||
final Iterable<SongsCompanion> songs;
|
|
||||||
|
|
||||||
AlbumSongsCompanion(this.album, this.songs);
|
|
||||||
}
|
|
||||||
|
|
||||||
class ArtistAlbumsCompanion {
|
|
||||||
final ArtistsCompanion artist;
|
|
||||||
final Iterable<AlbumsCompanion> albums;
|
|
||||||
|
|
||||||
ArtistAlbumsCompanion(this.artist, this.albums);
|
|
||||||
}
|
|
||||||
|
|
||||||
class PlaylistWithSongsCompanion {
|
|
||||||
final PlaylistsCompanion playist;
|
|
||||||
final Iterable<PlaylistSongsCompanion> songs;
|
|
||||||
|
|
||||||
PlaylistWithSongsCompanion(this.playist, this.songs);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Future<void> saveArtist(
|
|
||||||
// SubtracksDatabase db,
|
|
||||||
// ArtistAlbumsCompanion artistAlbums,
|
|
||||||
// ) async {
|
|
||||||
// return db.background((db) async {
|
|
||||||
// final artist = artistAlbums.artist;
|
|
||||||
// final albums = artistAlbums.albums;
|
|
||||||
|
|
||||||
// await db.batch((batch) {
|
|
||||||
// batch.insertAllOnConflictUpdate(db.artists, [artist]);
|
|
||||||
// batch.insertAllOnConflictUpdate(db.albums, albums);
|
|
||||||
|
|
||||||
// // remove this artistId from albums not found in source
|
|
||||||
// // don't delete them since they coud have been moved to another artist
|
|
||||||
// // that we haven't synced yet
|
|
||||||
// final albumIds = {for (var a in albums) a.id.value};
|
|
||||||
// batch.update(
|
|
||||||
// db.albums,
|
|
||||||
// const AlbumsCompanion(artistId: Value(null)),
|
|
||||||
// where: (tbl) =>
|
|
||||||
// tbl.sourceId.equals(artist.sourceId.value) &
|
|
||||||
// tbl.artistId.equals(artist.id.value) &
|
|
||||||
// tbl.id.isNotIn(albumIds),
|
|
||||||
// );
|
|
||||||
// });
|
|
||||||
// });
|
|
||||||
// }
|
|
||||||
|
|
||||||
// Future<void> saveAlbum(
|
|
||||||
// SubtracksDatabase db,
|
|
||||||
// AlbumSongsCompanion albumSongs,
|
|
||||||
// ) async {
|
|
||||||
// return db.background((db) async {
|
|
||||||
// final album = albumSongs.album.copyWith(synced: Value(DateTime.now()));
|
|
||||||
// final songs = albumSongs.songs;
|
|
||||||
|
|
||||||
// final songIds = {for (var a in songs) a.id.value};
|
|
||||||
// final hardDeletedSongIds = (await (db.selectOnly(db.songs)
|
|
||||||
// ..addColumns([db.songs.id])
|
|
||||||
// ..where(
|
|
||||||
// db.songs.sourceId.equals(album.sourceId.value) &
|
|
||||||
// db.songs.albumId.equals(album.id.value) &
|
|
||||||
// db.songs.id.isNotIn(songIds) &
|
|
||||||
// db.songs.downloadFilePath.isNull() &
|
|
||||||
// db.songs.downloadTaskId.isNull(),
|
|
||||||
// ))
|
|
||||||
// .map((row) => row.read(db.songs.id))
|
|
||||||
// .get())
|
|
||||||
// .whereNotNull();
|
|
||||||
|
|
||||||
// await db.batch((batch) {
|
|
||||||
// batch.insertAllOnConflictUpdate(db.albums, [album]);
|
|
||||||
// batch.insertAllOnConflictUpdate(db.songs, songs);
|
|
||||||
|
|
||||||
// // soft delete songs that have been downloaded so that the user
|
|
||||||
// // can decide to keep or remove them later
|
|
||||||
// // TODO: add a setting to skip soft delete and just remove download too
|
|
||||||
// batch.update(
|
|
||||||
// db.songs,
|
|
||||||
// const SongsCompanion(isDeleted: Value(true)),
|
|
||||||
// where: (tbl) =>
|
|
||||||
// tbl.sourceId.equals(album.sourceId.value) &
|
|
||||||
// tbl.albumId.equals(album.id.value) &
|
|
||||||
// tbl.id.isNotIn(songIds) &
|
|
||||||
// (tbl.downloadFilePath.isNotNull() | tbl.downloadTaskId.isNotNull()),
|
|
||||||
// );
|
|
||||||
|
|
||||||
// // safe to hard delete songs that have not been downloaded
|
|
||||||
// batch.deleteWhere(
|
|
||||||
// db.songs,
|
|
||||||
// (tbl) =>
|
|
||||||
// tbl.sourceId.equals(album.sourceId.value) &
|
|
||||||
// tbl.id.isIn(hardDeletedSongIds),
|
|
||||||
// );
|
|
||||||
|
|
||||||
// // also need to remove these songs from any playlists that contain them
|
|
||||||
// batch.deleteWhere(
|
|
||||||
// db.playlistSongs,
|
|
||||||
// (tbl) =>
|
|
||||||
// tbl.sourceId.equals(album.sourceId.value) &
|
|
||||||
// tbl.songId.isIn(hardDeletedSongIds),
|
|
||||||
// );
|
|
||||||
// });
|
|
||||||
// });
|
|
||||||
// }
|
|
||||||
|
|
||||||
// Future<void> savePlaylist(
|
|
||||||
// SubtracksDatabase db,
|
|
||||||
// PlaylistWithSongsCompanion playlistWithSongs,
|
|
||||||
// ) async {
|
|
||||||
// return db.background((db) async {
|
|
||||||
// final playlist =
|
|
||||||
// playlistWithSongs.playist.copyWith(synced: Value(DateTime.now()));
|
|
||||||
// final songs = playlistWithSongs.songs;
|
|
||||||
|
|
||||||
// await db.batch((batch) {
|
|
||||||
// batch.insertAllOnConflictUpdate(db.playlists, [playlist]);
|
|
||||||
// batch.insertAllOnConflictUpdate(db.songs, songs);
|
|
||||||
|
|
||||||
// batch.insertAllOnConflictUpdate(
|
|
||||||
// db.playlistSongs,
|
|
||||||
// songs.mapIndexed(
|
|
||||||
// (index, song) => PlaylistSongsCompanion.insert(
|
|
||||||
// sourceId: playlist.sourceId.value,
|
|
||||||
// playlistId: playlist.id.value,
|
|
||||||
// songId: song.id.value,
|
|
||||||
// position: index,
|
|
||||||
// ),
|
|
||||||
// ),
|
|
||||||
// );
|
|
||||||
|
|
||||||
// // the new playlist could be shorter than the old one, so we delete
|
|
||||||
// // playlist songs above our new playlist's length
|
|
||||||
// batch.deleteWhere(
|
|
||||||
// db.playlistSongs,
|
|
||||||
// (tbl) =>
|
|
||||||
// tbl.sourceId.equals(playlist.sourceId.value) &
|
|
||||||
// tbl.playlistId.equals(playlist.id.value) &
|
|
||||||
// tbl.position.isBiggerOrEqualValue(songs.length),
|
|
||||||
// );
|
|
||||||
// });
|
|
||||||
// });
|
|
||||||
// }
|
|
||||||
@ -1,94 +0,0 @@
|
|||||||
import 'dart:async';
|
|
||||||
|
|
||||||
import 'package:drift/drift.dart';
|
|
||||||
import 'package:drift/isolate.dart';
|
|
||||||
|
|
||||||
/// https://github.com/simolus3/drift/issues/2326#issuecomment-1445138730
|
|
||||||
class ErrorLoggingDatabase implements QueryExecutor {
|
|
||||||
final QueryExecutor inner;
|
|
||||||
final void Function(Object, StackTrace) onError;
|
|
||||||
|
|
||||||
ErrorLoggingDatabase(this.inner, this.onError);
|
|
||||||
|
|
||||||
Future<T> _handleErrors<T>(Future<T> Function() body) {
|
|
||||||
return Future.sync(body)
|
|
||||||
.onError<DriftWrappedException>((error, stackTrace) {
|
|
||||||
onError(error, error.trace ?? stackTrace);
|
|
||||||
throw error;
|
|
||||||
}).onError<DriftRemoteException>((error, stackTrace) {
|
|
||||||
onError(error, error.remoteStackTrace ?? stackTrace);
|
|
||||||
throw error;
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
@override
|
|
||||||
TransactionExecutor beginTransaction() {
|
|
||||||
return _ErrorLoggingTransactionExecutor(inner.beginTransaction(), onError);
|
|
||||||
}
|
|
||||||
|
|
||||||
@override
|
|
||||||
Future<void> close() {
|
|
||||||
return _handleErrors(inner.close);
|
|
||||||
}
|
|
||||||
|
|
||||||
@override
|
|
||||||
SqlDialect get dialect => inner.dialect;
|
|
||||||
|
|
||||||
@override
|
|
||||||
Future<bool> ensureOpen(QueryExecutorUser user) {
|
|
||||||
return _handleErrors(() => inner.ensureOpen(user));
|
|
||||||
}
|
|
||||||
|
|
||||||
@override
|
|
||||||
Future<void> runBatched(BatchedStatements statements) {
|
|
||||||
return _handleErrors(() => inner.runBatched(statements));
|
|
||||||
}
|
|
||||||
|
|
||||||
@override
|
|
||||||
Future<void> runCustom(String statement, [List<Object?>? args]) {
|
|
||||||
return _handleErrors(() => inner.runCustom(statement, args));
|
|
||||||
}
|
|
||||||
|
|
||||||
@override
|
|
||||||
Future<int> runDelete(String statement, List<Object?> args) {
|
|
||||||
return _handleErrors(() => inner.runDelete(statement, args));
|
|
||||||
}
|
|
||||||
|
|
||||||
@override
|
|
||||||
Future<int> runInsert(String statement, List<Object?> args) {
|
|
||||||
return _handleErrors(() => inner.runInsert(statement, args));
|
|
||||||
}
|
|
||||||
|
|
||||||
@override
|
|
||||||
Future<List<Map<String, Object?>>> runSelect(
|
|
||||||
String statement, List<Object?> args) {
|
|
||||||
return _handleErrors(() => inner.runSelect(statement, args));
|
|
||||||
}
|
|
||||||
|
|
||||||
@override
|
|
||||||
Future<int> runUpdate(String statement, List<Object?> args) {
|
|
||||||
return _handleErrors(() => inner.runUpdate(statement, args));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
class _ErrorLoggingTransactionExecutor extends ErrorLoggingDatabase
|
|
||||||
implements TransactionExecutor {
|
|
||||||
final TransactionExecutor transaction;
|
|
||||||
|
|
||||||
_ErrorLoggingTransactionExecutor(
|
|
||||||
this.transaction, void Function(Object, StackTrace) onError)
|
|
||||||
: super(transaction, onError);
|
|
||||||
|
|
||||||
@override
|
|
||||||
Future<void> rollback() {
|
|
||||||
return _handleErrors(transaction.rollback);
|
|
||||||
}
|
|
||||||
|
|
||||||
@override
|
|
||||||
Future<void> send() {
|
|
||||||
return _handleErrors(transaction.send);
|
|
||||||
}
|
|
||||||
|
|
||||||
@override
|
|
||||||
bool get supportsNestedTransactions => transaction.supportsNestedTransactions;
|
|
||||||
}
|
|
||||||
@ -1,567 +0,0 @@
|
|||||||
import '../models/music.dart';
|
|
||||||
import '../models/settings.dart';
|
|
||||||
import '../models/support.dart';
|
|
||||||
import 'converters.dart';
|
|
||||||
|
|
||||||
--
|
|
||||||
-- SCHEMA
|
|
||||||
--
|
|
||||||
|
|
||||||
CREATE TABLE queue(
|
|
||||||
"index" INT NOT NULL PRIMARY KEY UNIQUE,
|
|
||||||
source_id INT NOT NULL,
|
|
||||||
id TEXT NOT NULL,
|
|
||||||
context ENUM(QueueContextType) NOT NULL,
|
|
||||||
context_id TEXT,
|
|
||||||
current_track BOOLEAN UNIQUE
|
|
||||||
);
|
|
||||||
CREATE INDEX queue_index ON queue ("index");
|
|
||||||
CREATE INDEX queue_current_track ON queue ("current_track");
|
|
||||||
|
|
||||||
CREATE TABLE last_audio_state(
|
|
||||||
id INT NOT NULL PRIMARY KEY,
|
|
||||||
queue_mode ENUM(QueueMode) NOT NULL,
|
|
||||||
shuffle_indicies TEXT MAPPED BY `const IListIntConverter()`,
|
|
||||||
repeat ENUM(RepeatMode) NOT NULL
|
|
||||||
);
|
|
||||||
|
|
||||||
CREATE TABLE last_bottom_nav_state(
|
|
||||||
id INT NOT NULL PRIMARY KEY,
|
|
||||||
tab TEXT NOT NULL
|
|
||||||
);
|
|
||||||
|
|
||||||
CREATE TABLE last_library_state(
|
|
||||||
id INT NOT NULL PRIMARY KEY,
|
|
||||||
tab TEXT NOT NULL,
|
|
||||||
albums_list TEXT NOT NULL MAPPED BY `const ListQueryConverter()`,
|
|
||||||
artists_list TEXT NOT NULL MAPPED BY `const ListQueryConverter()`,
|
|
||||||
playlists_list TEXT NOT NULL MAPPED BY `const ListQueryConverter()`,
|
|
||||||
songs_list TEXT NOT NULL MAPPED BY `const ListQueryConverter()`
|
|
||||||
);
|
|
||||||
|
|
||||||
CREATE TABLE app_settings(
|
|
||||||
id INT NOT NULL PRIMARY KEY,
|
|
||||||
max_bitrate_wifi INT NOT NULL,
|
|
||||||
max_bitrate_mobile INT NOT NULL,
|
|
||||||
stream_format TEXT
|
|
||||||
) WITH AppSettings;
|
|
||||||
|
|
||||||
CREATE TABLE sources(
|
|
||||||
id INT NOT NULL PRIMARY KEY AUTOINCREMENT,
|
|
||||||
name TEXT NOT NULL COLLATE NOCASE,
|
|
||||||
address TEXT NOT NULL MAPPED BY `const UriConverter()`,
|
|
||||||
is_active BOOLEAN UNIQUE,
|
|
||||||
created_at DATETIME NOT NULL DEFAULT (strftime('%s', CURRENT_TIMESTAMP))
|
|
||||||
);
|
|
||||||
|
|
||||||
CREATE TABLE subsonic_sources(
|
|
||||||
source_id INT NOT NULL PRIMARY KEY,
|
|
||||||
features TEXT NOT NULL MAPPED BY `const SubsonicFeatureListConverter()`,
|
|
||||||
username TEXT NOT NULL,
|
|
||||||
password TEXT NOT NULL,
|
|
||||||
use_token_auth BOOLEAN NOT NULL DEFAULT 1,
|
|
||||||
FOREIGN KEY (source_id) REFERENCES sources (id) ON DELETE CASCADE
|
|
||||||
);
|
|
||||||
|
|
||||||
CREATE TABLE artists(
|
|
||||||
source_id INT NOT NULL,
|
|
||||||
id TEXT NOT NULL,
|
|
||||||
name TEXT NOT NULL COLLATE NOCASE,
|
|
||||||
album_count INT NOT NULL,
|
|
||||||
starred DATETIME,
|
|
||||||
updated DATETIME NOT NULL DEFAULT (strftime('%s', CURRENT_TIMESTAMP)),
|
|
||||||
PRIMARY KEY (source_id, id),
|
|
||||||
FOREIGN KEY (source_id) REFERENCES sources (id) ON DELETE CASCADE
|
|
||||||
) WITH Artist;
|
|
||||||
CREATE INDEX artists_source_id ON artists (source_id);
|
|
||||||
|
|
||||||
CREATE VIRTUAL TABLE artists_fts USING fts5(source_id, name, content=artists, content_rowid=rowid);
|
|
||||||
|
|
||||||
CREATE TRIGGER artists_ai AFTER INSERT ON artists BEGIN
|
|
||||||
INSERT INTO artists_fts(rowid, source_id, name)
|
|
||||||
VALUES (new.rowid, new.source_id, new.name);
|
|
||||||
END;
|
|
||||||
|
|
||||||
CREATE TRIGGER artists_ad AFTER DELETE ON artists BEGIN
|
|
||||||
INSERT INTO artists_fts(artists_fts, rowid, source_id, name)
|
|
||||||
VALUES('delete', old.rowid, old.source_id, old.name);
|
|
||||||
END;
|
|
||||||
|
|
||||||
CREATE TRIGGER artists_au AFTER UPDATE ON artists BEGIN
|
|
||||||
INSERT INTO artists_fts(artists_fts, rowid, source_id, name)
|
|
||||||
VALUES('delete', old.rowid, old.source_id, old.name);
|
|
||||||
INSERT INTO artists_fts(rowid, source_id, name)
|
|
||||||
VALUES (new.rowid, new.source_id, new.name);
|
|
||||||
END;
|
|
||||||
|
|
||||||
CREATE TABLE albums(
|
|
||||||
source_id INT NOT NULL,
|
|
||||||
id TEXT NOT NULL,
|
|
||||||
artist_id TEXT,
|
|
||||||
name TEXT NOT NULL COLLATE NOCASE,
|
|
||||||
album_artist TEXT COLLATE NOCASE,
|
|
||||||
created DATETIME NOT NULL,
|
|
||||||
cover_art TEXT,
|
|
||||||
genre TEXT,
|
|
||||||
year INT,
|
|
||||||
starred DATETIME,
|
|
||||||
song_count INT NOT NULL,
|
|
||||||
frequent_rank INT,
|
|
||||||
recent_rank INT,
|
|
||||||
is_deleted BOOLEAN NOT NULL DEFAULT 0,
|
|
||||||
updated DATETIME NOT NULL DEFAULT (strftime('%s', CURRENT_TIMESTAMP)),
|
|
||||||
PRIMARY KEY (source_id, id),
|
|
||||||
FOREIGN KEY (source_id) REFERENCES sources (id) ON DELETE CASCADE
|
|
||||||
) WITH Album;
|
|
||||||
CREATE INDEX albums_source_id ON albums (source_id);
|
|
||||||
CREATE INDEX albums_source_id_artist_id_idx ON albums (source_id, artist_id);
|
|
||||||
|
|
||||||
CREATE VIRTUAL TABLE albums_fts USING fts5(source_id, name, content=albums, content_rowid=rowid);
|
|
||||||
|
|
||||||
CREATE TRIGGER albums_ai AFTER INSERT ON albums BEGIN
|
|
||||||
INSERT INTO albums_fts(rowid, source_id, name)
|
|
||||||
VALUES (new.rowid, new.source_id, new.name);
|
|
||||||
END;
|
|
||||||
|
|
||||||
CREATE TRIGGER albums_ad AFTER DELETE ON albums BEGIN
|
|
||||||
INSERT INTO albums_fts(albums_fts, rowid, source_id, name)
|
|
||||||
VALUES('delete', old.rowid, old.source_id, old.name);
|
|
||||||
END;
|
|
||||||
|
|
||||||
CREATE TRIGGER albums_au AFTER UPDATE ON albums BEGIN
|
|
||||||
INSERT INTO albums_fts(albums_fts, rowid, source_id, name)
|
|
||||||
VALUES('delete', old.rowid, old.source_id, old.name);
|
|
||||||
INSERT INTO albums_fts(rowid, source_id, name)
|
|
||||||
VALUES (new.rowid, new.source_id, new.name);
|
|
||||||
END;
|
|
||||||
|
|
||||||
CREATE TABLE playlists(
|
|
||||||
source_id INT NOT NULL,
|
|
||||||
id TEXT NOT NULL,
|
|
||||||
name TEXT NOT NULL COLLATE NOCASE,
|
|
||||||
comment TEXT COLLATE NOCASE,
|
|
||||||
cover_art TEXT,
|
|
||||||
song_count INT NOT NULL,
|
|
||||||
created DATETIME NOT NULL,
|
|
||||||
updated DATETIME NOT NULL DEFAULT (strftime('%s', CURRENT_TIMESTAMP)),
|
|
||||||
PRIMARY KEY (source_id, id),
|
|
||||||
FOREIGN KEY (source_id) REFERENCES sources (id) ON DELETE CASCADE
|
|
||||||
) WITH Playlist;
|
|
||||||
CREATE INDEX playlists_source_id ON playlists (source_id);
|
|
||||||
CREATE TABLE playlist_songs(
|
|
||||||
source_id INT NOT NULL,
|
|
||||||
playlist_id TEXT NOT NULL,
|
|
||||||
song_id TEXT NOT NULL,
|
|
||||||
position INT NOT NULL,
|
|
||||||
updated DATETIME NOT NULL DEFAULT (strftime('%s', CURRENT_TIMESTAMP)),
|
|
||||||
PRIMARY KEY (source_id, playlist_id, position),
|
|
||||||
FOREIGN KEY (source_id) REFERENCES sources (id) ON DELETE CASCADE
|
|
||||||
);
|
|
||||||
CREATE INDEX playlist_songs_source_id_playlist_id_idx ON playlist_songs (source_id, playlist_id);
|
|
||||||
CREATE INDEX playlist_songs_source_id_song_id_idx ON playlist_songs (source_id, song_id);
|
|
||||||
|
|
||||||
CREATE VIRTUAL TABLE playlists_fts USING fts5(source_id, name, content=playlists, content_rowid=rowid);
|
|
||||||
|
|
||||||
CREATE TRIGGER playlists_ai AFTER INSERT ON playlists BEGIN
|
|
||||||
INSERT INTO playlists_fts(rowid, source_id, name)
|
|
||||||
VALUES (new.rowid, new.source_id, new.name);
|
|
||||||
END;
|
|
||||||
|
|
||||||
CREATE TRIGGER playlists_ad AFTER DELETE ON playlists BEGIN
|
|
||||||
INSERT INTO playlists_fts(playlists_fts, rowid, source_id, name)
|
|
||||||
VALUES('delete', old.rowid, old.source_id, old.name);
|
|
||||||
END;
|
|
||||||
|
|
||||||
CREATE TRIGGER playlists_au AFTER UPDATE ON playlists BEGIN
|
|
||||||
INSERT INTO playlists_fts(playlists_fts, rowid, source_id, name)
|
|
||||||
VALUES('delete', old.rowid, old.source_id, old.name);
|
|
||||||
INSERT INTO playlists_fts(rowid, source_id, name)
|
|
||||||
VALUES (new.rowid, new.source_id, new.name);
|
|
||||||
END;
|
|
||||||
|
|
||||||
CREATE TABLE songs(
|
|
||||||
source_id INT NOT NULL,
|
|
||||||
id TEXT NOT NULL,
|
|
||||||
album_id TEXT,
|
|
||||||
artist_id TEXT,
|
|
||||||
title TEXT NOT NULL COLLATE NOCASE,
|
|
||||||
album TEXT COLLATE NOCASE,
|
|
||||||
artist TEXT COLLATE NOCASE,
|
|
||||||
duration INT MAPPED BY `const DurationSecondsConverter()`,
|
|
||||||
track INT,
|
|
||||||
disc INT,
|
|
||||||
starred DATETIME,
|
|
||||||
genre TEXT,
|
|
||||||
download_task_id TEXT UNIQUE,
|
|
||||||
download_file_path TEXT UNIQUE,
|
|
||||||
is_deleted BOOLEAN NOT NULL DEFAULT 0,
|
|
||||||
updated DATETIME NOT NULL DEFAULT (strftime('%s', CURRENT_TIMESTAMP)),
|
|
||||||
PRIMARY KEY (source_id, id),
|
|
||||||
FOREIGN KEY (source_id) REFERENCES sources (id) ON DELETE CASCADE
|
|
||||||
) WITH Song;
|
|
||||||
CREATE INDEX songs_source_id_album_id_idx ON songs (source_id, album_id);
|
|
||||||
CREATE INDEX songs_source_id_artist_id_idx ON songs (source_id, artist_id);
|
|
||||||
CREATE INDEX songs_download_task_id_idx ON songs (download_task_id);
|
|
||||||
|
|
||||||
CREATE VIRTUAL TABLE songs_fts USING fts5(source_id, title, content=songs, content_rowid=rowid);
|
|
||||||
|
|
||||||
CREATE TRIGGER songs_ai AFTER INSERT ON songs BEGIN
|
|
||||||
INSERT INTO songs_fts(rowid, source_id, title)
|
|
||||||
VALUES (new.rowid, new.source_id, new.title);
|
|
||||||
END;
|
|
||||||
|
|
||||||
CREATE TRIGGER songs_ad AFTER DELETE ON songs BEGIN
|
|
||||||
INSERT INTO songs_fts(songs_fts, rowid, source_id, title)
|
|
||||||
VALUES('delete', old.rowid, old.source_id, old.title);
|
|
||||||
END;
|
|
||||||
|
|
||||||
CREATE TRIGGER songs_au AFTER UPDATE ON songs BEGIN
|
|
||||||
INSERT INTO songs_fts(songs_fts, rowid, source_id, title)
|
|
||||||
VALUES('delete', old.rowid, old.source_id, old.title);
|
|
||||||
INSERT INTO songs_fts(rowid, source_id, title)
|
|
||||||
VALUES (new.rowid, new.source_id, new.title);
|
|
||||||
END;
|
|
||||||
|
|
||||||
--
|
|
||||||
-- QUERIES
|
|
||||||
--
|
|
||||||
|
|
||||||
sourcesCount:
|
|
||||||
SELECT COUNT(*)
|
|
||||||
FROM sources;
|
|
||||||
|
|
||||||
allSubsonicSources WITH SubsonicSettings:
|
|
||||||
SELECT
|
|
||||||
sources.id,
|
|
||||||
sources.name,
|
|
||||||
sources.address,
|
|
||||||
sources.is_active,
|
|
||||||
sources.created_at,
|
|
||||||
subsonic_sources.features,
|
|
||||||
subsonic_sources.username,
|
|
||||||
subsonic_sources.password,
|
|
||||||
subsonic_sources.use_token_auth
|
|
||||||
FROM sources
|
|
||||||
JOIN subsonic_sources ON subsonic_sources.source_id = sources.id;
|
|
||||||
|
|
||||||
albumIdsWithDownloadStatus:
|
|
||||||
SELECT albums.id
|
|
||||||
FROM albums
|
|
||||||
JOIN songs on songs.source_id = albums.source_id AND songs.album_id = albums.id
|
|
||||||
WHERE
|
|
||||||
albums.source_id = :source_id
|
|
||||||
AND (songs.download_file_path IS NOT NULL OR songs.download_task_id IS NOT NULL)
|
|
||||||
GROUP BY albums.id;
|
|
||||||
|
|
||||||
artistIdsWithDownloadStatus:
|
|
||||||
SELECT artists.id
|
|
||||||
FROM artists
|
|
||||||
LEFT JOIN albums ON artists.source_id = albums.source_id AND artists.id = albums.artist_id
|
|
||||||
LEFT JOIN songs ON albums.source_id = songs.source_id AND albums.id = songs.album_id
|
|
||||||
WHERE
|
|
||||||
artists.source_id = :source_id
|
|
||||||
AND (songs.download_file_path IS NOT NULL OR songs.download_task_id IS NOT NULL)
|
|
||||||
GROUP BY artists.id;
|
|
||||||
|
|
||||||
playlistIdsWithDownloadStatus:
|
|
||||||
SELECT playlists.id
|
|
||||||
FROM playlists
|
|
||||||
LEFT JOIN playlist_songs ON playlist_songs.source_id = playlists.source_id AND playlist_songs.playlist_id = playlists.id
|
|
||||||
LEFT JOIN songs ON playlist_songs.source_id = songs.source_id AND playlist_songs.song_id = songs.id
|
|
||||||
WHERE
|
|
||||||
playlists.source_id = :source_id
|
|
||||||
AND (songs.download_file_path IS NOT NULL OR songs.download_task_id IS NOT NULL)
|
|
||||||
GROUP BY playlists.id;
|
|
||||||
|
|
||||||
searchArtists:
|
|
||||||
SELECT rowid
|
|
||||||
FROM artists_fts
|
|
||||||
WHERE artists_fts MATCH :query
|
|
||||||
ORDER BY rank
|
|
||||||
LIMIT :limit OFFSET :offset;
|
|
||||||
|
|
||||||
searchAlbums:
|
|
||||||
SELECT rowid
|
|
||||||
FROM albums_fts
|
|
||||||
WHERE albums_fts MATCH :query
|
|
||||||
ORDER BY rank
|
|
||||||
LIMIT :limit OFFSET :offset;
|
|
||||||
|
|
||||||
searchPlaylists:
|
|
||||||
SELECT rowid
|
|
||||||
FROM playlists_fts
|
|
||||||
WHERE playlists_fts MATCH :query
|
|
||||||
ORDER BY rank
|
|
||||||
LIMIT :limit OFFSET :offset;
|
|
||||||
|
|
||||||
searchSongs:
|
|
||||||
SELECT rowid
|
|
||||||
FROM songs_fts
|
|
||||||
WHERE songs_fts MATCH :query
|
|
||||||
ORDER BY rank
|
|
||||||
LIMIT :limit OFFSET :offset;
|
|
||||||
|
|
||||||
artistById:
|
|
||||||
SELECT * FROM artists
|
|
||||||
WHERE source_id = :source_id AND id = :id;
|
|
||||||
|
|
||||||
albumById:
|
|
||||||
SELECT * FROM albums
|
|
||||||
WHERE source_id = :source_id AND id = :id;
|
|
||||||
|
|
||||||
albumsByArtistId:
|
|
||||||
SELECT * FROM albums
|
|
||||||
WHERE source_id = :source_id AND artist_id = :artist_id;
|
|
||||||
|
|
||||||
albumsInIds:
|
|
||||||
SELECT * FROM albums
|
|
||||||
WHERE source_id = :source_id AND id IN :ids;
|
|
||||||
|
|
||||||
playlistById:
|
|
||||||
SELECT * FROM playlists
|
|
||||||
WHERE source_id = :source_id AND id = :id;
|
|
||||||
|
|
||||||
songById:
|
|
||||||
SELECT * FROM songs
|
|
||||||
WHERE source_id = :source_id AND id = :id;
|
|
||||||
|
|
||||||
albumGenres:
|
|
||||||
SELECT
|
|
||||||
genre
|
|
||||||
FROM albums
|
|
||||||
WHERE genre IS NOT NULL AND source_id = :source_id
|
|
||||||
GROUP BY genre
|
|
||||||
ORDER BY COUNT(genre) DESC
|
|
||||||
LIMIT :limit OFFSET :offset;
|
|
||||||
|
|
||||||
albumsByGenre:
|
|
||||||
SELECT
|
|
||||||
albums.*
|
|
||||||
FROM albums
|
|
||||||
JOIN songs ON albums.source_id = songs.source_id AND albums.id = songs.album_id
|
|
||||||
WHERE songs.source_id = :source_id AND songs.genre = :genre
|
|
||||||
GROUP BY albums.id
|
|
||||||
ORDER BY albums.created DESC, albums.name
|
|
||||||
LIMIT :limit OFFSET :offset;
|
|
||||||
|
|
||||||
filterSongsByGenre:
|
|
||||||
SELECT
|
|
||||||
songs.*
|
|
||||||
FROM songs
|
|
||||||
JOIN albums ON albums.source_id = songs.source_id AND albums.id = songs.album_id
|
|
||||||
WHERE $predicate
|
|
||||||
ORDER BY $order
|
|
||||||
LIMIT $limit;
|
|
||||||
|
|
||||||
songsByGenreCount:
|
|
||||||
SELECT
|
|
||||||
COUNT(*)
|
|
||||||
FROM songs
|
|
||||||
WHERE songs.source_id = :source_id AND songs.genre = :genre;
|
|
||||||
|
|
||||||
songsWithDownloadTasks:
|
|
||||||
SELECT * FROM songs
|
|
||||||
WHERE download_task_id IS NOT NULL;
|
|
||||||
|
|
||||||
songByDownloadTask:
|
|
||||||
SELECT * FROM songs
|
|
||||||
WHERE download_task_id = :task_id;
|
|
||||||
|
|
||||||
clearSongDownloadTaskBySong:
|
|
||||||
UPDATE songs SET
|
|
||||||
download_task_id = NULL
|
|
||||||
WHERE source_id = :source_id AND id = :id;
|
|
||||||
|
|
||||||
completeSongDownload:
|
|
||||||
UPDATE songs SET
|
|
||||||
download_task_id = NULL,
|
|
||||||
download_file_path = :file_path
|
|
||||||
WHERE download_task_id = :task_id;
|
|
||||||
|
|
||||||
clearSongDownloadTask:
|
|
||||||
UPDATE songs SET
|
|
||||||
download_task_id = NULL,
|
|
||||||
download_file_path = NULL
|
|
||||||
WHERE download_task_id = :task_id;
|
|
||||||
|
|
||||||
updateSongDownloadTask:
|
|
||||||
UPDATE songs SET
|
|
||||||
download_task_id = :task_id
|
|
||||||
WHERE source_id = :source_id AND id = :id;
|
|
||||||
|
|
||||||
deleteSongDownloadFile:
|
|
||||||
UPDATE songs SET
|
|
||||||
download_task_id = NULL,
|
|
||||||
download_file_path = NULL
|
|
||||||
WHERE source_id = :source_id AND id = :id;
|
|
||||||
|
|
||||||
albumDownloadStatus WITH ListDownloadStatus:
|
|
||||||
SELECT
|
|
||||||
COUNT(*) as total,
|
|
||||||
COUNT(CASE WHEN songs.download_file_path IS NOT NULL THEN songs.id ELSE NULL END) AS downloaded,
|
|
||||||
COUNT(CASE WHEN songs.download_task_id IS NOT NULL THEN songs.id ELSE NULL END) AS downloading
|
|
||||||
FROM albums
|
|
||||||
JOIN songs ON albums.source_id = songs.source_id AND albums.id = songs.album_id
|
|
||||||
WHERE albums.source_id = :source_id AND albums.id = :id;
|
|
||||||
|
|
||||||
playlistDownloadStatus WITH ListDownloadStatus:
|
|
||||||
SELECT
|
|
||||||
COUNT(DISTINCT songs.id) as total,
|
|
||||||
COUNT(DISTINCT CASE WHEN songs.download_file_path IS NOT NULL THEN songs.id ELSE NULL END) AS downloaded,
|
|
||||||
COUNT(DISTINCT CASE WHEN songs.download_task_id IS NOT NULL THEN songs.id ELSE NULL END) AS downloading
|
|
||||||
FROM playlists
|
|
||||||
JOIN playlist_songs ON
|
|
||||||
playlist_songs.source_id = playlists.source_id
|
|
||||||
AND playlist_songs.playlist_id = playlists.id
|
|
||||||
JOIN songs ON
|
|
||||||
songs.source_id = playlist_songs.source_id
|
|
||||||
AND songs.id = playlist_songs.song_id
|
|
||||||
WHERE
|
|
||||||
playlists.source_id = :source_id AND playlists.id = :id;
|
|
||||||
|
|
||||||
filterAlbums:
|
|
||||||
SELECT
|
|
||||||
albums.*
|
|
||||||
FROM albums
|
|
||||||
WHERE $predicate
|
|
||||||
ORDER BY $order
|
|
||||||
LIMIT $limit;
|
|
||||||
|
|
||||||
filterAlbumsDownloaded:
|
|
||||||
SELECT
|
|
||||||
albums.*
|
|
||||||
FROM albums
|
|
||||||
LEFT JOIN songs ON albums.source_id = songs.source_id AND albums.id = songs.album_id
|
|
||||||
WHERE $predicate
|
|
||||||
GROUP BY albums.source_id, albums.id
|
|
||||||
HAVING SUM(CASE WHEN songs.download_file_path IS NOT NULL THEN 1 ELSE 0 END) > 0
|
|
||||||
ORDER BY $order
|
|
||||||
LIMIT $limit;
|
|
||||||
|
|
||||||
filterArtists:
|
|
||||||
SELECT
|
|
||||||
artists.*
|
|
||||||
FROM artists
|
|
||||||
WHERE $predicate
|
|
||||||
ORDER BY $order
|
|
||||||
LIMIT $limit;
|
|
||||||
|
|
||||||
filterArtistsDownloaded WITH Artist:
|
|
||||||
SELECT
|
|
||||||
artists.*,
|
|
||||||
COUNT(DISTINCT CASE WHEN songs.download_file_path IS NOT NULL THEN songs.album_id ELSE NULL END) AS album_count
|
|
||||||
FROM artists
|
|
||||||
LEFT JOIN albums ON artists.source_id = albums.source_id AND artists.id = albums.artist_id
|
|
||||||
LEFT JOIN songs ON albums.source_id = songs.source_id AND albums.id = songs.album_id
|
|
||||||
WHERE $predicate
|
|
||||||
GROUP BY artists.source_id, artists.id
|
|
||||||
HAVING SUM(CASE WHEN songs.download_file_path IS NOT NULL THEN 1 ELSE 0 END) > 0
|
|
||||||
ORDER BY $order
|
|
||||||
LIMIT $limit;
|
|
||||||
|
|
||||||
filterPlaylists:
|
|
||||||
SELECT
|
|
||||||
playlists.*
|
|
||||||
FROM playlists
|
|
||||||
WHERE $predicate
|
|
||||||
ORDER BY $order
|
|
||||||
LIMIT $limit;
|
|
||||||
|
|
||||||
filterPlaylistsDownloaded WITH Playlist:
|
|
||||||
SELECT
|
|
||||||
playlists.*,
|
|
||||||
COUNT(CASE WHEN songs.download_file_path IS NOT NULL THEN songs.id ELSE NULL END) AS song_count
|
|
||||||
FROM playlists
|
|
||||||
LEFT JOIN playlist_songs ON playlist_songs.source_id = playlists.source_id AND playlist_songs.playlist_id = playlists.id
|
|
||||||
LEFT JOIN songs ON playlist_songs.source_id = songs.source_id AND playlist_songs.song_id = songs.id
|
|
||||||
WHERE $predicate
|
|
||||||
GROUP BY playlists.source_id, playlists.id
|
|
||||||
HAVING SUM(CASE WHEN songs.download_file_path IS NOT NULL THEN 1 ELSE 0 END) > 0
|
|
||||||
ORDER BY $order
|
|
||||||
LIMIT $limit;
|
|
||||||
|
|
||||||
filterSongs:
|
|
||||||
SELECT
|
|
||||||
songs.*
|
|
||||||
FROM songs
|
|
||||||
WHERE $predicate
|
|
||||||
ORDER BY $order
|
|
||||||
LIMIT $limit;
|
|
||||||
|
|
||||||
filterSongsDownloaded:
|
|
||||||
SELECT
|
|
||||||
songs.*
|
|
||||||
FROM songs
|
|
||||||
WHERE $predicate AND songs.download_file_path IS NOT NULL
|
|
||||||
ORDER BY $order
|
|
||||||
LIMIT $limit;
|
|
||||||
|
|
||||||
playlistIsDownloaded:
|
|
||||||
SELECT
|
|
||||||
COUNT(*) = 0
|
|
||||||
FROM playlists
|
|
||||||
JOIN playlist_songs ON
|
|
||||||
playlist_songs.source_id = playlists.source_id
|
|
||||||
AND playlist_songs.playlist_id = playlists.id
|
|
||||||
JOIN songs ON
|
|
||||||
songs.source_id = playlist_songs.source_id
|
|
||||||
AND songs.id = playlist_songs.song_id
|
|
||||||
WHERE
|
|
||||||
playlists.source_id = :source_id AND playlists.id = :id
|
|
||||||
AND songs.download_file_path IS NULL;
|
|
||||||
|
|
||||||
playlistHasDownloadsInProgress:
|
|
||||||
SELECT
|
|
||||||
COUNT(*) > 0
|
|
||||||
FROM playlists
|
|
||||||
JOIN playlist_songs ON
|
|
||||||
playlist_songs.source_id = playlists.source_id
|
|
||||||
AND playlist_songs.playlist_id = playlists.id
|
|
||||||
JOIN songs ON
|
|
||||||
songs.source_id = playlist_songs.source_id
|
|
||||||
AND songs.id = playlist_songs.song_id
|
|
||||||
WHERE playlists.source_id = :source_id AND playlists.id = :id
|
|
||||||
AND songs.download_task_id IS NOT NULL;
|
|
||||||
|
|
||||||
songsInIds:
|
|
||||||
SELECT *
|
|
||||||
FROM songs
|
|
||||||
WHERE source_id = :source_id AND id IN :ids;
|
|
||||||
|
|
||||||
songsInRowIds:
|
|
||||||
SELECT *
|
|
||||||
FROM songs
|
|
||||||
WHERE ROWID IN :row_ids;
|
|
||||||
|
|
||||||
albumsInRowIds:
|
|
||||||
SELECT *
|
|
||||||
FROM albums
|
|
||||||
WHERE ROWID IN :row_ids;
|
|
||||||
|
|
||||||
artistsInRowIds:
|
|
||||||
SELECT *
|
|
||||||
FROM artists
|
|
||||||
WHERE ROWID IN :row_ids;
|
|
||||||
|
|
||||||
playlistsInRowIds:
|
|
||||||
SELECT *
|
|
||||||
FROM playlists
|
|
||||||
WHERE ROWID IN :row_ids;
|
|
||||||
|
|
||||||
currentTrackIndex:
|
|
||||||
SELECT
|
|
||||||
queue."index"
|
|
||||||
FROM queue
|
|
||||||
WHERE queue.current_track = 1;
|
|
||||||
|
|
||||||
queueLength:
|
|
||||||
SELECT COUNT(*) FROM queue;
|
|
||||||
|
|
||||||
queueInIndicies:
|
|
||||||
SELECT *
|
|
||||||
FROM queue
|
|
||||||
WHERE queue."index" IN :indicies;
|
|
||||||
|
|
||||||
getAppSettings:
|
|
||||||
SELECT * FROM app_settings
|
|
||||||
WHERE id = 1;
|
|
||||||